-
Notifications
You must be signed in to change notification settings - Fork 300
Expand file tree
/
Copy pathAzureDevOpsLogIssueFormatter.cs
More file actions
101 lines (87 loc) · 4.24 KB
/
Copy pathAzureDevOpsLogIssueFormatter.cs
File metadata and controls
101 lines (87 loc) · 4.24 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
// 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.Helpers;
namespace Microsoft.Testing.Platform.OutputDevice;
/// <summary>
/// Helpers for translating output-device messages into Azure Pipelines logging commands
/// (<see href="https://learn.microsoft.com/azure/devops/pipelines/scripts/logging-commands">##vso[...]</see>)
/// so that errors and warnings surface on the Azure DevOps build / pipeline summary instead of
/// being just colored text in the agent log.
/// See https://github.com/microsoft/testfx/issues/5979.
/// </summary>
internal static class AzureDevOpsLogIssueFormatter
{
internal const string SeverityError = "error";
internal const string SeverityWarning = "warning";
// Opt-out: setting TESTINGPLATFORM_AZDO_OUTPUT to one of these disables automatic
// ##vso[task.logissue] emission even when TF_BUILD=true.
private const string OptOutEnvironmentVariableName = "TESTINGPLATFORM_AZDO_OUTPUT";
/// <summary>
/// Returns <c>true</c> when the current process is running on an Azure DevOps agent
/// (<c>TF_BUILD=true</c>), regardless of the <c>TESTINGPLATFORM_AZDO_OUTPUT</c> opt-out. Use this
/// for the AzureDevOpsReport extension's explicit <c>--report-azdo</c> output (the user opted in via
/// the option), and reserve <see cref="IsAzureDevOpsEnvironment"/> for the platform's automatic
/// <c>##vso[task.logissue]</c> emission, which the opt-out disables.
/// </summary>
public static bool IsAzureDevOpsAgent(IEnvironment environment)
=> bool.TryParse(environment.GetEnvironmentVariable("TF_BUILD"), out bool tfBuild) && tfBuild;
/// <summary>
/// Returns <c>true</c> when the current process is running on an Azure DevOps agent
/// (TF_BUILD=true) and the user has not opted out via <c>TESTINGPLATFORM_AZDO_OUTPUT=off|false|0</c>.
/// </summary>
public static bool IsAzureDevOpsEnvironment(IEnvironment environment)
{
if (!IsAzureDevOpsAgent(environment))
{
return false;
}
string? optOut = environment.GetEnvironmentVariable(OptOutEnvironmentVariableName);
return RoslynString.IsNullOrEmpty(optOut) || !IsOffValue(optOut);
}
/// <summary>
/// Formats a message as a <c>##vso[task.logissue type=<severity>]<message></c> line
/// with the standard Azure Pipelines escaping rules applied to the message body.
/// </summary>
public static string FormatLogIssue(string severity, string message)
=> $"##vso[task.logissue type={severity}]{Escape(message)}";
/// <summary>
/// Escapes a value for inclusion in the message body of an Azure Pipelines logging command
/// per the official escaping rules
/// (<see href="https://learn.microsoft.com/azure/devops/pipelines/scripts/logging-commands#formatting-commands">formatting commands</see>):
/// <c>%</c> -> <c>%25</c>, <c>;</c> -> <c>%3B</c>, <c>\r</c> -> <c>%0D</c>, <c>\n</c> -> <c>%0A</c>, <c>]</c> -> <c>%5D</c>.
/// <c>%</c> must be escaped first so the other replacements don't get double-encoded.
/// </summary>
internal static string Escape(string value)
{
if (RoslynString.IsNullOrEmpty(value))
{
return value;
}
StringBuilder? builder = null;
for (int i = 0; i < value.Length; i++)
{
char c = value[i];
string? replacement = c switch
{
'%' => "%25",
';' => "%3B",
'\r' => "%0D",
'\n' => "%0A",
']' => "%5D",
_ => null,
};
if (replacement is null)
{
builder?.Append(c);
continue;
}
builder ??= new StringBuilder(value.Length + 8).Append(value, 0, i);
builder.Append(replacement);
}
return builder?.ToString() ?? value;
}
private static bool IsOffValue(string value)
=> value.Equals("off", StringComparison.OrdinalIgnoreCase)
|| value.Equals("false", StringComparison.OrdinalIgnoreCase)
|| value.Equals("0", StringComparison.Ordinal);
}