From 706b54a91a6fa853d01d92eec2ed9f85ca444922 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sat, 16 May 2026 15:44:19 +0200 Subject: [PATCH] Auto-upload test artifacts and build tags to Azure DevOps Add opt-in Azure DevOps artifact upload support, help/info updates, and unit coverage for the new reporter behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AzureDevOpsArtifactUploader.cs | 357 ++++++++++++++ .../AzureDevOpsCommandLineOptions.cs | 8 + .../AzureDevOpsCommandLineProvider.cs | 79 +++- .../AzureDevOpsExtensions.cs | 13 + .../AzureDevOpsReporter.cs | 6 +- ...esting.Extensions.AzureDevOpsReport.csproj | 1 + .../PathComparison.cs | 15 + .../Resources/AzureDevOpsResources.resx | 33 +- .../Resources/xlf/AzureDevOpsResources.cs.xlf | 44 +- .../Resources/xlf/AzureDevOpsResources.de.xlf | 44 +- .../Resources/xlf/AzureDevOpsResources.es.xlf | 44 +- .../Resources/xlf/AzureDevOpsResources.fr.xlf | 44 +- .../Resources/xlf/AzureDevOpsResources.it.xlf | 44 +- .../Resources/xlf/AzureDevOpsResources.ja.xlf | 44 +- .../Resources/xlf/AzureDevOpsResources.ko.xlf | 44 +- .../Resources/xlf/AzureDevOpsResources.pl.xlf | 44 +- .../xlf/AzureDevOpsResources.pt-BR.xlf | 44 +- .../Resources/xlf/AzureDevOpsResources.ru.xlf | 44 +- .../Resources/xlf/AzureDevOpsResources.tr.xlf | 44 +- .../xlf/AzureDevOpsResources.zh-Hans.xlf | 44 +- .../xlf/AzureDevOpsResources.zh-Hant.xlf | 44 +- .../TargetFrameworkMonikerHelper.cs | 14 + .../HelpInfoAllExtensionsTests.cs | 42 ++ .../AzureDevOpsArtifactUploaderTests.cs | 447 ++++++++++++++++++ 24 files changed, 1540 insertions(+), 47 deletions(-) create mode 100644 src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsArtifactUploader.cs create mode 100644 src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/PathComparison.cs create mode 100644 src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/TargetFrameworkMonikerHelper.cs create mode 100644 test/UnitTests/Microsoft.Testing.Extensions.UnitTests/AzureDevOpsArtifactUploaderTests.cs 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 bfe0b50025..01aacd3377 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsCommandLineOptions.cs +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsCommandLineOptions.cs @@ -7,4 +7,12 @@ internal static class AzureDevOpsCommandLineOptions { public const string AzureDevOpsOptionName = "report-azdo"; 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 85598bc581..33309f3ce9 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsCommandLineProvider.cs +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsCommandLineProvider.cs @@ -1,7 +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; @@ -10,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"]; public string Uid => nameof(AzureDevOpsCommandLineProvider); @@ -27,30 +38,70 @@ public IReadOnlyCollection GetCommandLineOptions() [ new CommandLineOption(AzureDevOpsCommandLineOptions.AzureDevOpsOptionName, AzureDevOpsResources.OptionDescription, ArgumentArity.Zero, 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) - { - if (commandOption.Name == AzureDevOpsCommandLineOptions.AzureDevOpsReportSeverity) + => commandOption.Name switch { - if (!SeverityOptions.Contains(arguments[0], StringComparer.OrdinalIgnoreCase)) - { - return ValidationResult.InvalidTask(string.Format(CultureInfo.InvariantCulture, AzureDevOpsResources.InvalidSeverity, arguments[0])); - } - } - - return ValidationResult.ValidTask; - } + 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, + }; public Task ValidateCommandLineOptionsAsync(ICommandLineOptions commandLineOptions) + => !commandLineOptions.IsOptionSet(AzureDevOpsCommandLineOptions.AzureDevOpsOptionName) + && commandLineOptions.IsOptionSet(AzureDevOpsCommandLineOptions.AzureDevOpsReportSeverity) + ? ValidationResult.InvalidTask(AzureDevOpsResources.AzureDevOpsReportSeverityRequiresAzureDevOps) + : HasArtifactUploadConfiguration(commandLineOptions) && IsArtifactUploadDisabled(commandLineOptions) + ? ValidationResult.InvalidTask(AzureDevOpsResources.ArtifactUploadOptionsRequireUploadArtifacts) + : ValidationResult.ValidTask; + + 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) { - if (!commandLineOptions.IsOptionSet(AzureDevOpsCommandLineOptions.AzureDevOpsOptionName) && - commandLineOptions.IsOptionSet(AzureDevOpsCommandLineOptions.AzureDevOpsReportSeverity)) + foreach (string argument in arguments) { - // If report-azdo is not set, but report-azdo-severity is set, it's invalid. - return ValidationResult.InvalidTask(AzureDevOpsResources.AzureDevOpsReportSeverityRequiresAzureDevOps); + 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, '/'); } diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsExtensions.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsExtensions.cs index 728a257520..d16eb29910 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsExtensions.cs +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsExtensions.cs @@ -29,7 +29,20 @@ public static void AddAzureDevOpsProvider(this ITestApplicationBuilder builder) serviceProvider.GetOutputDevice(), serviceProvider.GetLoggerFactory())); + var compositeArtifactUploader = + new CompositeExtensionFactory(serviceProvider => + new AzureDevOpsArtifactUploader( + serviceProvider.GetCommandLineOptions(), + serviceProvider.GetConfiguration(), + serviceProvider.GetEnvironment(), + serviceProvider.GetFileSystem(), + serviceProvider.GetOutputDevice(), + serviceProvider.GetTestApplicationModuleInfo(), + serviceProvider.GetLoggerFactory())); + builder.TestHost.AddDataConsumer(compositeTestSessionAzDoService); + builder.TestHost.AddDataConsumer(compositeArtifactUploader); + 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 aebb6b47e8..6379b27e8c 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsReporter.cs +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/AzureDevOpsReporter.cs @@ -42,7 +42,7 @@ public AzureDevOpsReporter( _fileSystem = fileSystem; _outputDisplay = outputDisplay; _logger = loggerFactory.CreateLogger(); - _targetFrameworkMoniker = GetTargetFrameworkMoniker(); + _targetFrameworkMoniker = TargetFrameworkMonikerHelper.GetTargetFrameworkMoniker(); } public Type[] DataTypesConsumed { get; } = @@ -307,10 +307,6 @@ private async Task WriteExceptionAsync(string testDisplayName, string? explanati return null; } - private static string GetTargetFrameworkMoniker() - => TargetFrameworkParser.GetShortTargetFramework(Assembly.GetEntryAssembly()?.GetCustomAttribute()?.FrameworkDisplayName) - ?? TargetFrameworkParser.GetShortTargetFramework(RuntimeInformation.FrameworkDescription); - private static (string Code, string File, int LineNumber)? GetStackFrameLocation(string stackTraceLine) { Match match = StackTraceHelper.GetFrameRegex().Match(stackTraceLine); 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 025d2c16f3..7585d91e4d 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,6 +48,7 @@ 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 516d5d4382..48ad43a860 100644 --- a/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/AzureDevOpsResources.resx +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOpsReport/Resources/AzureDevOpsResources.resx @@ -130,10 +130,39 @@ Invalid option {0}. - Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Enable Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. Severity to use for the reported event. Options are: error (default) and warning. {Locked="error"}{Locked="warning"} - \ No newline at end of file + + 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"} + + 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 076a3037b9..6d08a90dbb 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-severity' requires '--report-azdo' to be enabled --report-azdo-severity vyžaduje, aby byla povolena možnost --report-azdo @@ -17,14 +27,24 @@ Generátor sestav Azure DevOps + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid option {0}. Neplatná možnost {0}. - Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. - Umožňuje generátoru sestav Azure DevOps zapisovat chyby do výstupu způsobem, který je pro AzureDev Ops srozumitelný. + Enable Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Umožňuje generátoru sestav Azure DevOps zapisovat chyby do výstupu způsobem, který je pro AzureDev Ops srozumitelný. @@ -32,6 +52,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 87be6a6ce5..db9c093cd8 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-severity' requires '--report-azdo' to be enabled Für „--report-azdo-severity“ muss „--report-azdo“ aktiviert sein. @@ -17,14 +27,24 @@ Azure DevOps-Berichts-Generator + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid option {0}. Ungültige Option „{0}“. - Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. - Azure DevOps-Berichts-Generator aktivieren, um Fehler auf eine Weise in die Ausgabe zu schreiben, die AzureDev Ops versteht. + Enable Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Azure DevOps-Berichts-Generator aktivieren, um Fehler auf eine Weise in die Ausgabe zu schreiben, die AzureDev Ops versteht. @@ -32,6 +52,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 c4a61c952f..f8ae542667 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-severity' requires '--report-azdo' to be enabled "--report-azdo-severity" requiere que se habilite "--report-azdo" @@ -17,14 +27,24 @@ Generador de informes de Azure DevOps + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid option {0}. Opción no válida {0}. - Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. - Habilite el generador de informes de Azure DevOps para escribir errores en la salida de una manera que AzureDev Ops comprenda. + Enable Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Habilite el generador de informes de Azure DevOps para escribir errores en la salida de una manera que AzureDev Ops comprenda. @@ -32,6 +52,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 4feb6d0ab7..ca4c768ac2 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-severity' requires '--report-azdo' to be enabled « --report-azdo-severity » nécessite l’activation de « --report-azdo » @@ -17,14 +27,24 @@ Générateur de rapports Azure DevOps + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid option {0}. Option {0} non valide. - Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. - Activez le générateur de rapports Azure DevOps pour écrire les erreurs dans la sortie d’une manière comprise par Azure DevOps. + Enable Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Activez le générateur de rapports Azure DevOps pour écrire les erreurs dans la sortie d’une manière comprise par Azure DevOps. @@ -32,6 +52,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 d9756f675c..5d457b9f66 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-severity' requires '--report-azdo' to be enabled '--report-azdo-severity' richiede l'autenticazione di '--report-azdo' @@ -17,14 +27,24 @@ Generatore di report di Azure DevOps + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid option {0}. Opzione non valida {0}. - Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. - Abilitare il generatore di report di Azure DevOps per scrivere gli errori nell'output in un modo che Azure DevOps comprenda. + Enable Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Abilitare il generatore di report di Azure DevOps per scrivere gli errori nell'output in un modo che Azure DevOps comprenda. @@ -32,6 +52,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 ea3529945b..48a3a826cb 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-severity' requires '--report-azdo' to be enabled '--report-azdo-severity' では、'--report-azdo' を有効にする必要があります @@ -17,14 +27,24 @@ Azure DevOps レポート生成プログラム + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid option {0}. オプション {0} が無効です。 - Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. - Azure DevOps レポート生成プログラムを有効にして、AzureDev Ops が理解する方法で出力にエラーを書き込みます。 + Enable Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Azure DevOps レポート生成プログラムを有効にして、AzureDev Ops が理解する方法で出力にエラーを書き込みます。 @@ -32,6 +52,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 f74caa7f6e..7ae8f614f4 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-severity' requires '--report-azdo' to be enabled '--report-azdo-severity'를 사용하려면 '--report-azdo'를 사용 설정 해야합니다. @@ -17,14 +27,24 @@ Azure DevOps 보고서 생성기 + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid option {0}. {0} 옵션이 잘못되었습니다. - Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. - Azure DevOps 보고서 생성기를 활성화하여 Azure DevOps가 이해할 수 있는 방식으로 출력에 오류를 기록합니다. + Enable Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Azure DevOps 보고서 생성기를 활성화하여 Azure DevOps가 이해할 수 있는 방식으로 출력에 오류를 기록합니다. @@ -32,6 +52,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 8890f27f70..9e1cd56299 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-severity' requires '--report-azdo' to be enabled Element „--report-azdo-severity” wymaga włączenia polecenia „--report-azdo” @@ -17,14 +27,24 @@ Generator raportów usługi Azure DevOps + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid option {0}. Nieprawidłowa opcja {0}. - Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. - Włącz generator raportów usługi Azure DevOps, aby zapisywać błędy w danych wyjściowych w sposób zrozumiały dla usługi Azure DevOps. + Enable Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Włącz generator raportów usługi Azure DevOps, aby zapisywać błędy w danych wyjściowych w sposób zrozumiały dla usługi Azure DevOps. @@ -32,6 +52,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 d660d0e56a..113bf6d0ce 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-severity' requires '--report-azdo' to be enabled '--report-azdo-severity' requer que '--report-azdo' esteja habilitado @@ -17,14 +27,24 @@ Gerador de relatórios do Azure DevOps + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid option {0}. Opção {0} inválida. - Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. - Habilitar o gerador de relatórios do Azure DevOps para gravar erros na saída de uma forma que o Azure DevOps entenda. + Enable Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Habilitar o gerador de relatórios do Azure DevOps para gravar erros na saída de uma forma que o Azure DevOps entenda. @@ -32,6 +52,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 756d7c9cae..5f8b2b229a 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-severity' requires '--report-azdo' to be enabled "--report-azdo-severity" требует включения "--report-azdo" @@ -17,14 +27,24 @@ Генератор отчетов Azure DevOps + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid option {0}. Недопустимый параметр {0}. - Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. - Включите генератор отчетов Azure DevOps для записи ошибок в выходные данные в формате, который понимает Azure DevOps. + Enable Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Включите генератор отчетов Azure DevOps для записи ошибок в выходные данные в формате, который понимает Azure DevOps. @@ -32,6 +52,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 69207860a2..974f3e0383 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-severity' requires '--report-azdo' to be enabled '--report-azdo-severity' öğesi '--report-azdo' öğesinin etkinleştirilmesini gerektirir @@ -17,14 +27,24 @@ Azure DevOps rapor oluşturucusu + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid option {0}. Geçersiz seçenek{0}. - Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. - Azure DevOps rapor oluşturucusunu, hataları Azure DevOps'un anlayacağı şekilde çıktıya yazması için etkinleştirin. + Enable Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Azure DevOps rapor oluşturucusunu, hataları Azure DevOps'un anlayacağı şekilde çıktıya yazması için etkinleştirin. @@ -32,6 +52,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 2b5199f4ad..f0e396e925 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-severity' requires '--report-azdo' to be enabled '--report-azdo-severity' 要求启用 '--report-azdo' @@ -17,14 +27,24 @@ Azure DevOps报表生成器 + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid option {0}. 选项 {0} 无效。 - Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. - 启用 Azure DevOps 报表生成器,以通过 Azure DevOps 可理解的方式将错误写入输出。 + Enable Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + 启用 Azure DevOps 报表生成器,以通过 Azure DevOps 可理解的方式将错误写入输出。 @@ -32,6 +52,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 6c61f0fb8f..8560099d19 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-severity' requires '--report-azdo' to be enabled '--report-azdo-severity' 需要啟用 '--report-azdo' @@ -17,14 +27,24 @@ Azure DevOps 報告產生器 + + Invalid glob pattern {0}. + Invalid glob pattern {0}. + + + + Invalid artifact upload mode {0}. + Invalid artifact upload mode {0}. + + Invalid option {0}. 無效的選項 {0}。 - Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. - 啟用 Azure DevOps 報告產生器,以 Azure DevOps 可理解的方式將錯誤寫入輸出。 + Enable Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + 啟用 Azure DevOps 報告產生器,以 Azure DevOps 可理解的方式將錯誤寫入輸出。 @@ -32,6 +52,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 7449a51387..0642494267 100644 --- a/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoAllExtensionsTests.cs +++ b/test/IntegrationTests/Microsoft.Testing.Platform.Acceptance.IntegrationTests/HelpInfoAllExtensionsTests.cs @@ -100,6 +100,18 @@ Disable reporting progress to screen. --output Output verbosity when reporting tests. Valid values are 'Normal', 'Detailed'. Default is 'Normal'. + --report-azdo + Enable Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + --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 @@ -265,6 +277,35 @@ Note that this is slowing down the test execution. Description: A global test execution timeout. Takes one argument as string in the format [h|m|s] where 'value' is float. Registered command line providers: + AzureDevOpsCommandLineProvider + Name: Azure DevOps report generator + Version: * + Description: Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Options: + --report-azdo + Arity: 0 + Hidden: False + Description: Enable Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + --report-azdo-severity + 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: * @@ -431,6 +472,7 @@ public sealed class TestAssetFixture() : TestAssetFixtureBase() + 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..18d8d1989d --- /dev/null +++ b/test/UnitTests/Microsoft.Testing.Extensions.UnitTests/AzureDevOpsArtifactUploaderTests.cs @@ -0,0 +1,447 @@ +// 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 const string ResultsDirectory = @"Q:\results"; + + 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([@"Q:\results\test.trx"]); + + Assert.IsFalse(await uploader.IsEnabledAsync().ConfigureAwait(false)); + + await uploader.OnTestSessionStartingAsync(new TestSessionContext()).ConfigureAwait(false); + await uploader.ConsumeAsync(CreateProducer("CrashDumpProcessLifetimeHandler", "Crash dump"), CreateFileArtifact(@"Q:\results\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(@"Q:\results\crashdump.dmp"), CancellationToken.None).ConfigureAwait(false); + await uploader.ConsumeAsync(CreateProducer("HangDumpProcessLifetimeHandler", "Hang dump"), CreateFileArtifact(@"Q:\results\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([@"Q:\results\a.trx", @"Q:\results\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([@"Q:\results\inside.trx", @"Q:\results-other\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]Q:\results\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([@"Q:\results\test.trx"]); + + await uploader.OnTestSessionStartingAsync(new TestSessionContext()).ConfigureAwait(false); + await uploader.ConsumeAsync(CreateProducer("CrashDumpProcessLifetimeHandler", "Crash dump"), CreateFileArtifact(@"Q:\results\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]Q:\results\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([@"Q:\results\keep.trx", @"Q:\results\keep.coverage", @"Q:\results\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]Q:\results\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(@"Q:\results\dump.dmp"), CancellationToken.None).ConfigureAwait(false); + await uploader.ConsumeAsync(CreateProducer("HangDumpProcessLifetimeHandler", "Hang dump"), CreateFileArtifact(@"Q:\results\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(@"Q:\results\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([@"Q:\results\test.trx"]); + + await uploader.OnTestSessionStartingAsync(new TestSessionContext()).ConfigureAwait(false); + await uploader.ConsumeAsync(CreateProducer("CrashDumpProcessLifetimeHandler", "Crash dump"), CreateFileArtifact(@"Q:\results\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 = "Q:\\results\\semi;line\r\nname.trx"; + _ = _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]Q:\\results\\semi%3Bline%0D%0Aname.trx" }, + 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 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; + } +}