Skip to content

Commit 5414878

Browse files
committed
Extract to extension
1 parent cb1502a commit 5414878

33 files changed

+1084
-164
lines changed

TestFx.sln

+7
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MSTest.Engine.UnitTests", "
222222
EndProject
223223
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MSTest.SourceGeneration.UnitTests", "test\UnitTests\MSTest.SourceGeneration.UnitTests\MSTest.SourceGeneration.UnitTests.csproj", "{E6C0466E-BE8D-C04F-149A-FD98438F1413}"
224224
EndProject
225+
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}"
226+
EndProject
225227
Global
226228
GlobalSection(SolutionConfigurationPlatforms) = preSolution
227229
Debug|Any CPU = Debug|Any CPU
@@ -524,6 +526,10 @@ Global
524526
{E6C0466E-BE8D-C04F-149A-FD98438F1413}.Debug|Any CPU.Build.0 = Debug|Any CPU
525527
{E6C0466E-BE8D-C04F-149A-FD98438F1413}.Release|Any CPU.ActiveCfg = Release|Any CPU
526528
{E6C0466E-BE8D-C04F-149A-FD98438F1413}.Release|Any CPU.Build.0 = Release|Any CPU
529+
{F608D3A3-125B-CD88-1D51-8714ED142029}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
530+
{F608D3A3-125B-CD88-1D51-8714ED142029}.Debug|Any CPU.Build.0 = Debug|Any CPU
531+
{F608D3A3-125B-CD88-1D51-8714ED142029}.Release|Any CPU.ActiveCfg = Release|Any CPU
532+
{F608D3A3-125B-CD88-1D51-8714ED142029}.Release|Any CPU.Build.0 = Release|Any CPU
527533
EndGlobalSection
528534
GlobalSection(SolutionProperties) = preSolution
529535
HideSolutionNode = FALSE
@@ -615,6 +621,7 @@ Global
615621
{7BA0E74E-798E-4399-2EDE-A23BD5DA78CA} = {E7F15C9C-3928-47AD-8462-64FD29FFCA54}
616622
{2C0DFAC0-5D58-D172-ECE4-CBB78AD03435} = {BB874DF1-44FE-415A-B634-A6B829107890}
617623
{E6C0466E-BE8D-C04F-149A-FD98438F1413} = {BB874DF1-44FE-415A-B634-A6B829107890}
624+
{F608D3A3-125B-CD88-1D51-8714ED142029} = {6AEE1440-FDF0-4729-8196-B24D0E333550}
618625
EndGlobalSection
619626
GlobalSection(ExtensibilityGlobals) = postSolution
620627
SolutionGuid = {31E0F4D5-975A-41CC-933E-545B2201FAF9}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using Microsoft.Testing.Platform;
5+
6+
namespace Microsoft.Testing.Extensions.TrxReport.Abstractions;
7+
8+
internal static class AzDoEscaper
9+
{
10+
public static string Escape(string value)
11+
{
12+
if (RoslynString.IsNullOrEmpty(value))
13+
{
14+
return value;
15+
}
16+
17+
var result = new StringBuilder(value.Length);
18+
foreach (char c in value)
19+
{
20+
switch (c)
21+
{
22+
case ';':
23+
result.Append("%3B");
24+
break;
25+
case '\r':
26+
result.Append("%0D");
27+
break;
28+
case '\n':
29+
result.Append("%0A");
30+
break;
31+
default:
32+
result.Append(c);
33+
break;
34+
}
35+
}
36+
37+
return result.ToString();
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
namespace Microsoft.Testing.Extensions.Reporting;
5+
6+
internal static class AzureDevOpsCommandLineOptions
7+
{
8+
public const string AzureDevOpsOptionName = "report-azdo";
9+
public const string AzureDevOpsReportSeverity = "report-azdo-severity";
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using Microsoft.Testing.Extensions.Reporting.Resources;
5+
using Microsoft.Testing.Platform.CommandLine;
6+
using Microsoft.Testing.Platform.Extensions;
7+
using Microsoft.Testing.Platform.Extensions.CommandLine;
8+
using Microsoft.Testing.Platform.Helpers;
9+
10+
namespace Microsoft.Testing.Extensions.Reporting;
11+
12+
internal sealed class AzureDevOpsCommandLineProvider : ICommandLineOptionsProvider
13+
{
14+
private static readonly string[] SeverityOptions = ["error", "warning"];
15+
16+
public string Uid => nameof(AzureDevOpsCommandLineProvider);
17+
18+
public string Version => AppVersion.DefaultSemVer;
19+
20+
public string DisplayName => AzureDevOpsResources.DisplayName;
21+
22+
public string Description => AzureDevOpsResources.Description;
23+
24+
public Task<bool> IsEnabledAsync() => Task.FromResult(true);
25+
26+
public IReadOnlyCollection<CommandLineOption> GetCommandLineOptions()
27+
=>
28+
[
29+
new CommandLineOption(AzureDevOpsCommandLineOptions.AzureDevOpsOptionName, AzureDevOpsResources.OptionDescription, ArgumentArity.Zero, false),
30+
new CommandLineOption(AzureDevOpsCommandLineOptions.AzureDevOpsReportSeverity, AzureDevOpsResources.SeverityOptionDescription, ArgumentArity.ExactlyOne, false),
31+
];
32+
33+
public Task<ValidationResult> ValidateOptionArgumentsAsync(CommandLineOption commandOption, string[] arguments)
34+
{
35+
if (commandOption.Name == AzureDevOpsCommandLineOptions.AzureDevOpsReportSeverity)
36+
{
37+
if (!SeverityOptions.Contains(arguments[0], StringComparer.OrdinalIgnoreCase))
38+
{
39+
return ValidationResult.InvalidTask(string.Format(CultureInfo.InvariantCulture, AzureDevOpsResources.InvalidSeverity, arguments[0]));
40+
}
41+
}
42+
43+
return ValidationResult.ValidTask;
44+
}
45+
46+
public Task<ValidationResult> ValidateCommandLineOptionsAsync(ICommandLineOptions commandLineOptions)
47+
=> ValidationResult.ValidTask;
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using Microsoft.Testing.Extensions.Reporting;
5+
using Microsoft.Testing.Extensions.TrxReport.Abstractions;
6+
using Microsoft.Testing.Platform.Builder;
7+
using Microsoft.Testing.Platform.Extensions;
8+
using Microsoft.Testing.Platform.Services;
9+
10+
namespace Microsoft.Testing.Extensions;
11+
12+
/// <summary>
13+
/// Provides extension methods for adding Azure DevOps reporting support to the test application builder.
14+
/// </summary>
15+
public static class AzureDevOpsExtensions
16+
{
17+
/// <summary>
18+
/// Adds support to the test application builder.
19+
/// </summary>
20+
/// <param name="builder">The test application builder.</param>
21+
public static void AddAzureDevOpsProvider(this ITestApplicationBuilder builder)
22+
{
23+
var compositeTestSessionAzDoService =
24+
new CompositeExtensionFactory<AzureDevOpsReporter>(serviceProvider =>
25+
new AzureDevOpsReporter(
26+
serviceProvider.GetCommandLineOptions(),
27+
serviceProvider.GetEnvironment(),
28+
serviceProvider.GetOutputDevice()));
29+
30+
builder.TestHost.AddDataConsumer(compositeTestSessionAzDoService);
31+
32+
builder.CommandLine.AddProvider(() => new AzureDevOpsCommandLineProvider());
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using Microsoft.Testing.Extensions.Reporting;
5+
using Microsoft.Testing.Extensions.Reporting.Resources;
6+
using Microsoft.Testing.Platform;
7+
using Microsoft.Testing.Platform.CommandLine;
8+
using Microsoft.Testing.Platform.Extensions.Messages;
9+
using Microsoft.Testing.Platform.Extensions.OutputDevice;
10+
using Microsoft.Testing.Platform.Extensions.TestHost;
11+
using Microsoft.Testing.Platform.Helpers;
12+
using Microsoft.Testing.Platform.OutputDevice;
13+
14+
namespace Microsoft.Testing.Extensions.TrxReport.Abstractions;
15+
16+
internal sealed class AzureDevOpsReporter :
17+
IDataConsumer,
18+
IDataProducer,
19+
IOutputDeviceDataProducer
20+
{
21+
private readonly IOutputDevice _outputDisplay;
22+
23+
private static readonly char[] NewlineCharacters = new char[] { '\r', '\n' };
24+
private readonly ICommandLineOptions _commandLine;
25+
private readonly IEnvironment _environment;
26+
private string _severity = "error";
27+
28+
public AzureDevOpsReporter(
29+
ICommandLineOptions commandLine,
30+
IEnvironment environment,
31+
IOutputDevice outputDisplay)
32+
{
33+
_commandLine = commandLine;
34+
_environment = environment;
35+
_outputDisplay = outputDisplay;
36+
}
37+
38+
public Type[] DataTypesConsumed { get; } =
39+
[
40+
typeof(TestNodeUpdateMessage)
41+
];
42+
43+
public Type[] DataTypesProduced { get; } = [typeof(SessionFileArtifact)];
44+
45+
/// <inheritdoc />
46+
public string Uid { get; } = nameof(AzureDevOpsReporter);
47+
48+
/// <inheritdoc />
49+
public string Version { get; } = AppVersion.DefaultSemVer;
50+
51+
/// <inheritdoc />
52+
public string DisplayName { get; } = AzureDevOpsResources.DisplayName;
53+
54+
/// <inheritdoc />
55+
public string Description { get; } = AzureDevOpsResources.Description;
56+
57+
/// <inheritdoc />
58+
public Task<bool> IsEnabledAsync()
59+
{
60+
bool isEnabled = _commandLine.IsOptionSet(AzureDevOpsCommandLineOptions.AzureDevOpsOptionName)
61+
&& string.Equals(_environment.GetEnvironmentVariable("TF_BUILD"), "true", StringComparison.OrdinalIgnoreCase);
62+
63+
if (isEnabled)
64+
{
65+
bool found = _commandLine.TryGetOptionArgumentList(AzureDevOpsCommandLineOptions.AzureDevOpsReportSeverity, out string[]? arguments);
66+
if (found && arguments?.Length > 0)
67+
{
68+
if (string.Equals(arguments[0], "warning", StringComparison.OrdinalIgnoreCase))
69+
{
70+
_severity = "warning";
71+
}
72+
}
73+
}
74+
75+
return Task.FromResult(isEnabled);
76+
}
77+
78+
public async Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken)
79+
{
80+
if (cancellationToken.IsCancellationRequested)
81+
{
82+
return;
83+
}
84+
85+
try
86+
{
87+
if (value is not TestNodeUpdateMessage nodeUpdateMessage)
88+
{
89+
return;
90+
}
91+
92+
TestNodeStateProperty nodeState = nodeUpdateMessage.TestNode.Properties.Single<TestNodeStateProperty>();
93+
94+
switch (nodeState)
95+
{
96+
case FailedTestNodeStateProperty failed:
97+
await WriteExceptionAsync(failed.Explanation, failed.Exception);
98+
break;
99+
case ErrorTestNodeStateProperty error:
100+
await WriteExceptionAsync(error.Explanation, error.Exception);
101+
break;
102+
case CancelledTestNodeStateProperty cancelled:
103+
await WriteExceptionAsync(cancelled.Explanation, cancelled.Exception);
104+
break;
105+
case TimeoutTestNodeStateProperty timeout:
106+
await WriteExceptionAsync(timeout.Explanation, timeout.Exception);
107+
break;
108+
}
109+
110+
return;
111+
}
112+
catch (OperationCanceledException ex) when (ex.CancellationToken == cancellationToken)
113+
{
114+
// Do nothing, we're stopping
115+
}
116+
117+
return;
118+
}
119+
120+
private async Task WriteExceptionAsync(string? explanation, Exception? exception)
121+
{
122+
if (exception == null || exception.StackTrace == null)
123+
{
124+
return;
125+
}
126+
127+
string message = explanation ?? exception.Message;
128+
129+
if (message == null)
130+
{
131+
return;
132+
}
133+
134+
string stackTrace = exception.StackTrace;
135+
int index = stackTrace.IndexOfAny(NewlineCharacters);
136+
string firstLine = index == -1 ? stackTrace : stackTrace.Substring(0, index);
137+
if (firstLine != null)
138+
{
139+
(string Code, string File, int LineNumber)? location = GetStackFrameLocation(firstLine);
140+
if (location != null)
141+
{
142+
string root = RootFinder.Find();
143+
string file = location.Value.File;
144+
string relativePath = file.StartsWith(root, StringComparison.CurrentCultureIgnoreCase) ? file.Substring(root.Length) : file;
145+
146+
string err = AzDoEscaper.Escape(message);
147+
148+
string line = $"##vso[task.logissue type={_severity};sourcepath={relativePath};linenumber={location.Value.LineNumber};columnnumber=1]{err}";
149+
await _outputDisplay.DisplayAsync(this, new FormattedTextOutputDeviceData(line));
150+
}
151+
}
152+
}
153+
154+
internal /* for testing */ static (string Code, string File, int LineNumber)? GetStackFrameLocation(string stackTraceLine)
155+
{
156+
Match match = StackTraceHelper.GetFrameRegex().Match(stackTraceLine);
157+
if (!match.Success)
158+
{
159+
return null;
160+
}
161+
162+
bool weHaveFilePathAndCodeLine = !RoslynString.IsNullOrWhiteSpace(match.Groups["code"].Value);
163+
if (!weHaveFilePathAndCodeLine)
164+
{
165+
return null;
166+
}
167+
168+
if (RoslynString.IsNullOrWhiteSpace(match.Groups["file"].Value))
169+
{
170+
return null;
171+
}
172+
173+
int line = int.TryParse(match.Groups["line"].Value, out int value) ? value : 0;
174+
175+
return (match.Groups["code"].Value, match.Groups["file"].Value, line);
176+
}
177+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
T:System.ArgumentNullException; Use 'ArgumentGuard' instead
2+
P:System.DateTime.Now; Use 'IClock' instead
3+
P:System.DateTime.UtcNow; Use 'IClock' instead
4+
M:System.Threading.Tasks.Task.Run(System.Action); Use 'ITask' instead
5+
M:System.Threading.Tasks.Task.WhenAll(System.Threading.Tasks.Task[]); Use 'ITask' instead
6+
M:System.Threading.Tasks.Task.WhenAll(System.Collections.Generic.IEnumerable{System.Threading.Tasks.Task}); Use 'ITask' instead
7+
M:System.String.IsNullOrEmpty(System.String); Use 'RoslynString.IsNullOrEmpty' instead
8+
M:System.String.IsNullOrWhiteSpace(System.String); Use 'RoslynString.IsNullOrWhiteSpace' instead
9+
M:System.Diagnostics.Debug.Assert(System.Boolean); Use 'RoslynDebug.Assert' instead
10+
M:System.Diagnostics.Debug.Assert(System.Boolean,System.String); Use 'RoslynDebug.Assert' instead

0 commit comments

Comments
 (0)