diff --git a/src/Platform/Microsoft.Testing.Extensions.TrxReport/TrxReportEngine.cs b/src/Platform/Microsoft.Testing.Extensions.TrxReport/TrxReportEngine.cs index 7ffe805ea8..94a0a8b16b 100644 --- a/src/Platform/Microsoft.Testing.Extensions.TrxReport/TrxReportEngine.cs +++ b/src/Platform/Microsoft.Testing.Extensions.TrxReport/TrxReportEngine.cs @@ -352,19 +352,11 @@ private async Task CopyArtifactIntoTrxDirectoryAndReturnHrefValueAsync(F string fileName = artifact.Name; string destination = Path.Combine(artifactDirectory, fileName); - int nameCounter = 0; // If the file already exists, append a number to the end of the file name - while (true) + for (int nameCounter = 1; File.Exists(destination) && nameCounter <= 10; nameCounter++) { - if (File.Exists(destination)) - { - nameCounter++; - destination = Path.Combine(artifactDirectory, $"{Path.GetFileNameWithoutExtension(fileName)}_{nameCounter}{Path.GetExtension(fileName)}"); - continue; - } - - break; + destination = Path.Combine(artifactDirectory, $"{Path.GetFileNameWithoutExtension(fileName)}_{nameCounter}{Path.GetExtension(fileName)}"); } await CopyFileAsync(artifact, new FileInfo(destination)).ConfigureAwait(false); @@ -536,7 +528,7 @@ private void AddResults(string testAppModule, XElement testRun, out XElement tes resultFiles ??= new XElement("ResultFiles"); resultFiles.Add(new XElement( "ResultFile", - new XAttribute("path", testFileArtifact.FileInfo.FullName))); + new XAttribute("path", testFileArtifact.FileInfo.Name))); } if (resultFiles is not null) diff --git a/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs b/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs index beb17eeeea..3c3f7d7d68 100644 --- a/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs +++ b/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs @@ -720,6 +720,9 @@ private static async Task BuildTestFrameworkAsync(TestFrameworkB await RegisterAsServiceOrConsumerOrBothAsync(testFrameworkBuilderData.TestExecutionRequestInvoker, serviceProvider, dataConsumersBuilder).ConfigureAwait(false); await RegisterAsServiceOrConsumerOrBothAsync(testFrameworkBuilderData.TestExecutionFilterFactory, serviceProvider, dataConsumersBuilder).ConfigureAwait(false); + // Register consumer that copies per-test file artifacts to the results directory. + dataConsumersBuilder.Add(new FileArtifactCopyDataConsumer(serviceProvider.GetConfiguration())); + // Create the test framework adapter ITestFrameworkCapabilities testFrameworkCapabilities = serviceProvider.GetTestFrameworkCapabilities(); ITestFramework testFramework = testFrameworkBuilderData.TestFrameworkManager.TestFrameworkFactory(testFrameworkCapabilities, serviceProvider); diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx b/src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx index 5236750662..f8cf35d536 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx +++ b/src/Platform/Microsoft.Testing.Platform/Resources/PlatformResources.resx @@ -746,4 +746,10 @@ Takes one argument as string in the format <value>[h|m|s] where 'value' is Waiting for debugger to attach (TESTINGPLATFORM_WAIT_ATTACH_DEBUGGER=1) is not supported in browser environments. + + File artifact copy + + + Copies per-test file artifacts to the test results directory. + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf index 85fc8d4bcf..694b5bf8af 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.cs.xlf @@ -296,6 +296,16 @@ selhalo s {0} upozorněním(i). + + Copies per-test file artifacts to the test results directory. + Copies per-test file artifacts to the test results directory. + + + + File artifact copy + File artifact copy + + Finished test session. Testovací relace byla dokončena. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf index 158af32848..68995fcf50 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.de.xlf @@ -296,6 +296,16 @@ fehlerhaft mit {0} Warnung(en) + + Copies per-test file artifacts to the test results directory. + Copies per-test file artifacts to the test results directory. + + + + File artifact copy + File artifact copy + + Finished test session. Die Testsitzung wurde beendet. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf index 18f14a67fb..19b2fb0ce7 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.es.xlf @@ -296,6 +296,16 @@ error con {0} advertencias + + Copies per-test file artifacts to the test results directory. + Copies per-test file artifacts to the test results directory. + + + + File artifact copy + File artifact copy + + Finished test session. Finalizó la sesión de prueba. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf index 291f760766..0791282570 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.fr.xlf @@ -296,6 +296,16 @@ a échoué avec {0} avertissement(s) + + Copies per-test file artifacts to the test results directory. + Copies per-test file artifacts to the test results directory. + + + + File artifact copy + File artifact copy + + Finished test session. Session de test terminée. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf index 4995771b47..332096d7d7 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.it.xlf @@ -296,6 +296,16 @@ non riuscito con {0} avvisi + + Copies per-test file artifacts to the test results directory. + Copies per-test file artifacts to the test results directory. + + + + File artifact copy + File artifact copy + + Finished test session. Sessione di test completata. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf index 953110e668..58d0dbaf3b 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ja.xlf @@ -296,6 +296,16 @@ {0} 件の警告付きで失敗しました + + Copies per-test file artifacts to the test results directory. + Copies per-test file artifacts to the test results directory. + + + + File artifact copy + File artifact copy + + Finished test session. テスト セッションを終了しました。 diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf index f8fe5c3ed4..08b28bff56 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ko.xlf @@ -296,6 +296,16 @@ {0} 경고와 함께 실패 + + Copies per-test file artifacts to the test results directory. + Copies per-test file artifacts to the test results directory. + + + + File artifact copy + File artifact copy + + Finished test session. 테스트 세션을 마쳤습니다. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf index 1256812f80..960fd3018b 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pl.xlf @@ -296,6 +296,16 @@ zakończono niepowodzeniem, z ostrzeżeniami w liczbie: {0} + + Copies per-test file artifacts to the test results directory. + Copies per-test file artifacts to the test results directory. + + + + File artifact copy + File artifact copy + + Finished test session. Zakończono sesję testą. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf index da56e750e5..002731c247 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.pt-BR.xlf @@ -296,6 +296,16 @@ falhou com {0} aviso(s) + + Copies per-test file artifacts to the test results directory. + Copies per-test file artifacts to the test results directory. + + + + File artifact copy + File artifact copy + + Finished test session. Sessão de teste concluída. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf index 749658de47..70b35c815a 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.ru.xlf @@ -296,6 +296,16 @@ сбой с предупреждениями ({0}) + + Copies per-test file artifacts to the test results directory. + Copies per-test file artifacts to the test results directory. + + + + File artifact copy + File artifact copy + + Finished test session. Тестовый сеанс завершен. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf index beecd40ddd..0d4c614d76 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.tr.xlf @@ -296,6 +296,16 @@ {0} uyarıyla başarısız oldu + + Copies per-test file artifacts to the test results directory. + Copies per-test file artifacts to the test results directory. + + + + File artifact copy + File artifact copy + + Finished test session. Test oturumu bitti. diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf index 633a4dd6ff..e5a32ec872 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hans.xlf @@ -296,6 +296,16 @@ 失败,出现 {0} 警告 + + Copies per-test file artifacts to the test results directory. + Copies per-test file artifacts to the test results directory. + + + + File artifact copy + File artifact copy + + Finished test session. 已完成测试会话。 diff --git a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf index 533e4a732f..f9d1b246df 100644 --- a/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf +++ b/src/Platform/Microsoft.Testing.Platform/Resources/xlf/PlatformResources.zh-Hant.xlf @@ -296,6 +296,16 @@ 失敗,有 {0} 個警告 + + Copies per-test file artifacts to the test results directory. + Copies per-test file artifacts to the test results directory. + + + + File artifact copy + File artifact copy + + Finished test session. 已完成測試會話。 diff --git a/src/Platform/Microsoft.Testing.Platform/TestHost/FileArtifactCopyDataConsumer.cs b/src/Platform/Microsoft.Testing.Platform/TestHost/FileArtifactCopyDataConsumer.cs new file mode 100644 index 0000000000..f6f63f2c7b --- /dev/null +++ b/src/Platform/Microsoft.Testing.Platform/TestHost/FileArtifactCopyDataConsumer.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Testing.Platform.Configurations; +using Microsoft.Testing.Platform.Extensions; +using Microsoft.Testing.Platform.Extensions.Messages; +using Microsoft.Testing.Platform.Helpers; +using Microsoft.Testing.Platform.Resources; + +namespace Microsoft.Testing.Platform.TestHost; + +/// +/// Copies per-test file artifacts to the test results directory so that the +/// results directory is self-contained. +/// +internal sealed class FileArtifactCopyDataConsumer : IDataConsumer +{ + private readonly IConfiguration _configuration; + + public FileArtifactCopyDataConsumer(IConfiguration configuration) + => _configuration = configuration; + + public Type[] DataTypesConsumed => [typeof(TestNodeUpdateMessage)]; + + public string Uid => nameof(FileArtifactCopyDataConsumer); + + public string Version => AppVersion.DefaultSemVer; + + public string DisplayName => PlatformResources.FileArtifactCopyDataConsumerDisplayName; + + public string Description => PlatformResources.FileArtifactCopyDataConsumerDescription; + + public Task IsEnabledAsync() => Task.FromResult(true); + + public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken) + { + if (value is not TestNodeUpdateMessage message) + { + return Task.CompletedTask; + } + + FileArtifactProperty[] artifacts = message.TestNode.Properties.OfType(); + if (artifacts.Length == 0) + { + return Task.CompletedTask; + } + + string resultsDirectory = _configuration.GetTestResultDirectory(); + + foreach (FileArtifactProperty artifact in artifacts) + { + CopyFileToResultsDirectory(artifact.FileInfo, resultsDirectory); + } + + return Task.CompletedTask; + } + + private static void CopyFileToResultsDirectory(FileInfo file, string resultsDirectory) + { + // If the file is already under the results directory, skip. + string fullResultsDirectory = Path.GetFullPath(resultsDirectory); + if (file.FullName.StartsWith(fullResultsDirectory, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + if (!Directory.Exists(resultsDirectory)) + { + Directory.CreateDirectory(resultsDirectory); + } + + string destination = Path.Combine(resultsDirectory, file.Name); + for (int nameCounter = 1; File.Exists(destination) && nameCounter <= 10; nameCounter++) + { + destination = Path.Combine(resultsDirectory, $"{Path.GetFileNameWithoutExtension(file.Name)}_{nameCounter}{Path.GetExtension(file.Name)}"); + } + + File.Copy(file.FullName, destination, overwrite: false); + } +} diff --git a/test/UnitTests/Microsoft.Testing.Extensions.UnitTests/TrxTests.cs b/test/UnitTests/Microsoft.Testing.Extensions.UnitTests/TrxTests.cs index 4769158d12..1a0af13863 100644 --- a/test/UnitTests/Microsoft.Testing.Extensions.UnitTests/TrxTests.cs +++ b/test/UnitTests/Microsoft.Testing.Extensions.UnitTests/TrxTests.cs @@ -473,11 +473,11 @@ public async Task TrxReportEngine_GenerateReportAsync_WithArtifactsByTestNode_Tr string trxContentsPattern = @" - + "; - Assert.IsTrue(Regex.IsMatch(trxContent, trxContentsPattern)); + Assert.IsTrue(Regex.IsMatch(trxContent, trxContentsPattern), trxContent); } [TestMethod] diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/TestHost/FileArtifactCopyDataConsumerTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/TestHost/FileArtifactCopyDataConsumerTests.cs new file mode 100644 index 0000000000..77b9059c54 --- /dev/null +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/TestHost/FileArtifactCopyDataConsumerTests.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Testing.Platform.Configurations; +using Microsoft.Testing.Platform.Extensions; +using Microsoft.Testing.Platform.Extensions.Messages; +using Microsoft.Testing.Platform.TestHost; + +using Moq; + +namespace Microsoft.Testing.Platform.UnitTests; + +[TestClass] +public sealed class FileArtifactCopyDataConsumerTests : IDisposable +{ + private readonly string _resultsDirectory; + private readonly string _sourceDirectory; + private readonly FileArtifactCopyDataConsumer _consumer; + + public FileArtifactCopyDataConsumerTests() + { + _resultsDirectory = Path.Combine(Path.GetTempPath(), $"TestResults_{Guid.NewGuid():N}"); + _sourceDirectory = Path.Combine(Path.GetTempPath(), $"TestSource_{Guid.NewGuid():N}"); + + Directory.CreateDirectory(_resultsDirectory); + Directory.CreateDirectory(_sourceDirectory); + + Mock configMock = new(); + configMock.Setup(c => c[PlatformConfigurationConstants.PlatformResultDirectory]).Returns(_resultsDirectory); + + _consumer = new FileArtifactCopyDataConsumer(configMock.Object); + } + + public void Dispose() + { + if (Directory.Exists(_resultsDirectory)) + { + Directory.Delete(_resultsDirectory, recursive: true); + } + + if (Directory.Exists(_sourceDirectory)) + { + Directory.Delete(_sourceDirectory, recursive: true); + } + } + + [TestMethod] + public async Task IsEnabledAsync_ReturnsTrue() + { + bool isEnabled = await _consumer.IsEnabledAsync(); + + Assert.IsTrue(isEnabled); + } + + [TestMethod] + public async Task ConsumeAsync_WithNoFileArtifacts_DoesNotCopyAnything() + { + TestNodeUpdateMessage message = CreateMessage(new PropertyBag(new PassedTestNodeStateProperty())); + + await _consumer.ConsumeAsync(new DummyProducer(), message, CancellationToken.None); + + Assert.AreEqual(0, Directory.GetFiles(_resultsDirectory).Length); + } + + [TestMethod] + public async Task ConsumeAsync_WithFileArtifact_CopiesFileToResultsDirectory() + { + string sourceFile = CreateSourceFile("TestOutput.txt", "test content"); + TestNodeUpdateMessage message = CreateMessage(new PropertyBag( + new PassedTestNodeStateProperty(), + new FileArtifactProperty(new FileInfo(sourceFile), "TestOutput.txt"))); + + await _consumer.ConsumeAsync(new DummyProducer(), message, CancellationToken.None); + + string expectedDestination = Path.Combine(_resultsDirectory, "TestOutput.txt"); + Assert.IsTrue(File.Exists(expectedDestination)); + Assert.AreEqual("test content", File.ReadAllText(expectedDestination)); + } + + [TestMethod] + public async Task ConsumeAsync_WithFileAlreadyInResultsDirectory_DoesNotCopyAgain() + { + string fileInResults = Path.Combine(_resultsDirectory, "AlreadyHere.txt"); + File.WriteAllText(fileInResults, "already here"); + + TestNodeUpdateMessage message = CreateMessage(new PropertyBag( + new PassedTestNodeStateProperty(), + new FileArtifactProperty(new FileInfo(fileInResults), "AlreadyHere.txt"))); + + await _consumer.ConsumeAsync(new DummyProducer(), message, CancellationToken.None); + + Assert.AreEqual(1, Directory.GetFiles(_resultsDirectory).Length); + Assert.AreEqual("already here", File.ReadAllText(fileInResults)); + } + + [TestMethod] + public async Task ConsumeAsync_WithNonTestNodeUpdateMessage_DoesNothing() + { + Mock dataMock = new(); + + await _consumer.ConsumeAsync(new DummyProducer(), dataMock.Object, CancellationToken.None); + + Assert.AreEqual(0, Directory.GetFiles(_resultsDirectory).Length); + } + + [TestMethod] + public async Task ConsumeAsync_WithDuplicateFileName_AppendsCounter() + { + // Pre-create a file in the results directory with the same name. + File.WriteAllText(Path.Combine(_resultsDirectory, "Duplicate.txt"), "original"); + + string sourceFile = CreateSourceFile("Duplicate.txt", "new content"); + TestNodeUpdateMessage message = CreateMessage(new PropertyBag( + new PassedTestNodeStateProperty(), + new FileArtifactProperty(new FileInfo(sourceFile), "Duplicate.txt"))); + + await _consumer.ConsumeAsync(new DummyProducer(), message, CancellationToken.None); + + string expectedDestination = Path.Combine(_resultsDirectory, "Duplicate_1.txt"); + Assert.IsTrue(File.Exists(expectedDestination)); + Assert.AreEqual("new content", File.ReadAllText(expectedDestination)); + } + + private string CreateSourceFile(string name, string content) + { + string path = Path.Combine(_sourceDirectory, name); + File.WriteAllText(path, content); + return path; + } + + private static TestNodeUpdateMessage CreateMessage(PropertyBag properties) + => new( + default, + new TestNode + { + Uid = new TestNodeUid("test-id"), + DisplayName = "TestMethod", + Properties = properties, + }); + + private sealed class DummyProducer : IDataProducer + { + public Type[] DataTypesProduced => [typeof(TestNodeUpdateMessage)]; + + public string Uid => nameof(DummyProducer); + + public string Version => "1.0.0"; + + public string DisplayName => string.Empty; + + public string Description => string.Empty; + + public Task IsEnabledAsync() => Task.FromResult(true); + } +}