-
Notifications
You must be signed in to change notification settings - Fork 300
Expand file tree
/
Copy pathAzureDevOpsLogGroupReporter.cs
More file actions
131 lines (113 loc) · 5.49 KB
/
Copy pathAzureDevOpsLogGroupReporter.cs
File metadata and controls
131 lines (113 loc) · 5.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using Microsoft.Testing.Extensions.AzureDevOpsReport.Resources;
using Microsoft.Testing.Extensions.Reporting;
using Microsoft.Testing.Platform.CommandLine;
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;
/// <summary>
/// Wraps each test assembly's output in an Azure DevOps log group (<c>##[group]</c> /
/// <c>##[endgroup]</c>) so the raw pipeline log view collapses the assembly's test output by
/// default. Unlike <c>##vso[task.logdetail]</c> records, the <c>##[group]</c> format commands are
/// rendered by the modern Azure DevOps Pipelines log viewer.
/// </summary>
/// <remarks>
/// This handler also implements <see cref="IDataConsumer"/> (with a no-op
/// <see cref="ConsumeAsync(IDataProducer, IData, CancellationToken)"/>) purely so that, at session
/// end, <c>CommonTestHost.NotifyTestSessionEndAsync</c> runs its
/// <see cref="OnTestSessionFinishingAsync(ITestSessionContext)"/> in the consumer phase — i.e.
/// after the producer-only AzDO handlers. Combined with registering it last, this ensures the
/// closing <c>##[endgroup]</c> is emitted after the other reporters' final <c>##vso[...]</c> lines,
/// so the group truly wraps the whole assembly's output.
/// </remarks>
internal sealed class AzureDevOpsLogGroupReporter : IDataConsumer, ITestSessionLifetimeHandler, IOutputDeviceDataProducer
{
private readonly ICommandLineOptions _commandLineOptions;
private readonly IEnvironment _environment;
private readonly IOutputDevice _outputDevice;
private readonly ITestApplicationModuleInfo _testApplicationModuleInfo;
private readonly ILogger _logger;
private readonly Lazy<string> _targetFrameworkMoniker;
private bool _groupOpened;
public AzureDevOpsLogGroupReporter(
ICommandLineOptions commandLineOptions,
IEnvironment environment,
IOutputDevice outputDevice,
ITestApplicationModuleInfo testApplicationModuleInfo,
ILoggerFactory loggerFactory)
{
_commandLineOptions = commandLineOptions;
_environment = environment;
_outputDevice = outputDevice;
_testApplicationModuleInfo = testApplicationModuleInfo;
_logger = loggerFactory.CreateLogger<AzureDevOpsLogGroupReporter>();
_targetFrameworkMoniker = new(TargetFrameworkMonikerHelper.GetTargetFrameworkMoniker);
}
public string Uid => nameof(AzureDevOpsLogGroupReporter);
public string Version => ExtensionVersion.DefaultSemVer;
public string DisplayName => AzureDevOpsResources.DisplayName;
public string Description => AzureDevOpsResources.Description;
public Type[] DataTypesConsumed { get; } = [typeof(TestNodeUpdateMessage)];
public Task<bool> IsEnabledAsync()
=> Task.FromResult(
_commandLineOptions.IsOptionSet(AzureDevOpsCommandLineOptions.AzureDevOpsOptionName)
&& AzureDevOpsConstants.IsRunningInAzureDevOps(_environment));
// No-op: this consumer subscribes to data only to be ordered in the consumer phase at session
// end (see the type-level remarks). It does not act on individual messages.
public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken)
=> Task.CompletedTask;
public async Task OnTestSessionStartingAsync(ITestSessionContext testSessionContext)
{
try
{
testSessionContext.CancellationToken.ThrowIfCancellationRequested();
string name = $"{_testApplicationModuleInfo.TryGetAssemblyName() ?? "unknown"} ({_targetFrameworkMoniker.Value})";
string line = $"##[group]{AzDoEscaper.Escape(string.Format(CultureInfo.InvariantCulture, AzureDevOpsResources.LogGroupHeader, name))}";
await _outputDevice.DisplayAsync(this, new AzureDevOpsCommandOutputDeviceData(line), testSessionContext.CancellationToken).ConfigureAwait(false);
_groupOpened = true;
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
LogUnexpectedException(nameof(OnTestSessionStartingAsync), ex);
}
}
public async Task OnTestSessionFinishingAsync(ITestSessionContext testSessionContext)
{
try
{
testSessionContext.CancellationToken.ThrowIfCancellationRequested();
if (!_groupOpened)
{
return;
}
await _outputDevice.DisplayAsync(this, new AzureDevOpsCommandOutputDeviceData("##[endgroup]"), testSessionContext.CancellationToken).ConfigureAwait(false);
_groupOpened = false;
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
LogUnexpectedException(nameof(OnTestSessionFinishingAsync), ex);
}
}
private void LogUnexpectedException(string callbackName, Exception ex)
{
if (_logger.IsEnabled(LogLevel.Warning))
{
_logger.LogWarning($"Unexpected exception in {callbackName}: {ex}");
}
}
}