diff --git a/TestFx.sln b/TestFx.sln index d8d48028b3..53b4a4b320 100644 --- a/TestFx.sln +++ b/TestFx.sln @@ -222,6 +222,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MSTest.Engine.UnitTests", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MSTest.SourceGeneration.UnitTests", "test\UnitTests\MSTest.SourceGeneration.UnitTests\MSTest.SourceGeneration.UnitTests.csproj", "{E6C0466E-BE8D-C04F-149A-FD98438F1413}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Testing.Extensions.AzureDevOps", "src\Platform\Microsoft.Testing.Extensions.AzureDevOps\Microsoft.Testing.Extensions.AzureDevOps.csproj", "{F608D3A3-125B-CD88-1D51-8714ED142029}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -524,6 +526,10 @@ Global {E6C0466E-BE8D-C04F-149A-FD98438F1413}.Debug|Any CPU.Build.0 = Debug|Any CPU {E6C0466E-BE8D-C04F-149A-FD98438F1413}.Release|Any CPU.ActiveCfg = Release|Any CPU {E6C0466E-BE8D-C04F-149A-FD98438F1413}.Release|Any CPU.Build.0 = Release|Any CPU + {F608D3A3-125B-CD88-1D51-8714ED142029}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F608D3A3-125B-CD88-1D51-8714ED142029}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F608D3A3-125B-CD88-1D51-8714ED142029}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F608D3A3-125B-CD88-1D51-8714ED142029}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -615,6 +621,7 @@ Global {7BA0E74E-798E-4399-2EDE-A23BD5DA78CA} = {E7F15C9C-3928-47AD-8462-64FD29FFCA54} {2C0DFAC0-5D58-D172-ECE4-CBB78AD03435} = {BB874DF1-44FE-415A-B634-A6B829107890} {E6C0466E-BE8D-C04F-149A-FD98438F1413} = {BB874DF1-44FE-415A-B634-A6B829107890} + {F608D3A3-125B-CD88-1D51-8714ED142029} = {6AEE1440-FDF0-4729-8196-B24D0E333550} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {31E0F4D5-975A-41CC-933E-545B2201FAF9} diff --git a/eng/Versions.props b/eng/Versions.props index 496c0c00dc..898e64057a 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -1,9 +1,9 @@ - 3.9.0 + 3.10.0 - 1.7.0 + 1.8.0 preview diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/AzDoEscaper.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/AzDoEscaper.cs new file mode 100644 index 0000000000..c64862bcc9 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/AzDoEscaper.cs @@ -0,0 +1,39 @@ +// 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; + +namespace Microsoft.Testing.Extensions.Reporting; + +internal static class AzDoEscaper +{ + public static string Escape(string value) + { + if (RoslynString.IsNullOrEmpty(value)) + { + return value; + } + + var result = new StringBuilder(value.Length); + foreach (char c in value) + { + switch (c) + { + case ';': + result.Append("%3B"); + break; + case '\r': + result.Append("%0D"); + break; + case '\n': + result.Append("%0A"); + break; + default: + result.Append(c); + break; + } + } + + return result.ToString(); + } +} diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/AzureDevOpsCommandLineOptions.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/AzureDevOpsCommandLineOptions.cs new file mode 100644 index 0000000000..bfe0b50025 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/AzureDevOpsCommandLineOptions.cs @@ -0,0 +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. + +namespace Microsoft.Testing.Extensions.Reporting; + +internal static class AzureDevOpsCommandLineOptions +{ + public const string AzureDevOpsOptionName = "report-azdo"; + public const string AzureDevOpsReportSeverity = "report-azdo-severity"; +} diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/AzureDevOpsCommandLineProvider.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/AzureDevOpsCommandLineProvider.cs new file mode 100644 index 0000000000..3727cd22fd --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/AzureDevOpsCommandLineProvider.cs @@ -0,0 +1,48 @@ +// 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.AzureDevOps.Resources; +using Microsoft.Testing.Platform.CommandLine; +using Microsoft.Testing.Platform.Extensions; +using Microsoft.Testing.Platform.Extensions.CommandLine; +using Microsoft.Testing.Platform.Helpers; + +namespace Microsoft.Testing.Extensions.Reporting; + +internal sealed class AzureDevOpsCommandLineProvider : ICommandLineOptionsProvider +{ + private static readonly string[] SeverityOptions = ["error", "warning"]; + + public string Uid => nameof(AzureDevOpsCommandLineProvider); + + public string Version => AppVersion.DefaultSemVer; + + public string DisplayName => AzureDevOpsResources.DisplayName; + + public string Description => AzureDevOpsResources.Description; + + public Task IsEnabledAsync() => Task.FromResult(true); + + public IReadOnlyCollection GetCommandLineOptions() + => + [ + new CommandLineOption(AzureDevOpsCommandLineOptions.AzureDevOpsOptionName, AzureDevOpsResources.OptionDescription, ArgumentArity.Zero, false), + new CommandLineOption(AzureDevOpsCommandLineOptions.AzureDevOpsReportSeverity, AzureDevOpsResources.SeverityOptionDescription, ArgumentArity.ExactlyOne, false), + ]; + + public Task ValidateOptionArgumentsAsync(CommandLineOption commandOption, string[] arguments) + { + if (commandOption.Name == AzureDevOpsCommandLineOptions.AzureDevOpsReportSeverity) + { + if (!SeverityOptions.Contains(arguments[0], StringComparer.OrdinalIgnoreCase)) + { + return ValidationResult.InvalidTask(string.Format(CultureInfo.InvariantCulture, AzureDevOpsResources.InvalidSeverity, arguments[0])); + } + } + + return ValidationResult.ValidTask; + } + + public Task ValidateCommandLineOptionsAsync(ICommandLineOptions commandLineOptions) + => ValidationResult.ValidTask; +} diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/AzureDevOpsExtensions.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/AzureDevOpsExtensions.cs new file mode 100644 index 0000000000..696ee37572 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/AzureDevOpsExtensions.cs @@ -0,0 +1,34 @@ +// 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.Reporting; +using Microsoft.Testing.Extensions.TrxReport.Abstractions; +using Microsoft.Testing.Platform.Builder; +using Microsoft.Testing.Platform.Extensions; +using Microsoft.Testing.Platform.Services; + +namespace Microsoft.Testing.Extensions; + +/// +/// Provides extension methods for adding Azure DevOps reporting support to the test application builder. +/// +public static class AzureDevOpsExtensions +{ + /// + /// Adds support to the test application builder. + /// + /// The test application builder. + public static void AddAzureDevOpsProvider(this ITestApplicationBuilder builder) + { + var compositeTestSessionAzDoService = + new CompositeExtensionFactory(serviceProvider => + new AzureDevOpsReporter( + serviceProvider.GetCommandLineOptions(), + serviceProvider.GetEnvironment(), + serviceProvider.GetOutputDevice())); + + builder.TestHost.AddDataConsumer(compositeTestSessionAzDoService); + + builder.CommandLine.AddProvider(() => new AzureDevOpsCommandLineProvider()); + } +} diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/AzureDevOpsReporter.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/AzureDevOpsReporter.cs new file mode 100644 index 0000000000..ad3572d3c3 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/AzureDevOpsReporter.cs @@ -0,0 +1,167 @@ +// 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.AzureDevOps; +using Microsoft.Testing.Extensions.AzureDevOps.Resources; +using Microsoft.Testing.Extensions.Reporting; +using Microsoft.Testing.Platform; +using Microsoft.Testing.Platform.CommandLine; +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.OutputDevice; + +namespace Microsoft.Testing.Extensions.TrxReport.Abstractions; + +internal sealed class AzureDevOpsReporter : + IDataConsumer, + IDataProducer, + IOutputDeviceDataProducer +{ + private readonly IOutputDevice _outputDisplay; + + private static readonly char[] NewlineCharacters = new char[] { '\r', '\n' }; + private readonly ICommandLineOptions _commandLine; + private readonly IEnvironment _environment; + private string _severity = "error"; + + public AzureDevOpsReporter( + ICommandLineOptions commandLine, + IEnvironment environment, + IOutputDevice outputDisplay) + { + _commandLine = commandLine; + _environment = environment; + _outputDisplay = outputDisplay; + } + + public Type[] DataTypesConsumed { get; } = + [ + typeof(TestNodeUpdateMessage) + ]; + + public Type[] DataTypesProduced { get; } = [typeof(SessionFileArtifact)]; + + /// + public string Uid { get; } = nameof(AzureDevOpsReporter); + + /// + public string Version { get; } = AppVersion.DefaultSemVer; + + /// + public string DisplayName { get; } = AzureDevOpsResources.DisplayName; + + /// + public string Description { get; } = AzureDevOpsResources.Description; + + /// + public Task IsEnabledAsync() + { + bool isEnabled = _commandLine.IsOptionSet(AzureDevOpsCommandLineOptions.AzureDevOpsOptionName) + && string.Equals(_environment.GetEnvironmentVariable("TF_BUILD"), "true", StringComparison.OrdinalIgnoreCase); + + if (isEnabled) + { + bool found = _commandLine.TryGetOptionArgumentList(AzureDevOpsCommandLineOptions.AzureDevOpsReportSeverity, out string[]? arguments); + if (found && arguments?.Length > 0) + { + _severity = arguments[0].ToLowerInvariant(); + } + } + + return Task.FromResult(isEnabled); + } + + public async Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + if (value is not TestNodeUpdateMessage nodeUpdateMessage) + { + return; + } + + TestNodeStateProperty nodeState = nodeUpdateMessage.TestNode.Properties.Single(); + + switch (nodeState) + { + case FailedTestNodeStateProperty failed: + await WriteExceptionAsync(failed.Explanation, failed.Exception); + break; + case ErrorTestNodeStateProperty error: + await WriteExceptionAsync(error.Explanation, error.Exception); + break; + case CancelledTestNodeStateProperty cancelled: + await WriteExceptionAsync(cancelled.Explanation, cancelled.Exception); + break; + case TimeoutTestNodeStateProperty timeout: + await WriteExceptionAsync(timeout.Explanation, timeout.Exception); + break; + } + + return; + } + + private async Task WriteExceptionAsync(string? explanation, Exception? exception) + { + if (exception == null || exception.StackTrace == null) + { + return; + } + + string message = explanation ?? exception.Message; + + if (message == null) + { + return; + } + + string stackTrace = exception.StackTrace; + int index = stackTrace.IndexOfAny(NewlineCharacters); + string firstLine = index == -1 ? stackTrace : stackTrace.Substring(0, index); + if (firstLine != null) + { + (string Code, string File, int LineNumber)? location = GetStackFrameLocation(firstLine); + if (location != null) + { + string root = RootFinder.Find(); + string file = location.Value.File; + string relativePath = file.StartsWith(root, StringComparison.CurrentCultureIgnoreCase) ? file.Substring(root.Length) : file; + string relativeNormalizedPath = relativePath.Replace('\\', '/'); + + string err = AzDoEscaper.Escape(message); + + string line = $"##vso[task.logissue type={_severity};sourcepath={relativeNormalizedPath};linenumber={location.Value.LineNumber};columnnumber=1]{err}"; + await _outputDisplay.DisplayAsync(this, new FormattedTextOutputDeviceData(line)); + } + } + } + + internal /* for testing */ static (string Code, string File, int LineNumber)? GetStackFrameLocation(string stackTraceLine) + { + Match match = StackTraceHelper.GetFrameRegex().Match(stackTraceLine); + if (!match.Success) + { + return null; + } + + bool weHaveFilePathAndCodeLine = !RoslynString.IsNullOrWhiteSpace(match.Groups["code"].Value); + if (!weHaveFilePathAndCodeLine) + { + return null; + } + + if (RoslynString.IsNullOrWhiteSpace(match.Groups["file"].Value)) + { + return null; + } + + int line = int.TryParse(match.Groups["line"].Value, out int value) ? value : 0; + + return (match.Groups["code"].Value, match.Groups["file"].Value, line); + } +} diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/BannedSymbols.txt b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/BannedSymbols.txt new file mode 100644 index 0000000000..ea8617fcb0 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/BannedSymbols.txt @@ -0,0 +1,10 @@ +T:System.ArgumentNullException; Use 'ArgumentGuard' instead +P:System.DateTime.Now; Use 'IClock' instead +P:System.DateTime.UtcNow; Use 'IClock' instead +M:System.Threading.Tasks.Task.Run(System.Action); Use 'ITask' instead +M:System.Threading.Tasks.Task.WhenAll(System.Threading.Tasks.Task[]); Use 'ITask' instead +M:System.Threading.Tasks.Task.WhenAll(System.Collections.Generic.IEnumerable{System.Threading.Tasks.Task}); Use 'ITask' instead +M:System.String.IsNullOrEmpty(System.String); Use 'RoslynString.IsNullOrEmpty' instead +M:System.String.IsNullOrWhiteSpace(System.String); Use 'RoslynString.IsNullOrWhiteSpace' instead +M:System.Diagnostics.Debug.Assert(System.Boolean); Use 'RoslynDebug.Assert' instead +M:System.Diagnostics.Debug.Assert(System.Boolean,System.String); Use 'RoslynDebug.Assert' instead diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Microsoft.Testing.Extensions.AzureDevOps.csproj b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Microsoft.Testing.Extensions.AzureDevOps.csproj new file mode 100644 index 0000000000..342819aa1b --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Microsoft.Testing.Extensions.AzureDevOps.csproj @@ -0,0 +1,49 @@ + + + + netstandard2.0;$(MicrosoftTestingTargetFrameworks) + + + 1.0.0 + alpha + + + + + + + + + + + + + + true + buildMultiTargeting + + + buildTransitive/$(TargetFramework) + + + build/$(TargetFramework) + + + + + + + + + + + + + + + + + + diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/PACKAGE.md b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/PACKAGE.md new file mode 100644 index 0000000000..214ac847f6 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/PACKAGE.md @@ -0,0 +1,9 @@ +# Microsoft.Testing + +Microsoft Testing is a set of platform, framework and protocol intended to make it possible to run any test on any target or device. + +Documentation can be found at . + +## About + +This package extends Microsoft Testing Platform to provide Azure DevOps reporting. diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/PublicAPI/PublicAPI.Shipped.txt b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/PublicAPI/PublicAPI.Shipped.txt new file mode 100644 index 0000000000..7dc5c58110 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/PublicAPI/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/PublicAPI/PublicAPI.Unshipped.txt b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/PublicAPI/PublicAPI.Unshipped.txt new file mode 100644 index 0000000000..eabd9f205a --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/PublicAPI/PublicAPI.Unshipped.txt @@ -0,0 +1,5 @@ +#nullable enable +Microsoft.Testing.Extensions.AzureDevOps.TestingPlatformBuilderHook +Microsoft.Testing.Extensions.AzureDevOpsExtensions +static Microsoft.Testing.Extensions.AzureDevOps.TestingPlatformBuilderHook.AddExtensions(Microsoft.Testing.Platform.Builder.ITestApplicationBuilder! testApplicationBuilder, string![]! _) -> void +static Microsoft.Testing.Extensions.AzureDevOpsExtensions.AddAzureDevOpsProvider(this Microsoft.Testing.Platform.Builder.ITestApplicationBuilder! builder) -> void diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/AzureDevOpsResources.resx b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/AzureDevOpsResources.resx new file mode 100644 index 0000000000..8114f1a73b --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/AzureDevOpsResources.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + + + Azure DevOps report generator + + + Invalid option {0}. + + + Eanble 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. + Do not translated 'error' or 'warning' those are literal values. + + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.cs.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.cs.xlf new file mode 100644 index 0000000000..fc0b855a4b --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.cs.xlf @@ -0,0 +1,32 @@ + + + + + + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + + + + Azure DevOps report generator + Azure DevOps report generator + + + + Invalid option {0}. + Invalid option {0}. + + + + Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Eanble 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. + Severity to use for the reported event. Options are: error (default) and warning. + Do not translated 'error' or 'warning' those are literal values. + + + + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.de.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.de.xlf new file mode 100644 index 0000000000..9a6ee72bc6 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.de.xlf @@ -0,0 +1,32 @@ + + + + + + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + + + + Azure DevOps report generator + Azure DevOps report generator + + + + Invalid option {0}. + Invalid option {0}. + + + + Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Eanble 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. + Severity to use for the reported event. Options are: error (default) and warning. + Do not translated 'error' or 'warning' those are literal values. + + + + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.es.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.es.xlf new file mode 100644 index 0000000000..c3877b772e --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.es.xlf @@ -0,0 +1,32 @@ + + + + + + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + + + + Azure DevOps report generator + Azure DevOps report generator + + + + Invalid option {0}. + Invalid option {0}. + + + + Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Eanble 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. + Severity to use for the reported event. Options are: error (default) and warning. + Do not translated 'error' or 'warning' those are literal values. + + + + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.fr.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.fr.xlf new file mode 100644 index 0000000000..2fb2366fd4 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.fr.xlf @@ -0,0 +1,32 @@ + + + + + + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + + + + Azure DevOps report generator + Azure DevOps report generator + + + + Invalid option {0}. + Invalid option {0}. + + + + Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Eanble 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. + Severity to use for the reported event. Options are: error (default) and warning. + Do not translated 'error' or 'warning' those are literal values. + + + + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.it.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.it.xlf new file mode 100644 index 0000000000..ca1921e41b --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.it.xlf @@ -0,0 +1,32 @@ + + + + + + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + + + + Azure DevOps report generator + Azure DevOps report generator + + + + Invalid option {0}. + Invalid option {0}. + + + + Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Eanble 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. + Severity to use for the reported event. Options are: error (default) and warning. + Do not translated 'error' or 'warning' those are literal values. + + + + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.ja.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.ja.xlf new file mode 100644 index 0000000000..1ebedda752 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.ja.xlf @@ -0,0 +1,32 @@ + + + + + + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + + + + Azure DevOps report generator + Azure DevOps report generator + + + + Invalid option {0}. + Invalid option {0}. + + + + Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Eanble 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. + Severity to use for the reported event. Options are: error (default) and warning. + Do not translated 'error' or 'warning' those are literal values. + + + + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.ko.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.ko.xlf new file mode 100644 index 0000000000..72115a171c --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.ko.xlf @@ -0,0 +1,32 @@ + + + + + + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + + + + Azure DevOps report generator + Azure DevOps report generator + + + + Invalid option {0}. + Invalid option {0}. + + + + Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Eanble 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. + Severity to use for the reported event. Options are: error (default) and warning. + Do not translated 'error' or 'warning' those are literal values. + + + + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.pl.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.pl.xlf new file mode 100644 index 0000000000..6bf78a2580 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.pl.xlf @@ -0,0 +1,32 @@ + + + + + + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + + + + Azure DevOps report generator + Azure DevOps report generator + + + + Invalid option {0}. + Invalid option {0}. + + + + Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Eanble 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. + Severity to use for the reported event. Options are: error (default) and warning. + Do not translated 'error' or 'warning' those are literal values. + + + + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.pt-BR.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.pt-BR.xlf new file mode 100644 index 0000000000..29986e2a74 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.pt-BR.xlf @@ -0,0 +1,32 @@ + + + + + + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + + + + Azure DevOps report generator + Azure DevOps report generator + + + + Invalid option {0}. + Invalid option {0}. + + + + Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Eanble 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. + Severity to use for the reported event. Options are: error (default) and warning. + Do not translated 'error' or 'warning' those are literal values. + + + + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.ru.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.ru.xlf new file mode 100644 index 0000000000..1495ec1d00 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.ru.xlf @@ -0,0 +1,32 @@ + + + + + + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + + + + Azure DevOps report generator + Azure DevOps report generator + + + + Invalid option {0}. + Invalid option {0}. + + + + Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Eanble 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. + Severity to use for the reported event. Options are: error (default) and warning. + Do not translated 'error' or 'warning' those are literal values. + + + + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.tr.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.tr.xlf new file mode 100644 index 0000000000..f9aab364ab --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.tr.xlf @@ -0,0 +1,32 @@ + + + + + + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + + + + Azure DevOps report generator + Azure DevOps report generator + + + + Invalid option {0}. + Invalid option {0}. + + + + Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Eanble 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. + Severity to use for the reported event. Options are: error (default) and warning. + Do not translated 'error' or 'warning' those are literal values. + + + + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.zh-Hans.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.zh-Hans.xlf new file mode 100644 index 0000000000..460f2cd2df --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.zh-Hans.xlf @@ -0,0 +1,32 @@ + + + + + + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + + + + Azure DevOps report generator + Azure DevOps report generator + + + + Invalid option {0}. + Invalid option {0}. + + + + Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Eanble 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. + Severity to use for the reported event. Options are: error (default) and warning. + Do not translated 'error' or 'warning' those are literal values. + + + + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.zh-Hant.xlf b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.zh-Hant.xlf new file mode 100644 index 0000000000..62c67b8235 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/Resources/xlf/AzureDevOpsResources.zh-Hant.xlf @@ -0,0 +1,32 @@ + + + + + + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + + + + Azure DevOps report generator + Azure DevOps report generator + + + + Invalid option {0}. + Invalid option {0}. + + + + Eanble Azure DevOps report generator to write errors to the output in a way that AzureDev Ops understands. + Eanble 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. + Severity to use for the reported event. Options are: error (default) and warning. + Do not translated 'error' or 'warning' those are literal values. + + + + \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/RootFinder.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/RootFinder.cs new file mode 100644 index 0000000000..c51612f974 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/RootFinder.cs @@ -0,0 +1,34 @@ +// 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.AzureDevOps; + +internal static class RootFinder +{ + private static string? s_root; + + public static string Find() + { + if (s_root != null) + { + return s_root; + } + + string path = AppContext.BaseDirectory; + string dir = path; + while (Directory.GetDirectoryRoot(dir) != dir) + { + if (Directory.Exists(Path.Combine(dir, ".git"))) + { + s_root = dir + Path.DirectorySeparatorChar; + return dir + Path.DirectorySeparatorChar; + } + else + { + dir = Directory.GetParent(dir)!.ToString(); + } + } + + throw new InvalidOperationException($"Could not find solution root, .git not found in {path} or any parent directory."); + } +} diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/TestingPlatformBuilderHook.cs b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/TestingPlatformBuilderHook.cs new file mode 100644 index 0000000000..930066693a --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/TestingPlatformBuilderHook.cs @@ -0,0 +1,20 @@ +// 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.Builder; + +namespace Microsoft.Testing.Extensions.AzureDevOps; + +/// +/// This class is used by Microsoft.Testing.Platform.MSBuild to hook into the Testing Platform Builder to add Azure DevOps reporting support. +/// +public static class TestingPlatformBuilderHook +{ + /// + /// Adds Azure DevOps reporting support to the Testing Platform Builder. + /// + /// The test application builder. + /// The command line arguments. + public static void AddExtensions(ITestApplicationBuilder testApplicationBuilder, string[] _) + => testApplicationBuilder.AddAzureDevOpsProvider(); +} diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/build/Microsoft.Testing.Extensions.AzureDevOps.props b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/build/Microsoft.Testing.Extensions.AzureDevOps.props new file mode 100644 index 0000000000..57f9f713e2 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/build/Microsoft.Testing.Extensions.AzureDevOps.props @@ -0,0 +1,3 @@ + + + diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/buildMultiTargeting/Microsoft.Testing.Extensions.AzureDevOps.props b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/buildMultiTargeting/Microsoft.Testing.Extensions.AzureDevOps.props new file mode 100644 index 0000000000..9cd86af126 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/buildMultiTargeting/Microsoft.Testing.Extensions.AzureDevOps.props @@ -0,0 +1,14 @@ + + + + + + Microsoft.Testing.Extensions.AzureDevOps + Microsoft.Testing.Extensions.AzureDevOps.TestingPlatformBuilderHook + + + diff --git a/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/buildTransitive/Microsoft.Testing.Extensions.AzureDevOps.props b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/buildTransitive/Microsoft.Testing.Extensions.AzureDevOps.props new file mode 100644 index 0000000000..57f9f713e2 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Extensions.AzureDevOps/buildTransitive/Microsoft.Testing.Extensions.AzureDevOps.props @@ -0,0 +1,3 @@ + + + diff --git a/src/Platform/Microsoft.Testing.Platform/Helpers/StackTraceHelper.cs b/src/Platform/Microsoft.Testing.Platform/Helpers/StackTraceHelper.cs new file mode 100644 index 0000000000..c36c6a0af7 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Platform/Helpers/StackTraceHelper.cs @@ -0,0 +1,64 @@ +// 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.Platform.Helpers; + +internal static partial class StackTraceHelper +{ +#if NET7_0_OR_GREATER + // Specifying no timeout, the regex is linear. And the timeout does not measure the regex only, but measures also any + // thread suspends, so the regex gets blamed incorrectly. + [GeneratedRegex(@"^ at ((?.+) in (?.+):line (?\d+)|(?.+))$", RegexOptions.ExplicitCapture)] + public static partial Regex GetFrameRegex(); +#else + private static Regex? s_regex; + + [MemberNotNull(nameof(s_regex))] + public static Regex GetFrameRegex() + { + if (s_regex != null) + { + return s_regex; + } + + string atResourceName = "Word_At"; + string inResourceName = "StackTrace_InFileLineNumber"; + + string? atString = null; + string? inString = null; + + // Grab words from localized resource, in case the stack trace is localized. + try + { + // Get these resources: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +#pragma warning disable RS0030 // Do not use banned APIs + MethodInfo? getResourceStringMethod = typeof(Environment).GetMethod( + "GetResourceString", + BindingFlags.Static | BindingFlags.NonPublic, null, [typeof(string)], null); +#pragma warning restore RS0030 // Do not use banned APIs + if (getResourceStringMethod is not null) + { + // at + atString = (string?)getResourceStringMethod.Invoke(null, [atResourceName]); + + // in {0}:line {1} + inString = (string?)getResourceStringMethod.Invoke(null, [inResourceName]); + } + } + catch + { + // If we fail, populate the defaults below. + } + + atString = atString == null || atString == atResourceName ? "at" : atString; + inString = inString == null || inString == inResourceName ? "in {0}:line {1}" : inString; + + string inPattern = string.Format(CultureInfo.InvariantCulture, inString, "(?.+)", @"(?\d+)"); + + // Specifying no timeout, the regex is linear. And the timeout does not measure the regex only, but measures also any + // thread suspends, so the regex gets blamed incorrectly. + s_regex = new Regex(@$"^ {atString} ((?.+) {inPattern}|(?.+))$", RegexOptions.Compiled | RegexOptions.ExplicitCapture); + return s_regex; + } +#endif +} diff --git a/src/Platform/Microsoft.Testing.Platform/Microsoft.Testing.Platform.csproj b/src/Platform/Microsoft.Testing.Platform/Microsoft.Testing.Platform.csproj index f9a9f82f32..13ce520dbb 100644 --- a/src/Platform/Microsoft.Testing.Platform/Microsoft.Testing.Platform.csproj +++ b/src/Platform/Microsoft.Testing.Platform/Microsoft.Testing.Platform.csproj @@ -37,6 +37,7 @@ This package provides the core platform and the .NET implementation of the proto + diff --git a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs index 56412e2dda..273aa97448 100644 --- a/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs +++ b/src/Platform/Microsoft.Testing.Platform/OutputDevice/Terminal/TerminalTestReporter.cs @@ -55,63 +55,6 @@ internal event EventHandler OnProgressStopUpdate private bool? _shouldShowPassedTests; -#if NET7_0_OR_GREATER - // Specifying no timeout, the regex is linear. And the timeout does not measure the regex only, but measures also any - // thread suspends, so the regex gets blamed incorrectly. - [GeneratedRegex(@"^ at ((?.+) in (?.+):line (?\d+)|(?.+))$", RegexOptions.ExplicitCapture)] - private static partial Regex GetFrameRegex(); -#else - private static Regex? s_regex; - - [MemberNotNull(nameof(s_regex))] - private static Regex GetFrameRegex() - { - if (s_regex != null) - { - return s_regex; - } - - string atResourceName = "Word_At"; - string inResourceName = "StackTrace_InFileLineNumber"; - - string? atString = null; - string? inString = null; - - // Grab words from localized resource, in case the stack trace is localized. - try - { - // Get these resources: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx -#pragma warning disable RS0030 // Do not use banned APIs - MethodInfo? getResourceStringMethod = typeof(Environment).GetMethod( - "GetResourceString", - BindingFlags.Static | BindingFlags.NonPublic, null, [typeof(string)], null); -#pragma warning restore RS0030 // Do not use banned APIs - if (getResourceStringMethod is not null) - { - // at - atString = (string?)getResourceStringMethod.Invoke(null, [atResourceName]); - - // in {0}:line {1} - inString = (string?)getResourceStringMethod.Invoke(null, [inResourceName]); - } - } - catch - { - // If we fail, populate the defaults below. - } - - atString = atString == null || atString == atResourceName ? "at" : atString; - inString = inString == null || inString == inResourceName ? "in {0}:line {1}" : inString; - - string inPattern = string.Format(CultureInfo.InvariantCulture, inString, "(?.+)", @"(?\d+)"); - - // Specifying no timeout, the regex is linear. And the timeout does not measure the regex only, but measures also any - // thread suspends, so the regex gets blamed incorrectly. - s_regex = new Regex(@$"^ {atString} ((?.+) {inPattern}|(?.+))$", RegexOptions.Compiled | RegexOptions.ExplicitCapture); - return s_regex; - } -#endif - private int _counter; /// @@ -667,7 +610,7 @@ private static void AppendAssemblyLinkTargetFrameworkAndArchitecture(ITerminal t internal /* for testing */ static void AppendStackFrame(ITerminal terminal, string stackTraceLine) { terminal.Append(DoubleIndentation); - Match match = GetFrameRegex().Match(stackTraceLine); + Match match = StackTraceHelper.GetFrameRegex().Match(stackTraceLine); if (match.Success) { bool weHaveFilePathAndCodeLine = !RoslynString.IsNullOrWhiteSpace(match.Groups["code"].Value);