diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsArtifactUploader.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsArtifactUploader.cs new file mode 100644 index 0000000000..affb688712 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsArtifactUploader.cs @@ -0,0 +1,357 @@ +// 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.Extensions.FileSystemGlobbing; +using Microsoft.Testing.Extensions.AzureDevOpsReport.Resources; +using Microsoft.Testing.Extensions.Reporting; +using Microsoft.Testing.Platform; +using Microsoft.Testing.Platform.CommandLine; +using Microsoft.Testing.Platform.Configurations; +using Microsoft.Testing.Platform.Extensions; +using Microsoft.Testing.Platform.Extensions.Messages; +using Microsoft.Testing.Platform.Extensions.OutputDevice; +using Microsoft.Testing.Platform.Extensions.TestHost; +using Microsoft.Testing.Platform.Helpers; +using Microsoft.Testing.Platform.Logging; +using Microsoft.Testing.Platform.OutputDevice; +using Microsoft.Testing.Platform.Services; + +namespace Microsoft.Testing.Extensions.AzureDevOpsReport; + +internal sealed class AzureDevOpsArtifactUploader : IDataConsumer, ITestSessionLifetimeHandler, IOutputDeviceDataProducer +{ + private const string AzureDevOpsArtifactUploadCommandFormat = "##vso[artifact.upload containerfolder={0};artifactname={0}]{1}"; + private const string AzureDevOpsBuildAddTagCommandPrefix = "##vso[build.addbuildtag]"; + private const string AzureDevOpsTfBuildVariableName = "TF_BUILD"; + private const string CrashDumpProducerUid = "CrashDumpProcessLifetimeHandler"; + private const string CrashDumpTag = "has-crashdump"; + private const string HangDumpProducerUid = "HangDumpProcessLifetimeHandler"; + private const string HangDumpTag = "has-hangdump"; + private const string TestFailuresTag = "has-test-failures"; + private static readonly string[] DefaultIncludePatterns = ["**/*"]; + + private readonly IConfiguration _configuration; + private readonly IEnvironment _environment; + private readonly IFileSystem _fileSystem; + private readonly IOutputDevice _outputDevice; + private readonly ITestApplicationModuleInfo _testApplicationModuleInfo; + private readonly ILogger _logger; + private readonly AzureDevOpsArtifactUploadMode _uploadMode; + private readonly string[] _includePatterns; + private readonly string[] _excludePatterns; + private readonly string? _artifactNameOverride; + private readonly Lazy _targetFrameworkMoniker; + + private bool _emitAzureDevOpsCommands; + private int _hasCrashDump; + private int _hasHangDump; + private int _hasTestFailures; + private string? _testResultsDirectory; + + public AzureDevOpsArtifactUploader( + ICommandLineOptions commandLineOptions, + IConfiguration configuration, + IEnvironment environment, + IFileSystem fileSystem, + IOutputDevice outputDevice, + ITestApplicationModuleInfo testApplicationModuleInfo, + ILoggerFactory loggerFactory) + { + _configuration = configuration; + _environment = environment; + _fileSystem = fileSystem; + _outputDevice = outputDevice; + _testApplicationModuleInfo = testApplicationModuleInfo; + _logger = loggerFactory.CreateLogger(); + _uploadMode = GetUploadMode(commandLineOptions); + _includePatterns = GetPatterns(commandLineOptions, AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactInclude, DefaultIncludePatterns); + _excludePatterns = GetPatterns(commandLineOptions, AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactExclude, []); + _artifactNameOverride = commandLineOptions.TryGetOptionArgumentList(AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactName, out string[]? artifactNameArguments) + && artifactNameArguments is [string artifactName] + ? artifactName + : null; + _targetFrameworkMoniker = new(TargetFrameworkMonikerHelper.GetTargetFrameworkMoniker); + } + + public Type[] DataTypesConsumed { get; } = [typeof(TestNodeUpdateMessage), typeof(FileArtifact)]; + + public string Uid => nameof(AzureDevOpsArtifactUploader); + + public string Version => ExtensionVersion.DefaultSemVer; + + public string DisplayName => AzureDevOpsResources.DisplayName; + + public string Description => AzureDevOpsResources.Description; + + public Task IsEnabledAsync() => Task.FromResult(_uploadMode is not AzureDevOpsArtifactUploadMode.Off); + + public async Task OnTestSessionStartingAsync(ITestSessionContext testSessionContext) + { + try + { + testSessionContext.CancellationToken.ThrowIfCancellationRequested(); + + string? configuredTestResultsDirectory = _configuration.GetTestResultDirectory(); + _testResultsDirectory = RoslynString.IsNullOrWhiteSpace(configuredTestResultsDirectory) + ? null + : Path.GetFullPath(configuredTestResultsDirectory); + _emitAzureDevOpsCommands = false; + Volatile.Write(ref _hasCrashDump, 0); + Volatile.Write(ref _hasHangDump, 0); + Volatile.Write(ref _hasTestFailures, 0); + + if (_uploadMode is AzureDevOpsArtifactUploadMode.Off) + { + return; + } + + _emitAzureDevOpsCommands = string.Equals(_environment.GetEnvironmentVariable(AzureDevOpsTfBuildVariableName), "true", StringComparison.OrdinalIgnoreCase); + if (_emitAzureDevOpsCommands) + { + return; + } + + if (_logger.IsEnabled(LogLevel.Warning)) + { + _logger.LogWarning(AzureDevOpsResources.ArtifactUploadRequiresTfBuildWarning); + } + + await _outputDevice.DisplayAsync(this, new WarningMessageOutputDeviceData(AzureDevOpsResources.ArtifactUploadRequiresTfBuildWarning), testSessionContext.CancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + LogUnexpectedException(nameof(OnTestSessionStartingAsync), ex); + } + } + + public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + + switch (value) + { + case TestNodeUpdateMessage nodeUpdateMessage when IsFailureState(nodeUpdateMessage.TestNode.Properties.SingleOrDefault()): + Interlocked.Exchange(ref _hasTestFailures, 1); + break; + + case FileArtifact: + TrackDump(dataProducer.Uid); + break; + } + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + LogUnexpectedException(nameof(ConsumeAsync), ex); + } + + return Task.CompletedTask; + } + + public async Task OnTestSessionFinishingAsync(ITestSessionContext testSessionContext) + { + try + { + testSessionContext.CancellationToken.ThrowIfCancellationRequested(); + + if (!_emitAzureDevOpsCommands) + { + return; + } + + if (_uploadMode is AzureDevOpsArtifactUploadMode.TagsOnly or AzureDevOpsArtifactUploadMode.All) + { + if (Volatile.Read(ref _hasCrashDump) == 1) + { + await EmitBuildTagAsync(CrashDumpTag, testSessionContext.CancellationToken).ConfigureAwait(false); + } + + if (Volatile.Read(ref _hasHangDump) == 1) + { + await EmitBuildTagAsync(HangDumpTag, testSessionContext.CancellationToken).ConfigureAwait(false); + } + + if (Volatile.Read(ref _hasTestFailures) == 1) + { + await EmitBuildTagAsync(TestFailuresTag, testSessionContext.CancellationToken).ConfigureAwait(false); + } + } + + if (_uploadMode is AzureDevOpsArtifactUploadMode.Files or AzureDevOpsArtifactUploadMode.All) + { + await EmitArtifactUploadCommandsAsync(testSessionContext.CancellationToken).ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + LogUnexpectedException(nameof(OnTestSessionFinishingAsync), ex); + } + } + + private async Task EmitArtifactUploadCommandsAsync(CancellationToken cancellationToken) + { + try + { + if (_testResultsDirectory is null || !_fileSystem.ExistDirectory(_testResultsDirectory)) + { + return; + } + + string[] files = _fileSystem.GetFiles(_testResultsDirectory, "*", SearchOption.AllDirectories); + if (files.Length == 0) + { + return; + } + + Matcher? matcher = ShouldUploadAllFiles() ? null : BuildMatcher(); + string artifactName = AzDoEscaper.Escape(GetArtifactName()); + string testResultsDirectoryWithSeparator = EnsureTrailingDirectorySeparator(_testResultsDirectory); + + foreach (string filePath in files.OrderBy(path => path, PathComparison.Comparer)) + { + string? relativePath = TryGetRelativePath(filePath, testResultsDirectoryWithSeparator); + if (relativePath is null) + { + continue; + } + + if (matcher is not null && !MatcherExtensions.Match(matcher, NormalizePath(relativePath)).HasMatches) + { + continue; + } + + await EmitArtifactUploadCommandAsync(artifactName, filePath, cancellationToken).ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + LogUnexpectedException(nameof(EmitArtifactUploadCommandsAsync), ex); + } + } + + private async Task EmitArtifactUploadCommandAsync(string artifactName, string filePath, CancellationToken cancellationToken) + => await EmitLineAsync(string.Format(CultureInfo.InvariantCulture, AzureDevOpsArtifactUploadCommandFormat, artifactName, AzDoEscaper.Escape(filePath)), cancellationToken).ConfigureAwait(false); + + private async Task EmitBuildTagAsync(string tag, CancellationToken cancellationToken) + => await EmitLineAsync($"{AzureDevOpsBuildAddTagCommandPrefix}{tag}", cancellationToken).ConfigureAwait(false); + + private async Task EmitLineAsync(string line, CancellationToken cancellationToken) + => await _outputDevice.DisplayAsync(this, new FormattedTextOutputDeviceData(line), cancellationToken).ConfigureAwait(false); + + private string GetArtifactName() + => _artifactNameOverride is { } artifactName && !RoslynString.IsNullOrWhiteSpace(artifactName) + ? artifactName + : $"TestResults_{_testApplicationModuleInfo.TryGetAssemblyName() ?? "unknown"}_{_targetFrameworkMoniker.Value}"; + + private Matcher BuildMatcher() + { + var matcher = new Matcher(PathComparison.Comparison); + foreach (string includePattern in _includePatterns) + { + matcher.AddInclude(includePattern); + } + + foreach (string excludePattern in _excludePatterns) + { + matcher.AddExclude(excludePattern); + } + + return matcher; + } + + private void LogUnexpectedException(string callbackName, Exception ex) + { + if (_logger.IsEnabled(LogLevel.Warning)) + { + _logger.LogWarning($"Unexpected exception in {callbackName}: {ex}"); + } + } + + private bool ShouldUploadAllFiles() + => _includePatterns.Length == 1 + && _includePatterns[0] == DefaultIncludePatterns[0] + && _excludePatterns.Length == 0; + + private void TrackDump(string dataProducerUid) + { + switch (dataProducerUid) + { + case CrashDumpProducerUid: + Volatile.Write(ref _hasCrashDump, 1); + break; + + case HangDumpProducerUid: + Volatile.Write(ref _hasHangDump, 1); + break; + } + } + + private static AzureDevOpsArtifactUploadMode GetUploadMode(ICommandLineOptions commandLineOptions) + => commandLineOptions.TryGetOptionArgumentList(AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifacts, out string[]? arguments) + && arguments is [string argument] + ? ParseUploadMode(argument) + : AzureDevOpsArtifactUploadMode.Off; + + private static string[] GetPatterns(ICommandLineOptions commandLineOptions, string optionName, string[] defaultPatterns) + => commandLineOptions.TryGetOptionArgumentList(optionName, out string[]? patterns) + && patterns is { Length: > 0 } + ? [.. patterns.Select(NormalizePath)] + : defaultPatterns; + + private static AzureDevOpsArtifactUploadMode ParseUploadMode(string mode) + => mode.ToLowerInvariant() switch + { + AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactsModeTagsOnly => AzureDevOpsArtifactUploadMode.TagsOnly, + AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactsModeFiles => AzureDevOpsArtifactUploadMode.Files, + AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactsModeAll => AzureDevOpsArtifactUploadMode.All, + _ => AzureDevOpsArtifactUploadMode.Off, + }; + + private static bool IsFailureState(TestNodeStateProperty? state) + { + if (state is FailedTestNodeStateProperty or ErrorTestNodeStateProperty or TimeoutTestNodeStateProperty) + { + return true; + } + +#pragma warning disable CS0618, MTP0001 // Type or member is obsolete + return state is CancelledTestNodeStateProperty; +#pragma warning restore CS0618, MTP0001 // Type or member is obsolete + } + + private static string EnsureTrailingDirectorySeparator(string path) + => path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; + + private static string NormalizePath(string path) + => path.Replace(Path.DirectorySeparatorChar, '/').Replace(Path.AltDirectorySeparatorChar, '/'); + + private static string? TryGetRelativePath(string filePath, string testResultsDirectoryWithSeparator) + => filePath.StartsWith(testResultsDirectoryWithSeparator, PathComparison.Comparison) + ? filePath.Substring(testResultsDirectoryWithSeparator.Length) + : null; +} + +internal enum AzureDevOpsArtifactUploadMode +{ + Off, + TagsOnly, + Files, + All, +} diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsCommandLineOptions.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsCommandLineOptions.cs index 54a7b1f96d..dde76672e2 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsCommandLineOptions.cs +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsCommandLineOptions.cs @@ -10,4 +10,12 @@ internal static class AzureDevOpsCommandLineOptions public const string AzureDevOpsFlakyHistory = "report-azdo-flaky-history"; public const string AzureDevOpsQuarantineFile = "report-azdo-quarantine-file"; public const string AzureDevOpsReportSeverity = "report-azdo-severity"; + public const string AzureDevOpsUploadArtifactExclude = "report-azdo-upload-artifact-exclude"; + public const string AzureDevOpsUploadArtifactInclude = "report-azdo-upload-artifact-include"; + public const string AzureDevOpsUploadArtifactName = "report-azdo-upload-artifact-name"; + public const string AzureDevOpsUploadArtifacts = "report-azdo-upload-artifacts"; + public const string AzureDevOpsUploadArtifactsModeAll = "all"; + public const string AzureDevOpsUploadArtifactsModeFiles = "files"; + public const string AzureDevOpsUploadArtifactsModeOff = "off"; + public const string AzureDevOpsUploadArtifactsModeTagsOnly = "tags-only"; } diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsCommandLineProvider.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsCommandLineProvider.cs index e6bfbe680a..5f722f04ec 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsCommandLineProvider.cs +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsCommandLineProvider.cs @@ -1,8 +1,10 @@ // 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.Extensions.FileSystemGlobbing; using Microsoft.Testing.Extensions.AzureDevOpsReport; using Microsoft.Testing.Extensions.AzureDevOpsReport.Resources; +using Microsoft.Testing.Platform; using Microsoft.Testing.Platform.CommandLine; using Microsoft.Testing.Platform.Extensions; using Microsoft.Testing.Platform.Extensions.CommandLine; @@ -11,6 +13,14 @@ namespace Microsoft.Testing.Extensions.Reporting; internal sealed class AzureDevOpsCommandLineProvider : ICommandLineOptionsProvider { + private static readonly string[] ArtifactUploadModes = + [ + AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactsModeOff, + AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactsModeTagsOnly, + AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactsModeFiles, + AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactsModeAll, + ]; + private static readonly string[] SeverityOptions = ["error", "warning"]; private static readonly string DemoteKnownFlakyOptionDescriptionFormatted = string.Format( @@ -36,13 +46,22 @@ public IReadOnlyCollection GetCommandLineOptions() new CommandLineOption(AzureDevOpsCommandLineOptions.AzureDevOpsFlakyHistory, AzureDevOpsResources.FlakyHistoryOptionDescription, ArgumentArity.ExactlyOne, false), new CommandLineOption(AzureDevOpsCommandLineOptions.AzureDevOpsQuarantineFile, AzureDevOpsResources.QuarantineFileOptionDescription, ArgumentArity.ExactlyOne, false), new CommandLineOption(AzureDevOpsCommandLineOptions.AzureDevOpsReportSeverity, AzureDevOpsResources.SeverityOptionDescription, ArgumentArity.ExactlyOne, false), + new CommandLineOption(AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactExclude, AzureDevOpsResources.UploadArtifactExcludeOptionDescription, ArgumentArity.ZeroOrMore, false), + new CommandLineOption(AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactInclude, AzureDevOpsResources.UploadArtifactIncludeOptionDescription, ArgumentArity.ZeroOrMore, false), + new CommandLineOption(AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactName, AzureDevOpsResources.UploadArtifactNameOptionDescription, ArgumentArity.ExactlyOne, false), + new CommandLineOption(AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifacts, AzureDevOpsResources.UploadArtifactsOptionDescription, ArgumentArity.ExactlyOne, false), ]; public Task ValidateOptionArgumentsAsync(CommandLineOption commandOption, string[] arguments) => commandOption.Name switch { AzureDevOpsCommandLineOptions.AzureDevOpsFlakyHistory => ValidateFlakyHistoryArgumentsAsync(arguments), - AzureDevOpsCommandLineOptions.AzureDevOpsReportSeverity => ValidateSeverityArgumentsAsync(arguments), + AzureDevOpsCommandLineOptions.AzureDevOpsReportSeverity when !SeverityOptions.Contains(arguments[0], StringComparer.OrdinalIgnoreCase) + => ValidationResult.InvalidTask(string.Format(CultureInfo.InvariantCulture, AzureDevOpsResources.InvalidSeverity, arguments[0])), + AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifacts when !ArtifactUploadModes.Contains(arguments[0], StringComparer.OrdinalIgnoreCase) + => ValidationResult.InvalidTask(string.Format(CultureInfo.InvariantCulture, AzureDevOpsResources.InvalidArtifactUploadMode, arguments[0])), + AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactInclude or AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactExclude + => ValidateGlobPatternsAsync(arguments), _ => ValidationResult.ValidTask, }; @@ -74,19 +93,60 @@ public Task ValidateCommandLineOptionsAsync(ICommandLineOption errorMessage = AzureDevOpsResources.AzureDevOpsDemoteKnownFlakyRequiresFlakyHistory; } + if (errorMessage is null && HasArtifactUploadConfiguration(commandLineOptions) && IsArtifactUploadDisabled(commandLineOptions)) + { + errorMessage = AzureDevOpsResources.ArtifactUploadOptionsRequireUploadArtifacts; + } + return errorMessage is null ? ValidationResult.ValidTask : ValidationResult.InvalidTask(errorMessage); } + private static bool HasArtifactUploadConfiguration(ICommandLineOptions commandLineOptions) + => commandLineOptions.IsOptionSet(AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactInclude) + || commandLineOptions.IsOptionSet(AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactExclude) + || commandLineOptions.IsOptionSet(AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactName); + + private static bool IsAbsolutePattern(string pattern) + => pattern.Length > 0 + && (pattern[0] is '/' or '\\' + || (pattern.Length >= 2 && pattern[1] == ':')); + + private static bool IsArtifactUploadDisabled(ICommandLineOptions commandLineOptions) + => !commandLineOptions.TryGetOptionArgumentList(AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifacts, out string[]? arguments) + || (arguments is [string argument] + && AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactsModeOff.Equals(argument, StringComparison.OrdinalIgnoreCase)); + + private static Task ValidateGlobPatternsAsync(string[] arguments) + { + foreach (string argument in arguments) + { + if (RoslynString.IsNullOrWhiteSpace(argument) || IsAbsolutePattern(argument)) + { + return ValidationResult.InvalidTask(string.Format(CultureInfo.InvariantCulture, AzureDevOpsResources.InvalidArtifactUploadGlob, argument)); + } + + try + { + var matcher = new Matcher(PathComparison.Comparison); + matcher.AddInclude(NormalizePattern(argument)); + } + catch (ArgumentException) + { + return ValidationResult.InvalidTask(string.Format(CultureInfo.InvariantCulture, AzureDevOpsResources.InvalidArtifactUploadGlob, argument)); + } + } + + return ValidationResult.ValidTask; + } + + private static string NormalizePattern(string pattern) + => pattern.Replace(Path.DirectorySeparatorChar, '/').Replace(Path.AltDirectorySeparatorChar, '/'); + private static Task ValidateFlakyHistoryArgumentsAsync(string[] arguments) => int.TryParse(arguments[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out int days) && days is >= 1 and <= 90 ? ValidationResult.ValidTask : ValidationResult.InvalidTask(string.Format(CultureInfo.InvariantCulture, AzureDevOpsResources.InvalidFlakyHistoryDays, arguments[0])); - - private static Task ValidateSeverityArgumentsAsync(string[] arguments) - => SeverityOptions.Contains(arguments[0], StringComparer.OrdinalIgnoreCase) - ? ValidationResult.ValidTask - : ValidationResult.InvalidTask(string.Format(CultureInfo.InvariantCulture, AzureDevOpsResources.InvalidSeverity, arguments[0])); } diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsExtensions.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsExtensions.cs index 2b0266564d..148b0afcb3 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsExtensions.cs +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsExtensions.cs @@ -4,6 +4,7 @@ using Microsoft.Testing.Extensions.AzureDevOpsReport; using Microsoft.Testing.Extensions.Reporting; using Microsoft.Testing.Platform.Builder; +using Microsoft.Testing.Platform.Extensions; using Microsoft.Testing.Platform.Services; namespace Microsoft.Testing.Extensions; @@ -21,6 +22,17 @@ public static void AddAzureDevOpsProvider(this ITestApplicationBuilder builder) { AzureDevOpsHistoryService? historyService = null; + var compositeArtifactUploader = + new CompositeExtensionFactory(serviceProvider => + new AzureDevOpsArtifactUploader( + serviceProvider.GetCommandLineOptions(), + serviceProvider.GetConfiguration(), + serviceProvider.GetEnvironment(), + serviceProvider.GetFileSystem(), + serviceProvider.GetOutputDevice(), + serviceProvider.GetTestApplicationModuleInfo(), + serviceProvider.GetLoggerFactory())); + builder.TestHost.AddDataConsumer(serviceProvider => { historyService ??= CreateHistoryService(serviceProvider); @@ -33,8 +45,10 @@ public static void AddAzureDevOpsProvider(this ITestApplicationBuilder builder) serviceProvider.GetLoggerFactory(), historyService); }); + builder.TestHost.AddDataConsumer(compositeArtifactUploader); builder.TestHost.AddTestSessionLifetimeHandler(serviceProvider => historyService ??= CreateHistoryService(serviceProvider)); + builder.TestHost.AddTestSessionLifetimeHandler(compositeArtifactUploader); builder.CommandLine.AddProvider(() => new AzureDevOpsCommandLineProvider()); } diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsReporter.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsReporter.cs index 7bad431d44..ddd2e6d540 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsReporter.cs +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsReporter.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// 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.Extensions.AzureDevOpsReport.Resources; @@ -69,7 +69,7 @@ public AzureDevOpsReporter( _outputDisplay = outputDisplay; _historyService = historyService; _logger = loggerFactory.CreateLogger(); - _targetFrameworkMoniker = GetTargetFrameworkMoniker(); + _targetFrameworkMoniker = TargetFrameworkMonikerHelper.GetTargetFrameworkMoniker(); } public Type[] DataTypesConsumed { get; } = @@ -397,10 +397,6 @@ private void EnsureEnabledConfigurationLoaded() _hasLoadedEnabledConfiguration = true; } - private static string GetTargetFrameworkMoniker() - => TargetFrameworkParser.GetShortTargetFramework(Assembly.GetEntryAssembly()?.GetCustomAttribute()?.FrameworkDisplayName) - ?? TargetFrameworkParser.GetShortTargetFramework(RuntimeInformation.FrameworkDescription); - private static string GetTestName(TestNode testNode) => testNode.Properties .OfType() diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Microsoft.Testing.Extensions.AzureDevOpsReport.csproj b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Microsoft.Testing.Extensions.AzureDevOpsReport.csproj index 584c40e84b..80cdb1073e 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Microsoft.Testing.Extensions.AzureDevOpsReport.csproj +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Microsoft.Testing.Extensions.AzureDevOpsReport.csproj @@ -48,9 +48,10 @@ This package extends Microsoft Testing Platform to provide a Azure DevOps report + - + diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/PathComparison.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/PathComparison.cs new file mode 100644 index 0000000000..ebf97fd1d0 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/PathComparison.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Testing.Extensions.AzureDevOpsReport; + +internal static class PathComparison +{ + public static StringComparison Comparison { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? StringComparison.OrdinalIgnoreCase + : StringComparison.Ordinal; + + public static StringComparer Comparer { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? StringComparer.OrdinalIgnoreCase + : StringComparer.Ordinal; +} diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/AzureDevOpsResources.resx b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/AzureDevOpsResources.resx index e3b964e574..b141d4254b 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/AzureDevOpsResources.resx +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/AzureDevOpsResources.resx @@ -136,6 +136,35 @@ Severity to use for the reported event. Options are: error (default) and warning. {Locked="error"}{Locked="warning"} + + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + + + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + {Locked="**/*"} + + + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + {Locked="TestResults_{assemblyName}_{tfm}"} + + + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + {Locked="off"}{Locked="tags-only"}{Locked="files"}{Locked="all"} + + + Invalid glob pattern {0}. + + + Invalid artifact upload mode {0}. + + + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + {Locked="--report-azdo-upload-artifact-include"}{Locked="--report-azdo-upload-artifact-exclude"}{Locked="--report-azdo-upload-artifact-name"}{Locked="--report-azdo-upload-artifacts"}{Locked="off"} + + + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + {Locked="TF_BUILD"}{Locked="true"} + '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled @@ -203,4 +232,4 @@ Azure DevOps quarantine file contains more than {0} patterns. Remaining patterns will be ignored. - \ No newline at end of file + diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.cs.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.cs.xlf index 2a2d9ac36f..1c03349ecc 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.cs.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.cs.xlf @@ -2,6 +2,16 @@ + + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + {Locked="--report-azdo-upload-artifact-include"}{Locked="--report-azdo-upload-artifact-exclude"}{Locked="--report-azdo-upload-artifact-name"}{Locked="--report-azdo-upload-artifacts"}{Locked="off"} + + + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + {Locked="TF_BUILD"}{Locked="true"} + '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled @@ -82,6 +92,16 @@ Azure DevOps flaky history returned {0} runs. Only the first {1} runs will be inspected. + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. @@ -132,6 +152,26 @@ Závažnost, která se má použít pro hlášenou událost. Možnosti: error (výchozí) a warning. {Locked="error"}{Locked="warning"} + + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + + + + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + {Locked="**/*"} + + + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + {Locked="TestResults_{assemblyName}_{tfm}"} + + + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + {Locked="off"}{Locked="tags-only"}{Locked="files"}{Locked="all"} + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.de.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.de.xlf index 4e019a7afd..a9da8e98ed 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.de.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.de.xlf @@ -2,6 +2,16 @@ + + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + {Locked="--report-azdo-upload-artifact-include"}{Locked="--report-azdo-upload-artifact-exclude"}{Locked="--report-azdo-upload-artifact-name"}{Locked="--report-azdo-upload-artifacts"}{Locked="off"} + + + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + {Locked="TF_BUILD"}{Locked="true"} + '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled @@ -82,6 +92,16 @@ Azure DevOps flaky history returned {0} runs. Only the first {1} runs will be inspected. + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. @@ -132,6 +152,26 @@ Schweregrad, der für das gemeldete Ereignis verwendet werden soll. Optionen sind: error (Standard) und warning. {Locked="error"}{Locked="warning"} + + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + + + + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + {Locked="**/*"} + + + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + {Locked="TestResults_{assemblyName}_{tfm}"} + + + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + {Locked="off"}{Locked="tags-only"}{Locked="files"}{Locked="all"} + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.es.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.es.xlf index 40c2a008d7..05ed7ecefa 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.es.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.es.xlf @@ -2,6 +2,16 @@ + + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + {Locked="--report-azdo-upload-artifact-include"}{Locked="--report-azdo-upload-artifact-exclude"}{Locked="--report-azdo-upload-artifact-name"}{Locked="--report-azdo-upload-artifacts"}{Locked="off"} + + + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + {Locked="TF_BUILD"}{Locked="true"} + '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled @@ -82,6 +92,16 @@ Azure DevOps flaky history returned {0} runs. Only the first {1} runs will be inspected. + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. @@ -132,6 +152,26 @@ Gravedad que se va a usar para el evento notificado. Las opciones son: error (valor predeterminado) y warning. {Locked="error"}{Locked="warning"} + + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + + + + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + {Locked="**/*"} + + + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + {Locked="TestResults_{assemblyName}_{tfm}"} + + + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + {Locked="off"}{Locked="tags-only"}{Locked="files"}{Locked="all"} + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.fr.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.fr.xlf index 4f82aa0755..2f1b47604c 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.fr.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.fr.xlf @@ -2,6 +2,16 @@ + + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + {Locked="--report-azdo-upload-artifact-include"}{Locked="--report-azdo-upload-artifact-exclude"}{Locked="--report-azdo-upload-artifact-name"}{Locked="--report-azdo-upload-artifacts"}{Locked="off"} + + + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + {Locked="TF_BUILD"}{Locked="true"} + '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled @@ -82,6 +92,16 @@ Azure DevOps flaky history returned {0} runs. Only the first {1} runs will be inspected. + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. @@ -132,6 +152,26 @@ Gravité à utiliser pour l’événement signalé. Les options sont : error (par défaut) et warning. {Locked="error"}{Locked="warning"} + + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + + + + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + {Locked="**/*"} + + + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + {Locked="TestResults_{assemblyName}_{tfm}"} + + + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + {Locked="off"}{Locked="tags-only"}{Locked="files"}{Locked="all"} + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.it.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.it.xlf index 79f20267c5..9692ea0a04 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.it.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.it.xlf @@ -2,6 +2,16 @@ + + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + {Locked="--report-azdo-upload-artifact-include"}{Locked="--report-azdo-upload-artifact-exclude"}{Locked="--report-azdo-upload-artifact-name"}{Locked="--report-azdo-upload-artifacts"}{Locked="off"} + + + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + {Locked="TF_BUILD"}{Locked="true"} + '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled @@ -82,6 +92,16 @@ Azure DevOps flaky history returned {0} runs. Only the first {1} runs will be inspected. + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. @@ -132,6 +152,26 @@ Gravità da usare per l'evento segnalato. Le opzioni sono: error (impostazione predefinita) e warning. {Locked="error"}{Locked="warning"} + + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + + + + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + {Locked="**/*"} + + + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + {Locked="TestResults_{assemblyName}_{tfm}"} + + + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + {Locked="off"}{Locked="tags-only"}{Locked="files"}{Locked="all"} + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ja.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ja.xlf index 2aefa6edb2..6dffb513ad 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ja.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ja.xlf @@ -2,6 +2,16 @@ + + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + {Locked="--report-azdo-upload-artifact-include"}{Locked="--report-azdo-upload-artifact-exclude"}{Locked="--report-azdo-upload-artifact-name"}{Locked="--report-azdo-upload-artifacts"}{Locked="off"} + + + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + {Locked="TF_BUILD"}{Locked="true"} + '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled @@ -82,6 +92,16 @@ Azure DevOps flaky history returned {0} runs. Only the first {1} runs will be inspected. + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. @@ -132,6 +152,26 @@ 報告されたイベントに使用する重大度。オプションは、error (既定) と warning です。 {Locked="error"}{Locked="warning"} + + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + + + + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + {Locked="**/*"} + + + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + {Locked="TestResults_{assemblyName}_{tfm}"} + + + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + {Locked="off"}{Locked="tags-only"}{Locked="files"}{Locked="all"} + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ko.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ko.xlf index 37ef6d3a84..c7c450887a 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ko.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ko.xlf @@ -2,6 +2,16 @@ + + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + {Locked="--report-azdo-upload-artifact-include"}{Locked="--report-azdo-upload-artifact-exclude"}{Locked="--report-azdo-upload-artifact-name"}{Locked="--report-azdo-upload-artifacts"}{Locked="off"} + + + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + {Locked="TF_BUILD"}{Locked="true"} + '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled @@ -82,6 +92,16 @@ Azure DevOps flaky history returned {0} runs. Only the first {1} runs will be inspected. + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. @@ -132,6 +152,26 @@ 보고된 이벤트에 사용할 심각도입니다. 옵션은 error(기본값)와 warning, 두 가지입니다. {Locked="error"}{Locked="warning"} + + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + + + + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + {Locked="**/*"} + + + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + {Locked="TestResults_{assemblyName}_{tfm}"} + + + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + {Locked="off"}{Locked="tags-only"}{Locked="files"}{Locked="all"} + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pl.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pl.xlf index 2492a4dd60..4bc5ec929d 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pl.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pl.xlf @@ -2,6 +2,16 @@ + + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + {Locked="--report-azdo-upload-artifact-include"}{Locked="--report-azdo-upload-artifact-exclude"}{Locked="--report-azdo-upload-artifact-name"}{Locked="--report-azdo-upload-artifacts"}{Locked="off"} + + + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + {Locked="TF_BUILD"}{Locked="true"} + '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled @@ -82,6 +92,16 @@ Azure DevOps flaky history returned {0} runs. Only the first {1} runs will be inspected. + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. @@ -132,6 +152,26 @@ Ważność do użycia dla zgłoszonego zdarzenia. Dostępne opcje to: error (domyślny) i warning. {Locked="error"}{Locked="warning"} + + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + + + + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + {Locked="**/*"} + + + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + {Locked="TestResults_{assemblyName}_{tfm}"} + + + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + {Locked="off"}{Locked="tags-only"}{Locked="files"}{Locked="all"} + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pt-BR.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pt-BR.xlf index da434eceec..ea099b0771 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pt-BR.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.pt-BR.xlf @@ -2,6 +2,16 @@ + + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + {Locked="--report-azdo-upload-artifact-include"}{Locked="--report-azdo-upload-artifact-exclude"}{Locked="--report-azdo-upload-artifact-name"}{Locked="--report-azdo-upload-artifacts"}{Locked="off"} + + + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + {Locked="TF_BUILD"}{Locked="true"} + '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled @@ -82,6 +92,16 @@ Azure DevOps flaky history returned {0} runs. Only the first {1} runs will be inspected. + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. @@ -132,6 +152,26 @@ A gravidade que será usada para o evento relatado. As opções são: error (padrão) e warning. {Locked="error"}{Locked="warning"} + + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + + + + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + {Locked="**/*"} + + + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + {Locked="TestResults_{assemblyName}_{tfm}"} + + + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + {Locked="off"}{Locked="tags-only"}{Locked="files"}{Locked="all"} + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ru.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ru.xlf index 9ea04a2a73..446cd14c03 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ru.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.ru.xlf @@ -2,6 +2,16 @@ + + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + {Locked="--report-azdo-upload-artifact-include"}{Locked="--report-azdo-upload-artifact-exclude"}{Locked="--report-azdo-upload-artifact-name"}{Locked="--report-azdo-upload-artifacts"}{Locked="off"} + + + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + {Locked="TF_BUILD"}{Locked="true"} + '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled @@ -82,6 +92,16 @@ Azure DevOps flaky history returned {0} runs. Only the first {1} runs will be inspected. + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. @@ -132,6 +152,26 @@ Уровень серьезности, используемый для события из отчета. Параметры: error (по умолчанию) и warning. {Locked="error"}{Locked="warning"} + + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + + + + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + {Locked="**/*"} + + + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + {Locked="TestResults_{assemblyName}_{tfm}"} + + + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + {Locked="off"}{Locked="tags-only"}{Locked="files"}{Locked="all"} + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.tr.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.tr.xlf index 2857d58967..112c619ea7 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.tr.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.tr.xlf @@ -2,6 +2,16 @@ + + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + {Locked="--report-azdo-upload-artifact-include"}{Locked="--report-azdo-upload-artifact-exclude"}{Locked="--report-azdo-upload-artifact-name"}{Locked="--report-azdo-upload-artifacts"}{Locked="off"} + + + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + {Locked="TF_BUILD"}{Locked="true"} + '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled @@ -82,6 +92,16 @@ Azure DevOps flaky history returned {0} runs. Only the first {1} runs will be inspected. + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. @@ -132,6 +152,26 @@ Raporlanan olay için kullanılacak önem derecesi. Seçenekler şunlardır: error (varsayılan) ve warning. {Locked="error"}{Locked="warning"} + + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + + + + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + {Locked="**/*"} + + + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + {Locked="TestResults_{assemblyName}_{tfm}"} + + + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + {Locked="off"}{Locked="tags-only"}{Locked="files"}{Locked="all"} + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hans.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hans.xlf index d5bf9341d5..0664b58d63 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hans.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hans.xlf @@ -2,6 +2,16 @@ + + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + {Locked="--report-azdo-upload-artifact-include"}{Locked="--report-azdo-upload-artifact-exclude"}{Locked="--report-azdo-upload-artifact-name"}{Locked="--report-azdo-upload-artifacts"}{Locked="off"} + + + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + {Locked="TF_BUILD"}{Locked="true"} + '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled @@ -82,6 +92,16 @@ Azure DevOps flaky history returned {0} runs. Only the first {1} runs will be inspected. + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. @@ -132,6 +152,26 @@ 要用于所报告事件的严重性。选项为: error (默认)和 warning。 {Locked="error"}{Locked="warning"} + + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + + + + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + {Locked="**/*"} + + + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + {Locked="TestResults_{assemblyName}_{tfm}"} + + + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + {Locked="off"}{Locked="tags-only"}{Locked="files"}{Locked="all"} + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hant.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hant.xlf index 657303d699..e005701285 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hant.xlf +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/xlf/AzureDevOpsResources.zh-Hant.xlf @@ -2,6 +2,16 @@ + + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + '--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'. + {Locked="--report-azdo-upload-artifact-include"}{Locked="--report-azdo-upload-artifact-exclude"}{Locked="--report-azdo-upload-artifact-name"}{Locked="--report-azdo-upload-artifacts"}{Locked="off"} + + + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags. + {Locked="TF_BUILD"}{Locked="true"} + '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled '--report-azdo-demote-known-flaky' requires '--report-azdo' to be enabled @@ -82,6 +92,16 @@ Azure DevOps flaky history returned {0} runs. Only the first {1} runs will be inspected. + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. Invalid value '{0}' for '--report-azdo-flaky-history'. Provide an integer between 1 and 90. @@ -132,6 +152,26 @@ 要用於所報告事件的嚴重性。選項為: error (預設) warning。 {Locked="error"}{Locked="warning"} + + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + + + + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + {Locked="**/*"} + + + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + {Locked="TestResults_{assemblyName}_{tfm}"} + + + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. + {Locked="off"}{Locked="tags-only"}{Locked="files"}{Locked="all"} + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/TargetFrameworkMonikerHelper.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/TargetFrameworkMonikerHelper.cs new file mode 100644 index 0000000000..13a8ab500e --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/TargetFrameworkMonikerHelper.cs @@ -0,0 +1,14 @@ +// 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.OutputDevice; + +namespace Microsoft.Testing.Extensions.AzureDevOpsReport; + +internal static class TargetFrameworkMonikerHelper +{ + public static string GetTargetFrameworkMoniker() + => TargetFrameworkParser.GetShortTargetFramework(Assembly.GetEntryAssembly()?.GetCustomAttribute()?.FrameworkDisplayName) + ?? TargetFrameworkParser.GetShortTargetFramework(System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription) + ?? "unknown"; +} diff --git a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoAllExtensionsTests.cs b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoAllExtensionsTests.cs index 7217ba2fc8..def921cb1c 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoAllExtensionsTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoAllExtensionsTests.cs @@ -113,6 +113,14 @@ Demote failures with an Azure DevOps flaky history of at least 25% in the select Path to a text file that lists quarantined test fully qualified names or glob patterns. Matching failures are reported as warnings. --report-azdo-severity Severity to use for the reported event. Options are: error (default) and warning. + --report-azdo-upload-artifact-exclude + Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + --report-azdo-upload-artifact-include + Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + --report-azdo-upload-artifact-name + Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + --report-azdo-upload-artifacts + Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. --report-trx Enable generating TRX report --report-trx-filename @@ -304,6 +312,22 @@ Takes one argument as string in the format [h|m|s] where 'value' is float Arity: 1 Hidden: False Description: Severity to use for the reported event. Options are: error (default) and warning. + --report-azdo-upload-artifact-exclude + Arity: 0..N + Hidden: False + Description: Exclude files from Azure DevOps artifact upload using glob patterns relative to the test results directory. + --report-azdo-upload-artifact-include + Arity: 0..N + Hidden: False + Description: Include files in Azure DevOps artifact upload using glob patterns relative to the test results directory. Defaults to '**/*'. + --report-azdo-upload-artifact-name + Arity: 1 + Hidden: False + Description: Override the Azure DevOps artifact container name. Defaults to 'TestResults_{assemblyName}_{tfm}'. + --report-azdo-upload-artifacts + Arity: 1 + Hidden: False + Description: Upload test result files and/or add build tags to Azure DevOps. Options are: off (default), tags-only, files, and all. CrashDumpCommandLineProvider Name: Crash dump Version: * diff --git a/test/UnitTests/Microsoft.Testing.Extensions.UnitTests/AzureDevOpsArtifactUploaderTests.cs b/test/UnitTests/Microsoft.Testing.Extensions.UnitTests/AzureDevOpsArtifactUploaderTests.cs new file mode 100644 index 0000000000..48b6f7ca47 --- /dev/null +++ b/test/UnitTests/Microsoft.Testing.Extensions.UnitTests/AzureDevOpsArtifactUploaderTests.cs @@ -0,0 +1,455 @@ +// 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.Extensions.AzureDevOpsReport; +using Microsoft.Testing.Extensions.Reporting; +using Microsoft.Testing.Extensions.UnitTests.Helpers; +using Microsoft.Testing.Platform.CommandLine; +using Microsoft.Testing.Platform.Configurations; +using Microsoft.Testing.Platform.Extensions.Messages; +using Microsoft.Testing.Platform.Extensions.OutputDevice; +using Microsoft.Testing.Platform.Helpers; +using Microsoft.Testing.Platform.Logging; +using Microsoft.Testing.Platform.OutputDevice; +using Microsoft.Testing.Platform.Services; +using Microsoft.Testing.Platform.TestHost; + +using Moq; + +namespace Microsoft.Testing.Extensions.UnitTests; + +[TestClass] +public sealed class AzureDevOpsArtifactUploaderTests +{ + private const string ArtifactUploadOptionsRequireUploadArtifactsMessage = "'--report-azdo-upload-artifact-include', '--report-azdo-upload-artifact-exclude', and '--report-azdo-upload-artifact-name' require '--report-azdo-upload-artifacts' to be set to a value other than 'off'."; + private static readonly string ResultsDirectory = Path.Combine(Path.GetTempPath(), "azdo-artifact-uploader-tests"); + private static readonly string OutsideResultsDirectory = Path.Combine(Path.GetTempPath(), "azdo-artifact-uploader-tests-outside"); + + private readonly Mock _configurationMock = new(); + private readonly Mock _environmentMock = new(); + private readonly Mock _fileSystemMock = new(); + private readonly Mock _outputDeviceMock = new(); + private readonly Mock _testApplicationModuleInfoMock = new(); + private readonly Mock _loggerFactoryMock = new(); + private readonly List _outputData = []; + + public AzureDevOpsArtifactUploaderTests() + { + _ = _configurationMock.SetupGet(configuration => configuration[PlatformConfigurationConstants.PlatformResultDirectory]).Returns(ResultsDirectory); + _ = _testApplicationModuleInfoMock.Setup(testApplicationModuleInfo => testApplicationModuleInfo.TryGetAssemblyName()).Returns("MyAssembly"); + _ = _loggerFactoryMock.Setup(loggerFactory => loggerFactory.CreateLogger(It.IsAny())).Returns(Mock.Of()); + _ = _outputDeviceMock + .Setup(outputDevice => outputDevice.DisplayAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((_, data, _) => _outputData.Add(data)) + .Returns(Task.CompletedTask); + } + + [TestMethod] + public async Task OffMode_IsDisabledAndNoOps() + { + AzureDevOpsArtifactUploader uploader = CreateUploader([]); + + _ = _environmentMock.Setup(environment => environment.GetEnvironmentVariable("TF_BUILD")).Returns("true"); + _ = _fileSystemMock.Setup(fileSystem => fileSystem.ExistDirectory(ResultsDirectory)).Returns(true); + _ = _fileSystemMock.Setup(fileSystem => fileSystem.GetFiles(ResultsDirectory, "*", SearchOption.AllDirectories)) + .Returns([InResults("test.trx")]); + + Assert.IsFalse(await uploader.IsEnabledAsync().ConfigureAwait(false)); + + await uploader.OnTestSessionStartingAsync(new TestSessionContext()).ConfigureAwait(false); + await uploader.ConsumeAsync(CreateProducer("CrashDumpProcessLifetimeHandler", "Crash dump"), CreateFileArtifact(InResults("dump.dmp")), CancellationToken.None).ConfigureAwait(false); + await uploader.ConsumeAsync(CreateProducer(), CreateFailedTestNodeUpdateMessage(), CancellationToken.None).ConfigureAwait(false); + await uploader.OnTestSessionFinishingAsync(new TestSessionContext()).ConfigureAwait(false); + + Assert.IsEmpty(GetFormattedLines()); + Assert.IsEmpty(GetWarnings()); + } + + [TestMethod] + public async Task TagsOnlyMode_EmitsOnlyBuildTags() + { + AzureDevOpsArtifactUploader uploader = CreateUploader(new Dictionary + { + [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifacts] = [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactsModeTagsOnly], + }); + + _ = _environmentMock.Setup(environment => environment.GetEnvironmentVariable("TF_BUILD")).Returns("true"); + + await uploader.OnTestSessionStartingAsync(new TestSessionContext()).ConfigureAwait(false); + await uploader.ConsumeAsync(CreateProducer("CrashDumpProcessLifetimeHandler", "Crash dump"), CreateFileArtifact(InResults("crashdump.dmp")), CancellationToken.None).ConfigureAwait(false); + await uploader.ConsumeAsync(CreateProducer("HangDumpProcessLifetimeHandler", "Hang dump"), CreateFileArtifact(InResults("hang", "hangdump.dmp")), CancellationToken.None).ConfigureAwait(false); + await uploader.ConsumeAsync(CreateProducer(), CreateFailedTestNodeUpdateMessage(), CancellationToken.None).ConfigureAwait(false); + await uploader.OnTestSessionFinishingAsync(new TestSessionContext()).ConfigureAwait(false); + + CollectionAssert.AreEquivalent( + new[] + { + "##vso[build.addbuildtag]has-crashdump", + "##vso[build.addbuildtag]has-hangdump", + "##vso[build.addbuildtag]has-test-failures", + }, + GetFormattedLines()); + Assert.DoesNotContain(line => line.Contains("artifact.upload", StringComparison.Ordinal), GetFormattedLines()); + Assert.IsEmpty(GetWarnings()); + } + + [TestMethod] + public async Task FilesMode_EmitsOnlyArtifactLines() + { + AzureDevOpsArtifactUploader uploader = CreateUploader(new Dictionary + { + [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactName] = ["Artifacts"], + [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifacts] = [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactsModeFiles], + }); + + _ = _environmentMock.Setup(environment => environment.GetEnvironmentVariable("TF_BUILD")).Returns("true"); + _ = _fileSystemMock.Setup(fileSystem => fileSystem.ExistDirectory(ResultsDirectory)).Returns(true); + _ = _fileSystemMock.Setup(fileSystem => fileSystem.GetFiles(ResultsDirectory, "*", SearchOption.AllDirectories)) + .Returns([InResults("a.trx"), InResults("b.dmp")]); + + await uploader.OnTestSessionStartingAsync(new TestSessionContext()).ConfigureAwait(false); + await uploader.ConsumeAsync(CreateProducer(), CreateFailedTestNodeUpdateMessage(), CancellationToken.None).ConfigureAwait(false); + await uploader.OnTestSessionFinishingAsync(new TestSessionContext()).ConfigureAwait(false); + + string[] lines = GetFormattedLines(); + Assert.HasCount(2, lines); + Assert.IsTrue(lines.All(line => line.StartsWith("##vso[artifact.upload containerfolder=Artifacts;artifactname=Artifacts]", StringComparison.Ordinal))); + Assert.DoesNotContain(line => line.Contains("build.addbuildtag", StringComparison.Ordinal), lines); + } + + [TestMethod] + public async Task FilesMode_SkipsArtifactsOutsideResultsDirectory() + { + AzureDevOpsArtifactUploader uploader = CreateUploader(new Dictionary + { + [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactName] = ["Artifacts"], + [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifacts] = [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactsModeFiles], + }); + + _ = _environmentMock.Setup(environment => environment.GetEnvironmentVariable("TF_BUILD")).Returns("true"); + _ = _fileSystemMock.Setup(fileSystem => fileSystem.ExistDirectory(ResultsDirectory)).Returns(true); + _ = _fileSystemMock.Setup(fileSystem => fileSystem.GetFiles(ResultsDirectory, "*", SearchOption.AllDirectories)) + .Returns([InResults("inside.trx"), OutsideResults("outside.trx")]); + + await uploader.OnTestSessionStartingAsync(new TestSessionContext()).ConfigureAwait(false); + await uploader.OnTestSessionFinishingAsync(new TestSessionContext()).ConfigureAwait(false); + + CollectionAssert.AreEqual( + new[] { $"##vso[artifact.upload containerfolder=Artifacts;artifactname=Artifacts]{InResults("inside.trx")}" }, + GetFormattedLines()); + } + + [TestMethod] + public async Task AllMode_EmitsBuildTagsAndArtifactLines() + { + AzureDevOpsArtifactUploader uploader = CreateUploader(new Dictionary + { + [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactName] = ["Artifacts"], + [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifacts] = [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactsModeAll], + }); + + _ = _environmentMock.Setup(environment => environment.GetEnvironmentVariable("TF_BUILD")).Returns("true"); + _ = _fileSystemMock.Setup(fileSystem => fileSystem.ExistDirectory(ResultsDirectory)).Returns(true); + _ = _fileSystemMock.Setup(fileSystem => fileSystem.GetFiles(ResultsDirectory, "*", SearchOption.AllDirectories)) + .Returns([InResults("test.trx")]); + + await uploader.OnTestSessionStartingAsync(new TestSessionContext()).ConfigureAwait(false); + await uploader.ConsumeAsync(CreateProducer("CrashDumpProcessLifetimeHandler", "Crash dump"), CreateFileArtifact(InResults("dump.dmp")), CancellationToken.None).ConfigureAwait(false); + await uploader.ConsumeAsync(CreateProducer(), CreateFailedTestNodeUpdateMessage(), CancellationToken.None).ConfigureAwait(false); + await uploader.OnTestSessionFinishingAsync(new TestSessionContext()).ConfigureAwait(false); + + CollectionAssert.AreEqual( + new[] + { + "##vso[build.addbuildtag]has-crashdump", + "##vso[build.addbuildtag]has-test-failures", + $"##vso[artifact.upload containerfolder=Artifacts;artifactname=Artifacts]{InResults("test.trx")}", + }, + GetFormattedLines()); + } + + [TestMethod] + public async Task IncludeAndExcludeGlobs_AreAppliedToArtifactUploads() + { + AzureDevOpsArtifactUploader uploader = CreateUploader(new Dictionary + { + [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactExclude] = ["skip/**"], + [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactInclude] = ["**/*.trx"], + [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactName] = ["Artifacts"], + [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifacts] = [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactsModeFiles], + }); + + _ = _environmentMock.Setup(environment => environment.GetEnvironmentVariable("TF_BUILD")).Returns("true"); + _ = _fileSystemMock.Setup(fileSystem => fileSystem.ExistDirectory(ResultsDirectory)).Returns(true); + _ = _fileSystemMock.Setup(fileSystem => fileSystem.GetFiles(ResultsDirectory, "*", SearchOption.AllDirectories)) + .Returns([InResults("keep.trx"), InResults("keep.coverage"), InResults("skip", "ignored.trx")]); + + await uploader.OnTestSessionStartingAsync(new TestSessionContext()).ConfigureAwait(false); + await uploader.OnTestSessionFinishingAsync(new TestSessionContext()).ConfigureAwait(false); + + CollectionAssert.AreEqual( + new[] { $"##vso[artifact.upload containerfolder=Artifacts;artifactname=Artifacts]{InResults("keep.trx")}" }, + GetFormattedLines()); + } + + [TestMethod] + public async Task ConsumedArtifacts_DetectCrashAndHangDumps() + { + AzureDevOpsArtifactUploader uploader = CreateUploader(new Dictionary + { + [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifacts] = [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactsModeTagsOnly], + }); + + _ = _environmentMock.Setup(environment => environment.GetEnvironmentVariable("TF_BUILD")).Returns("true"); + + await uploader.OnTestSessionStartingAsync(new TestSessionContext()).ConfigureAwait(false); + await uploader.ConsumeAsync(CreateProducer("CrashDumpProcessLifetimeHandler", "Crash dump"), CreateFileArtifact(InResults("dump.dmp")), CancellationToken.None).ConfigureAwait(false); + await uploader.ConsumeAsync(CreateProducer("HangDumpProcessLifetimeHandler", "Hang dump"), CreateFileArtifact(InResults("hang", "dump.log")), CancellationToken.None).ConfigureAwait(false); + await uploader.OnTestSessionFinishingAsync(new TestSessionContext()).ConfigureAwait(false); + + CollectionAssert.AreEquivalent( + new[] + { + "##vso[build.addbuildtag]has-crashdump", + "##vso[build.addbuildtag]has-hangdump", + }, + GetFormattedLines()); + } + + [TestMethod] + public async Task NonDumpProducer_DoesNotEmitDumpTags() + { + AzureDevOpsArtifactUploader uploader = CreateUploader(new Dictionary + { + [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifacts] = [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactsModeTagsOnly], + }); + + _ = _environmentMock.Setup(environment => environment.GetEnvironmentVariable("TF_BUILD")).Returns("true"); + + await uploader.OnTestSessionStartingAsync(new TestSessionContext()).ConfigureAwait(false); + await uploader.ConsumeAsync(CreateProducer("GenericProducer", "Generic"), CreateFileArtifact(InResults("hang", "dump.dmp")), CancellationToken.None).ConfigureAwait(false); + await uploader.OnTestSessionFinishingAsync(new TestSessionContext()).ConfigureAwait(false); + + Assert.IsEmpty(GetFormattedLines()); + } + + [TestMethod] + public async Task HasTestFailuresTag_IsNotEmittedWithoutFailures() + { + AzureDevOpsArtifactUploader uploader = CreateUploader(new Dictionary + { + [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifacts] = [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactsModeTagsOnly], + }); + + _ = _environmentMock.Setup(environment => environment.GetEnvironmentVariable("TF_BUILD")).Returns("true"); + + await uploader.OnTestSessionStartingAsync(new TestSessionContext()).ConfigureAwait(false); + await uploader.ConsumeAsync(CreateProducer(), CreatePassedTestNodeUpdateMessage(), CancellationToken.None).ConfigureAwait(false); + await uploader.OnTestSessionFinishingAsync(new TestSessionContext()).ConfigureAwait(false); + + Assert.DoesNotContain("##vso[build.addbuildtag]has-test-failures", GetFormattedLines()); + } + + [TestMethod] + public async Task MissingTfBuild_EmitsWarningAndSkipsOutput() + { + AzureDevOpsArtifactUploader uploader = CreateUploader(new Dictionary + { + [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifacts] = [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactsModeAll], + }); + + _ = _environmentMock.Setup(environment => environment.GetEnvironmentVariable("TF_BUILD")).Returns((string?)null); + _ = _fileSystemMock.Setup(fileSystem => fileSystem.ExistDirectory(ResultsDirectory)).Returns(true); + _ = _fileSystemMock.Setup(fileSystem => fileSystem.GetFiles(ResultsDirectory, "*", SearchOption.AllDirectories)) + .Returns([InResults("test.trx")]); + + await uploader.OnTestSessionStartingAsync(new TestSessionContext()).ConfigureAwait(false); + await uploader.ConsumeAsync(CreateProducer("CrashDumpProcessLifetimeHandler", "Crash dump"), CreateFileArtifact(InResults("dump.dmp")), CancellationToken.None).ConfigureAwait(false); + await uploader.ConsumeAsync(CreateProducer(), CreateFailedTestNodeUpdateMessage(), CancellationToken.None).ConfigureAwait(false); + await uploader.OnTestSessionFinishingAsync(new TestSessionContext()).ConfigureAwait(false); + + CollectionAssert.AreEqual( + new[] { "Azure DevOps artifact upload was requested, but TF_BUILD is not set to 'true'; skipping Azure DevOps artifact upload and build tags." }, + GetWarnings()); + Assert.IsEmpty(GetFormattedLines()); + } + + [TestMethod] + public async Task EmptyTestResultsDirectory_DoesNotEmitArtifactLines() + { + AzureDevOpsArtifactUploader uploader = CreateUploader(new Dictionary + { + [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifacts] = [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactsModeFiles], + }); + + _ = _environmentMock.Setup(environment => environment.GetEnvironmentVariable("TF_BUILD")).Returns("true"); + _ = _fileSystemMock.Setup(fileSystem => fileSystem.ExistDirectory(ResultsDirectory)).Returns(true); + _ = _fileSystemMock.Setup(fileSystem => fileSystem.GetFiles(ResultsDirectory, "*", SearchOption.AllDirectories)).Returns([]); + + await uploader.OnTestSessionStartingAsync(new TestSessionContext()).ConfigureAwait(false); + await uploader.OnTestSessionFinishingAsync(new TestSessionContext()).ConfigureAwait(false); + + Assert.IsEmpty(GetFormattedLines()); + Assert.IsEmpty(GetWarnings()); + } + + [TestMethod] + public async Task WhitespaceTestResultsDirectory_DoesNotEmitArtifactLines() + { + AzureDevOpsArtifactUploader uploader = CreateUploader(new Dictionary + { + [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifacts] = [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactsModeFiles], + }); + + _ = _configurationMock.SetupGet(configuration => configuration[PlatformConfigurationConstants.PlatformResultDirectory]).Returns(" "); + _ = _environmentMock.Setup(environment => environment.GetEnvironmentVariable("TF_BUILD")).Returns("true"); + + await uploader.OnTestSessionStartingAsync(new TestSessionContext()).ConfigureAwait(false); + await uploader.OnTestSessionFinishingAsync(new TestSessionContext()).ConfigureAwait(false); + + Assert.IsEmpty(GetFormattedLines()); + Assert.IsEmpty(GetWarnings()); + } + + [TestMethod] + public async Task ArtifactUploadPaths_AreEscaped() + { + AzureDevOpsArtifactUploader uploader = CreateUploader(new Dictionary + { + [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactName] = ["Artifacts"], + [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifacts] = [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactsModeFiles], + }); + + string specialPath = $"{ResultsDirectory}{Path.DirectorySeparatorChar}semi;line\r\nname.trx"; + string escapedSpecialPath = specialPath.Replace(";", "%3B").Replace("\r", "%0D").Replace("\n", "%0A"); + _ = _environmentMock.Setup(environment => environment.GetEnvironmentVariable("TF_BUILD")).Returns("true"); + _ = _fileSystemMock.Setup(fileSystem => fileSystem.ExistDirectory(ResultsDirectory)).Returns(true); + _ = _fileSystemMock.Setup(fileSystem => fileSystem.GetFiles(ResultsDirectory, "*", SearchOption.AllDirectories)).Returns([specialPath]); + + await uploader.OnTestSessionStartingAsync(new TestSessionContext()).ConfigureAwait(false); + await uploader.OnTestSessionFinishingAsync(new TestSessionContext()).ConfigureAwait(false); + + CollectionAssert.AreEqual( + new[] { $"##vso[artifact.upload containerfolder=Artifacts;artifactname=Artifacts]{escapedSpecialPath}" }, + GetFormattedLines()); + } + + [DataRow(AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactInclude, @"C:\absolute\*.trx")] + [DataRow(AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactInclude, "/absolute/*.trx")] + [DataRow(AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactExclude, @"\absolute\*.trx")] + [TestMethod] + public async Task ValidateOptionArgumentsAsync_RejectsAbsoluteGlobPatterns(string optionName, string pattern) + { + var provider = new AzureDevOpsCommandLineProvider(); + Microsoft.Testing.Platform.Extensions.CommandLine.CommandLineOption option = provider.GetCommandLineOptions().Single(commandLineOption => commandLineOption.Name == optionName); + + ValidationResult result = await provider.ValidateOptionArgumentsAsync(option, [pattern]).ConfigureAwait(false); + + Assert.IsFalse(result.IsValid); + Assert.AreEqual($"Invalid glob pattern {pattern}.", result.ErrorMessage); + } + + [DataRow(AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactInclude, false)] + [DataRow(AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactExclude, false)] + [DataRow(AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactName, false)] + [DataRow(AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactInclude, true)] + [DataRow(AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactExclude, true)] + [DataRow(AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactName, true)] + [TestMethod] + public async Task ValidateCommandLineOptionsAsync_RejectsArtifactSettingsWhenUploadsAreDisabled(string optionName, bool setOffMode) + { + var provider = new AzureDevOpsCommandLineProvider(); + var options = new Dictionary + { + [optionName] = optionName == AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactName ? ["Artifacts"] : ["**/*.trx"], + }; + + if (setOffMode) + { + options[AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifacts] = [AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifactsModeOff]; + } + + ValidationResult result = await provider.ValidateCommandLineOptionsAsync(new TestCommandLineOptions(options)).ConfigureAwait(false); + + Assert.IsFalse(result.IsValid); + Assert.AreEqual(ArtifactUploadOptionsRequireUploadArtifactsMessage, result.ErrorMessage); + } + + [TestMethod] + public async Task ValidateOptionArgumentsAsync_RejectsUnknownUploadMode() + { + var provider = new AzureDevOpsCommandLineProvider(); + Microsoft.Testing.Platform.Extensions.CommandLine.CommandLineOption option = provider.GetCommandLineOptions().Single(commandLineOption => commandLineOption.Name == AzureDevOpsCommandLineOptions.AzureDevOpsUploadArtifacts); + + ValidationResult result = await provider.ValidateOptionArgumentsAsync(option, ["unexpected"]).ConfigureAwait(false); + + Assert.IsFalse(result.IsValid); + Assert.AreEqual("Invalid artifact upload mode unexpected.", result.ErrorMessage); + } + + private AzureDevOpsArtifactUploader CreateUploader(Dictionary options) + => new( + new TestCommandLineOptions(options), + _configurationMock.Object, + _environmentMock.Object, + _fileSystemMock.Object, + _outputDeviceMock.Object, + _testApplicationModuleInfoMock.Object, + _loggerFactoryMock.Object); + + private static FileArtifact CreateFileArtifact(string path) + => new(new FileInfo(path), "Artifact"); + + private static TestNodeUpdateMessage CreateFailedTestNodeUpdateMessage() + => CreateTestNodeUpdateMessage(new FailedTestNodeStateProperty()); + + private static TestNodeUpdateMessage CreatePassedTestNodeUpdateMessage() + => CreateTestNodeUpdateMessage(new PassedTestNodeStateProperty()); + + private static TestNodeUpdateMessage CreateTestNodeUpdateMessage(TestNodeStateProperty state) + => new( + new SessionUid("session"), + new TestNode + { + Uid = "test", + DisplayName = "TestDisplayName", + Properties = new PropertyBag(state), + }); + + private static IDataProducer CreateProducer(string uid = "Producer", string displayName = "Producer") + => new TestProducer(uid, displayName); + + private static string InResults(params string[] segments) + => segments.Aggregate(ResultsDirectory, Path.Combine); + + private static string OutsideResults(params string[] segments) + => segments.Aggregate(OutsideResultsDirectory, Path.Combine); + + private string[] GetFormattedLines() + => [.. _outputData.OfType().Select(output => output.Text)]; + + private string[] GetWarnings() + => [.. _outputData.OfType().Select(output => output.Message)]; + + private sealed class TestProducer(string uid, string displayName) : IDataProducer + { + public Type[] DataTypesProduced { get; } = [typeof(FileArtifact)]; + + public string Uid { get; } = uid; + + public string Version { get; } = "1.0.0"; + + public string DisplayName { get; } = displayName; + + public string Description { get; } = displayName; + + public Task IsEnabledAsync() => Task.FromResult(true); + } + + private sealed class TestSessionContext : ITestSessionContext + { + public SessionUid SessionUid { get; } = new("session"); + + public CancellationToken CancellationToken { get; } = CancellationToken.None; + } +}