-
-
Notifications
You must be signed in to change notification settings - Fork 69
/
Copy pathGitHubReporter.cs
241 lines (190 loc) · 8.81 KB
/
GitHubReporter.cs
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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
using System.Collections.Concurrent;
using System.Reflection;
using System.Text;
using Microsoft.Testing.Platform.Extensions;
using Microsoft.Testing.Platform.Extensions.Messages;
using Microsoft.Testing.Platform.Extensions.TestHost;
using TUnit.Engine.Framework;
namespace TUnit.Engine.Reporters;
public class GitHubReporter(IExtension extension) : IDataConsumer, ITestApplicationLifecycleCallbacks, IFilterReceiver
{
private const long MaxFileSizeInBytes = 1 * 1024 * 1024; // 1MB
private string _outputSummaryFilePath = null!;
public async Task<bool> IsEnabledAsync()
{
if (Environment.GetEnvironmentVariable("DISABLE_GITHUB_REPORTER") is not null)
{
return false;
}
if (Environment.GetEnvironmentVariable("GITHUB_ACTIONS") is null)
{
return false;
}
if (Environment.GetEnvironmentVariable("GITHUB_STEP_SUMMARY") is not { } fileName
|| !File.Exists(fileName))
{
return false;
}
_outputSummaryFilePath = fileName;
return await extension.IsEnabledAsync();
}
public string Uid { get; } = $"{extension.Uid}GitHubReporter";
public string Version => extension.Version;
public string DisplayName => extension.DisplayName;
public string Description => extension.Description;
private readonly ConcurrentDictionary<string, List<TestNodeUpdateMessage>> _updates = [];
public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken)
{
var testNodeUpdateMessage = (TestNodeUpdateMessage)value;
_updates.GetOrAdd(testNodeUpdateMessage.TestNode.Uid.Value, []).Add(testNodeUpdateMessage);
return Task.CompletedTask;
}
public Type[] DataTypesConsumed { get; } = [typeof(TestNodeUpdateMessage)];
public Task BeforeRunAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public Task AfterRunAsync(int exitCode, CancellationToken cancellation)
{
if (_updates.Count is 0)
{
return Task.CompletedTask;
}
var last = _updates.ToDictionary(x => x.Key, x => x.Value.Last());
var passedCount = last.Count(x =>
x.Value.TestNode.Properties.AsEnumerable().Any(p => p is PassedTestNodeStateProperty));
var failed = last.Where(x =>
x.Value.TestNode.Properties.AsEnumerable()
.Any(p => p is FailedTestNodeStateProperty or ErrorTestNodeStateProperty)).ToArray();
var cancelled = last.Where(x =>
x.Value.TestNode.Properties.AsEnumerable().Any(p => p is CancelledTestNodeStateProperty)).ToArray();
var timeout = last
.Where(x => x.Value.TestNode.Properties.AsEnumerable().Any(p => p is TimeoutTestNodeStateProperty))
.ToArray();
var skipped = last
.Where(x => x.Value.TestNode.Properties.AsEnumerable().Any(p => p is SkippedTestNodeStateProperty))
.ToArray();
var inProgress = last.Where(x =>
x.Value.TestNode.Properties.AsEnumerable().Any(p => p is InProgressTestNodeStateProperty)).ToArray();
var stringBuilder = new StringBuilder();
stringBuilder.AppendLine($"### {Assembly.GetEntryAssembly()?.GetName().Name}");
if (!string.IsNullOrEmpty(Filter))
{
stringBuilder.AppendLine($"#### Filter: `{Filter}`");
}
stringBuilder.AppendLine();
stringBuilder.AppendLine("| Test Count | Status |");
stringBuilder.AppendLine("| --- | --- |");
stringBuilder.AppendLine($"| {passedCount} | Passed |");
stringBuilder.AppendLine($"| {failed.Length} | Failed |");
if(skipped.Length > 0)
{
stringBuilder.AppendLine($"| {skipped.Length} | Skipped |");
}
if(timeout.Length > 0)
{
stringBuilder.AppendLine($"| {timeout.Length} | Timed Out |");
}
if(cancelled.Length > 0)
{
stringBuilder.AppendLine($"| {cancelled.Length} | Cancelled |");
}
if(inProgress.Length > 0)
{
stringBuilder.AppendLine($"| {inProgress.Length} | In Progress (never completed) |");
}
if (passedCount == last.Count)
{
return WriteFile(stringBuilder.ToString());
}
stringBuilder.AppendLine();
stringBuilder.AppendLine();
stringBuilder.AppendLine("### Details");
stringBuilder.AppendLine();
stringBuilder.AppendLine("| Test | Status | Details | Duration |");
stringBuilder.AppendLine("| --- | --- | --- | --- |");
foreach (var testNodeUpdateMessage in last.Values)
{
var name = testNodeUpdateMessage.TestNode.DisplayName;
var stateProperty = testNodeUpdateMessage.TestNode.Properties.OfType<TestNodeStateProperty>().FirstOrDefault();
if (stateProperty is PassedTestNodeStateProperty)
{
continue;
}
var status = GetStatus(stateProperty);
var details = GetDetails(stateProperty, testNodeUpdateMessage.TestNode.Properties).Replace("\n", " <br> ");
var timingProperty = testNodeUpdateMessage.TestNode.Properties.AsEnumerable().OfType<TimingProperty>().FirstOrDefault();
var duration = timingProperty?.GlobalTiming.Duration;
stringBuilder.AppendLine($"| {name} | {status} | {details} | {duration} |");
}
return WriteFile(stringBuilder.ToString());
}
private Task WriteFile(string contents)
{
var fileInfo = new FileInfo(_outputSummaryFilePath);
var currentFileSize = fileInfo.Exists ? fileInfo.Length : 0;
long newContentSize = Encoding.UTF8.GetByteCount(contents);
var newSize = currentFileSize + newContentSize;
if (newSize > MaxFileSizeInBytes)
{
Console.WriteLine("Appending to the GitHub Step Summary would exceed the 1MB file size limit.");
return Task.CompletedTask;
}
#if NET
return File.AppendAllTextAsync(_outputSummaryFilePath, contents, Encoding.UTF8);
#else
File.AppendAllText(_outputSummaryFilePath, contents, Encoding.UTF8);
return Task.CompletedTask;
#endif
}
private string GetDetails(TestNodeStateProperty? stateProperty, PropertyBag properties)
{
if (stateProperty is FailedTestNodeStateProperty
or ErrorTestNodeStateProperty
or TimeoutTestNodeStateProperty
or CancelledTestNodeStateProperty)
{
return $"<pre>{GetError(stateProperty)}</pre>";
}
if (stateProperty is SkippedTestNodeStateProperty skippedTestNodeStateProperty)
{
return skippedTestNodeStateProperty.Explanation ?? "Skipped (No reason provided)";
}
if (stateProperty is InProgressTestNodeStateProperty)
{
var timingProperty = properties.AsEnumerable().OfType<TimingProperty>().FirstOrDefault();
var start = timingProperty?.GlobalTiming.StartTime;
var end = timingProperty?.GlobalTiming.EndTime;
return $"Start: {start} | End: {end}";
}
return "Unknown Test State";
}
private string? GetError(TestNodeStateProperty? stateProperty)
{
return stateProperty switch
{
ErrorTestNodeStateProperty errorTestNodeStateProperty => errorTestNodeStateProperty.Exception?.ToString() ??
errorTestNodeStateProperty.Explanation,
FailedTestNodeStateProperty failedTestNodeStateProperty =>
failedTestNodeStateProperty.Exception?.ToString() ?? failedTestNodeStateProperty.Explanation,
TimeoutTestNodeStateProperty timeoutTestNodeStateProperty => timeoutTestNodeStateProperty.Exception
?.ToString() ?? timeoutTestNodeStateProperty.Explanation,
CancelledTestNodeStateProperty cancelledTestNodeStateProperty => cancelledTestNodeStateProperty.Exception?.ToString() ?? cancelledTestNodeStateProperty.Explanation,
_ => null
};
}
private static string GetStatus(TestNodeStateProperty? stateProperty)
{
return stateProperty switch
{
CancelledTestNodeStateProperty => "Cancelled",
ErrorTestNodeStateProperty or FailedTestNodeStateProperty => "Failed",
InProgressTestNodeStateProperty => "In Progress (never finished)",
PassedTestNodeStateProperty => "Passed",
SkippedTestNodeStateProperty => "Skipped",
TimeoutTestNodeStateProperty => "Timed Out",
_ => "Unknown"
};
}
public string? Filter { get; set; }
}