返回提交历史
Modified
README.md
+1
-1
Modified
README.zh-CN.md
+1
-1
Modified
XFEExtension.NetCore.XUnit.Analyzer/Diagnostics/XUnitCodeAnalyzer.cs
+1
-1
Modified
XFEExtension.NetCore.XUnit.Analyzer/Generator/XUnitCodeGenerator.cs
+15
-10
Modified
XFEExtension.NetCore.XUnit.Test/LegacyCompatibilityTests.cs
+6
-1
Modified
XFEExtension.NetCore.XUnit.Test/Program.cs
+58
-0
Modified
XFEExtension.NetCore.XUnit/Attributes/LegacyAttributes.cs
+9
-9
Modified
XFEExtension.NetCore.XUnit/Execution/AsyncLocalConsoleCapture.cs
+48
-15
Modified
XFEExtension.NetCore.XUnit/Execution/TestExecutor.cs
+11
-4
Modified
XFEExtension.NetCore.XUnit/Execution/WorkerProcess.cs
+12
-2
Modified
XFEExtension.NetCore.XUnit/Execution/XfeRunner.cs
+6
-1
Modified
XFEExtension.NetCore.XUnit/Reporting/ConsolePresenter.cs
+63
-1
Modified
XFEExtension.NetCore.XUnit/Runtime/Descriptors.cs
+5
-0
Modified
XFEExtension.NetCore.XUnit/Runtime/Results.cs
+17
-1
Modified
XFEExtension.NetCore.XUnit/XFEExtension.NetCore.XUnit.csproj
+2
-2
XFEstudio/XFEExtension.NetCore.XUnit
恢复 SMTest 等旧特性单次执行行为
恢复 SMTest/SMNTest/SMRTest/SMNRTest 旧 3.x 单方法计时特性的默认“单次执行”行为,默认模式下单次运行并完整捕获标准输出/错误输出,专属面板展示,且不再作为 Benchmark 运行。BenchmarkAttribute 现代行为保持不变,仅通过 --benchmarks 显式运行。调整生成器、分析器、测试用例和控制台输出逻辑,支持详细输出。TestCaseResult 和 TestDescriptor 增加兼容性字段。更新 README 说明行为变化和迁移建议。增强控制台输出捕获,支持并发和多通道。版本号升至 4.0.1,ReleaseNotes 记录兼容性修复。
e760c24
代码差异
15 个文件
+255
-49
@@ -89,4 +89,4 @@ The analyzer rejects `async void` and invalid lifecycle signatures. Migration di
89
89
90
90
## Migrating from 3.x
91
91
92
`CTest`, `MTest`, `MRTest`, `SMTest`, `SetUp`, and `XFECode` remain source-compatible for the 4.x line and are marked obsolete. Migrate to `TestFixture`, `Test`, `TestCase`, `Benchmark`, `BeforeEach`, and `Assert`. Legacy timing attributes are treated as benchmarks and therefore run only with `--benchmarks`. The compatibility surface will be removed in 5.0.
92
`CTest`, `MTest`, `MRTest`, `SMTest`, `SetUp`, and `XFECode` remain source-compatible for the 4.x line and are marked obsolete. Migrate to `TestFixture`, `Test`, `TestCase`, `Benchmark`, `BeforeEach`, and `Assert`. `SMTest`, `SMNTest`, `SMRTest`, and `SMNRTest` keep their original single-run behavior: they run in the default test mode without command-line options, await asynchronous results, compare legacy expected return values, and show all captured console output in a dedicated compatibility panel. Modern `Benchmark` methods remain opt-in through `--benchmarks`. The compatibility surface will be removed in 5.0.
@@ -87,4 +87,4 @@ dotnet run -c Release -- --benchmarks --baseline previous/benchmark-results.json
87
87
88
88
## 从 3.x 迁移
89
89
90
`CTest`、`MTest`、`MRTest`、`SMTest`、`SetUp` 和 `XFECode` 在整个 4.x 中继续保留并标记为过时。请迁移到 `TestFixture`、`Test`、`TestCase`、`Benchmark`、`BeforeEach` 和 `Assert`。旧计时特性会作为基准处理,因此只在使用 `--benchmarks` 时执行;兼容 API 将在 5.0 删除。
90
`CTest`、`MTest`、`MRTest`、`SMTest`、`SetUp` 和 `XFECode` 在整个 4.x 中继续保留并标记为过时。请迁移到 `TestFixture`、`Test`、`TestCase`、`Benchmark`、`BeforeEach` 和 `Assert`。`SMTest`、`SMNTest`、`SMRTest` 和 `SMNRTest` 保留原有的单次执行语义:无需命令行参数即可在默认测试模式运行,完整等待异步返回、比较旧式期望返回值,并在专属兼容面板中展示捕获到的全部控制台输出。现代 `Benchmark` 仍需通过 `--benchmarks` 显式运行;兼容 API 将在 5.0 删除。
@@ -102,7 +102,7 @@ public sealed class XUnitCodeAnalyzer : DiagnosticAnalyzer
102
102
{
103
103
"CTestAttribute" or "CNTestAttribute" => "TestFixtureAttribute",
104
104
"MTestAttribute" or "MNTestAttribute" or "MRTestAttribute" or "MNRTestAttribute" => "TestCaseAttribute",
105
"SMTestAttribute" or "SMNTestAttribute" or "SMRTestAttribute" or "SMNRTestAttribute" => "BenchmarkAttribute",
105
"SMTestAttribute" or "SMNTestAttribute" or "SMRTestAttribute" or "SMNRTestAttribute" => "TestCaseAttribute",
106
106
"SetUpAttribute" => "BeforeEachAttribute",
107
107
_ => null
108
108
};
@@ -191,6 +191,7 @@ public sealed class XUnitCodeGenerator : IIncrementalGenerator
191
191
var testCases = FindAttributes(method, "TestCaseAttribute").ToArray();
192
192
var memberData = FindAttributes(method, "MemberDataAttribute").ToArray();
193
193
var legacyCases = method.GetAttributes().Where(static attribute => Inherits(attribute.AttributeClass, "MTestAttribute")).ToArray();
194
var legacySingleRunCases = method.GetAttributes().Where(static attribute => Inherits(attribute.AttributeClass, "SMTestAttribute")).ToArray();
194
195
var classCases = method.ContainingType.GetAttributes().Where(static attribute => Inherits(attribute.AttributeClass, "CTestAttribute")).ToArray();
195
196
var index = 0;
196
197
@@ -226,10 +227,18 @@ public sealed class XUnitCodeGenerator : IIncrementalGenerator
226
227
AppendTest(output, method, wrapper, lifecycle, ArrayExpression(arguments), index++, name, true, hasExpected, expected, factory);
227
228
}
228
229
}
230
231
foreach (var attribute in legacySingleRunCases)
232
{
233
var arguments = GetLegacyArguments(attribute, true, out var expected, out var hasExpected, out var name);
234
var factory = "static () => global::XFEExtension.NetCore.XUnit.Runtime.XfeObjectFactory.Create(typeof(" + TypeName(method.ContainingType) + "), [])";
235
AppendTest(output, method, wrapper, lifecycle, ArrayExpression(arguments), index++, name, true, hasExpected, expected, factory, legacySingleRun: true);
236
}
229
237
}
230
238
231
239
private static void AppendTest(StringBuilder output, IMethodSymbol method, string wrapper, string lifecycle, string arguments, int index,
232
string? name, bool legacy, bool hasExpected, string expected, string factory, string? customId = null, string? customDisplay = null, int indent = 8)
240
string? name, bool legacy, bool hasExpected, string expected, string factory, string? customId = null, string? customDisplay = null, int indent = 8,
241
bool legacySingleRun = false)
233
242
{
234
243
var spaces = new string(' ', indent);
235
244
var id = customId ?? Escape(Id(method, index));
@@ -251,6 +260,7 @@ public sealed class XUnitCodeGenerator : IIncrementalGenerator
251
260
output.Append(spaces).Append(" TimeoutMilliseconds = ").Append(GetInt(method, "TimeoutAttribute")).AppendLine(",");
252
261
output.Append(spaces).Append(" RetryCount = ").Append(GetInt(method, "RetryAttribute")).AppendLine(",");
253
262
output.Append(spaces).Append(" IsLegacy = ").Append(legacy ? "true" : "false").AppendLine(",");
263
output.Append(spaces).Append(" IsLegacySingleRun = ").Append(legacySingleRun ? "true" : "false").AppendLine(",");
254
264
output.Append(spaces).Append(" HasExpectedResult = ").Append(hasExpected ? "true" : "false").AppendLine(",");
255
265
output.Append(spaces).Append(" ExpectedResult = ").Append(expected).AppendLine(",");
256
266
output.Append(spaces).Append(" Factory = ").Append(factory).AppendLine(",");
@@ -262,20 +272,15 @@ public sealed class XUnitCodeGenerator : IIncrementalGenerator
262
272
private static void BuildBenchmarkRegistrations(StringBuilder output, IMethodSymbol method, string wrapper, string overheadWrapper, string lifecycle)
263
273
{
264
274
var benchmark = FindAttribute(method, "BenchmarkAttribute");
265
var legacy = method.GetAttributes().Where(static attribute => Inherits(attribute.AttributeClass, "SMTestAttribute")).ToArray();
266
if (benchmark is null && legacy.Length == 0)
275
if (benchmark is null)
267
276
return;
268
var argumentSets = benchmark is null
269
? legacy.Select(attribute => GetLegacyArguments(attribute, true, out _, out _, out _)).ToArray()
270
: FindAttributes(method, "ArgumentsAttribute").Select(attribute => GetArrayArgument(attribute, 0)).DefaultIfEmpty(ImmutableArray<TypedConstant>.Empty).ToArray();
277
var argumentSets = FindAttributes(method, "ArgumentsAttribute").Select(attribute => GetArrayArgument(attribute, 0)).DefaultIfEmpty(ImmutableArray<TypedConstant>.Empty).ToArray();
271
278
var parameterSets = BuildParameterSets(method.ContainingType);
272
279
var caseIndex = 0;
273
280
foreach (var arguments in argumentSets)
274
281
foreach (var parameters in parameterSets)
275
282
{
276
var legacyAttribute = benchmark is null ? legacy[Math.Min(caseIndex, legacy.Length - 1)] : null;
277
var legacyName = legacyAttribute is null ? null : GetLegacyName(legacyAttribute);
278
var displayName = GetNamedString(benchmark, "Name") ?? legacyName ?? method.ContainingType.Name + "." + method.Name;
283
var displayName = GetNamedString(benchmark, "Name") ?? method.ContainingType.Name + "." + method.Name;
279
284
if (parameters.Count > 0)
280
285
displayName += "(" + string.Join(", ", parameters.Select(static pair => pair.Key + "=" + pair.Value.DisplayValue)) + ")";
281
286
output.AppendLine(" registry.AddBenchmark(new global::XFEExtension.NetCore.XUnit.Runtime.BenchmarkDescriptor");
@@ -288,7 +293,7 @@ public sealed class XUnitCodeGenerator : IIncrementalGenerator
288
293
output.Append(" Categories = ").Append(StringArray(GetCategories(method))).AppendLine(",");
289
294
output.Append(" Baseline = ").Append(GetNamedBool(benchmark, "Baseline") ? "true" : "false").AppendLine(",");
290
295
output.Append(" Strategy = (global::XFEExtension.NetCore.XUnit.Attributes.BenchmarkStrategy)").Append(GetNamedInt(benchmark, "Strategy")).AppendLine(",");
291
output.Append(" IsLegacy = ").Append(benchmark is null ? "true" : "false").AppendLine(",");
296
output.AppendLine(" IsLegacy = false,");
292
297
output.Append(" ParameterKey = ").Append(Escape(string.Join(";", parameters.Select(static pair => pair.Key + "=" + pair.Value.DisplayValue)))).AppendLine(",");
293
298
output.Append(" Factory = static () => global::XFEExtension.NetCore.XUnit.Runtime.XfeObjectFactory.Create(typeof(").Append(TypeName(method.ContainingType)).AppendLine("), []),");
294
299
output.Append(" Invoker = ").Append(wrapper).AppendLine(",");
@@ -19,6 +19,11 @@ internal sealed class LegacyCompatibilityTests
19
19
public int ComparesLegacyReturnValue(int left, int right) => left + right;
20
20
21
21
[SMTest]
22
public int RunsLegacyBenchmark() => 40 + 2;
22
public int RunsLegacyBenchmark()
23
{
24
Console.WriteLine("SMTest standard output");
25
Console.Error.WriteLine("SMTest error output");
26
return 40 + 2;
27
}
23
28
}
24
29
#pragma warning restore CS0618, XFE0100
@@ -1,4 +1,6 @@
1
1
using System.Collections.Immutable;
2
using System.Diagnostics;
3
using System.Reflection;
2
4
using System.Text.Json;
3
5
using Microsoft.CodeAnalysis;
4
6
using Microsoft.CodeAnalysis.CodeActions;
@@ -146,6 +148,16 @@ internal class Program
146
148
[Test]
147
149
public void UsesConfiguredActivator() => Assert.True(TrackingActivator.ActivationCount > 0);
148
150
151
[Test]
152
public void RegistersSmTestAsDefaultSingleRunTest()
153
{
154
var registry = XfeGeneratedRegistry.Create();
155
var descriptor = Assert.Single(registry.Tests.Where(static test => test.MethodName == nameof(LegacyCompatibilityTests.RunsLegacyBenchmark)));
156
157
Assert.True(descriptor.IsLegacySingleRun);
158
Assert.False(registry.Benchmarks.Any(static benchmark => benchmark.MethodName == nameof(LegacyCompatibilityTests.RunsLegacyBenchmark)));
159
}
160
149
161
[Test]
150
162
[Skip("Verifies skip reporting.")]
151
163
public void SkippedTest() => throw new InvalidOperationException("A skipped test must not run.");
@@ -170,6 +182,23 @@ internal sealed class IsolatedTests
170
182
[TestFixture]
171
183
internal sealed class WorkerProcessTests
172
184
{
185
[Test]
186
public async Task RunsSmTestWithoutModeArgumentsAndShowsAllConsoleOutput()
187
{
188
using var process = StartCurrentProcess("--filter", nameof(LegacyCompatibilityTests.RunsLegacyBenchmark), "--report", "none", "--language", "en");
189
var standardOutput = process.StandardOutput.ReadToEndAsync();
190
var errorOutput = process.StandardError.ReadToEndAsync();
191
192
await process.WaitForExitAsync();
193
var output = await standardOutput + await errorOutput;
194
195
Assert.Equal(0, process.ExitCode);
196
Assert.Equal(1, CountOccurrences(output, "SMTest standard output"));
197
Assert.Equal(1, CountOccurrences(output, "SMTest error output"));
198
Assert.Contains("SMTest single-run output", output);
199
Assert.False(output.Contains("Benchmark results", StringComparison.Ordinal));
200
}
201
173
202
[Test]
174
203
public async Task TerminatesHardTimeoutWorker()
175
204
{
@@ -193,6 +222,35 @@ internal sealed class WorkerProcessTests
193
222
[Explicit("Selected by ReportsCrashedWorker to validate crash reporting.")]
194
223
[Isolated]
195
224
public void WorkerCrashProbe() => Environment.FailFast("Intentional worker crash probe.");
225
226
private static Process StartCurrentProcess(params string[] arguments)
227
{
228
var processPath = Environment.ProcessPath ?? throw new InvalidOperationException("Cannot determine the current executable path.");
229
var startInfo = new ProcessStartInfo(processPath)
230
{
231
UseShellExecute = false,
232
RedirectStandardOutput = true,
233
RedirectStandardError = true,
234
WorkingDirectory = Environment.CurrentDirectory
235
};
236
if (string.Equals(Path.GetFileNameWithoutExtension(processPath), "dotnet", StringComparison.OrdinalIgnoreCase))
237
startInfo.ArgumentList.Add(Assembly.GetEntryAssembly()?.Location ?? throw new InvalidOperationException("Cannot determine the entry assembly."));
238
foreach (var argument in arguments)
239
startInfo.ArgumentList.Add(argument);
240
return Process.Start(startInfo) ?? throw new InvalidOperationException("Failed to start the current test process.");
241
}
242
243
private static int CountOccurrences(string value, string search)
244
{
245
var count = 0;
246
var startIndex = 0;
247
while ((startIndex = value.IndexOf(search, startIndex, StringComparison.Ordinal)) >= 0)
248
{
249
count++;
250
startIndex += search.Length;
251
}
252
return count;
253
}
196
254
}
197
255
198
256
[TestFixture]
@@ -138,14 +138,14 @@ public sealed class MNRTestAttribute : MRTestAttribute
138
138
}
139
139
140
140
/// <summary>
141
/// 兼容 3.x 的单方法计时特性;4.x 将其作为基准执行。
141
/// 兼容 3.x 的单方法计时特性;4.x 在默认测试模式中单次执行并展示其全部控制台输出。
142
142
/// </summary>
143
[Obsolete("Use BenchmarkAttribute. Legacy attributes will be removed in XUnit 5.0.")]
143
[Obsolete("Use TestAttribute or TestCaseAttribute. Legacy attributes will be removed in XUnit 5.0.")]
144
144
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
145
145
public class SMTestAttribute : XFETestAttributeBase
146
146
{
147
147
/// <summary>
148
/// 初始化旧基准特性。
148
/// 初始化旧单次执行测试特性。
149
149
/// </summary>
150
150
/// <param name="values">传递给基准方法的参数。</param>
151
151
public SMTestAttribute(params object?[] values) => Params = values;
@@ -154,7 +154,7 @@ public class SMTestAttribute : XFETestAttributeBase
154
154
/// <summary>
155
155
/// 兼容 3.x 的具名单方法计时特性。
156
156
/// </summary>
157
[Obsolete("Use BenchmarkAttribute.Name. Legacy attributes will be removed in XUnit 5.0.")]
157
[Obsolete("Use TestAttribute or TestCaseAttribute. Legacy attributes will be removed in XUnit 5.0.")]
158
158
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
159
159
public sealed class SMNTestAttribute : SMTestAttribute
160
160
{
@@ -164,7 +164,7 @@ public sealed class SMNTestAttribute : SMTestAttribute
164
164
public string TimerName { get; set; } = string.Empty;
165
165
166
166
/// <summary>
167
/// 使用计时器名称和调用参数初始化旧基准。
167
/// 使用计时器名称和调用参数初始化旧单次执行测试。
168
168
/// </summary>
169
169
/// <param name="timerName">计时器显示名称。</param>
170
170
/// <param name="values">传递给基准方法的参数。</param>
@@ -174,7 +174,7 @@ public sealed class SMNTestAttribute : SMTestAttribute
174
174
/// <summary>
175
175
/// 兼容 3.x 的带返回值单方法计时特性。
176
176
/// </summary>
177
[Obsolete("Use BenchmarkAttribute and consume the return value. Legacy attributes will be removed in XUnit 5.0.")]
177
[Obsolete("Use TestCaseAttribute and Assert.Equal. Legacy attributes will be removed in XUnit 5.0.")]
178
178
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
179
179
public class SMRTestAttribute : SMTestAttribute
180
180
{
@@ -184,7 +184,7 @@ public class SMRTestAttribute : SMTestAttribute
184
184
public object? ReturnValue { get; set; }
185
185
186
186
/// <summary>
187
/// 使用方法参数和位于最后一项的期望返回值初始化旧基准。
187
/// 使用方法参数和位于最后一项的期望返回值初始化旧单次执行测试。
188
188
/// </summary>
189
189
/// <param name="valuesAndResult">方法参数,最后一项为期望返回值。</param>
190
190
public SMRTestAttribute(params object?[] valuesAndResult)
@@ -201,7 +201,7 @@ public class SMRTestAttribute : SMTestAttribute
201
201
/// <summary>
202
202
/// 兼容 3.x 的具名带返回值单方法计时特性。
203
203
/// </summary>
204
[Obsolete("Use BenchmarkAttribute.Name and consume the return value. Legacy attributes will be removed in XUnit 5.0.")]
204
[Obsolete("Use TestCaseAttribute and Assert.Equal. Legacy attributes will be removed in XUnit 5.0.")]
205
205
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
206
206
public sealed class SMNRTestAttribute : SMRTestAttribute
207
207
{
@@ -211,7 +211,7 @@ public sealed class SMNRTestAttribute : SMRTestAttribute
211
211
public string? TimerName { get; set; }
212
212
213
213
/// <summary>
214
/// 使用计时器名称、方法参数和期望返回值初始化旧基准。
214
/// 使用计时器名称、方法参数和期望返回值初始化旧单次执行测试。
215
215
/// </summary>
216
216
/// <param name="timerName">计时器显示名称。</param>
217
217
/// <param name="valuesAndResult">方法参数,最后一项为期望返回值。</param>
@@ -4,9 +4,10 @@ namespace XFEExtension.NetCore.XUnit.Execution;
4
4
5
5
internal sealed class AsyncLocalConsoleCapture : TextWriter
6
6
{
7
private static readonly AsyncLocal<StringBuilder?> s_current = new();
7
private static readonly AsyncLocal<CaptureBuffer?> s_current = new();
8
8
private readonly TextWriter _fallback;
9
private static AsyncLocalConsoleCapture? s_instance;
9
private static AsyncLocalConsoleCapture? s_outputInstance;
10
private static AsyncLocalConsoleCapture? s_errorInstance;
10
11
11
12
private AsyncLocalConsoleCapture(TextWriter fallback) => _fallback = fallback;
12
13
@@ -14,47 +15,79 @@ internal sealed class AsyncLocalConsoleCapture : TextWriter
14
15
15
16
public static void Install()
16
17
{
17
if (s_instance is not null)
18
if (s_outputInstance is not null)
18
19
return;
19
s_instance = new AsyncLocalConsoleCapture(Console.Out);
20
Console.SetOut(s_instance);
20
s_outputInstance = new AsyncLocalConsoleCapture(Console.Out);
21
s_errorInstance = new AsyncLocalConsoleCapture(Console.Error);
22
Console.SetOut(s_outputInstance);
23
Console.SetError(s_errorInstance);
21
24
}
22
25
23
26
public static IDisposable Begin(out Func<string> getOutput)
24
27
{
25
28
var previous = s_current.Value;
26
var builder = new StringBuilder();
27
s_current.Value = builder;
28
getOutput = builder.ToString;
29
var buffer = new CaptureBuffer();
30
s_current.Value = buffer;
31
getOutput = buffer.GetText;
29
32
return new CaptureScope(previous);
30
33
}
31
34
32
35
public override void Write(char value)
33
36
{
34
if (s_current.Value is { } builder)
35
builder.Append(value);
37
if (s_current.Value is { } buffer)
38
buffer.Append(value);
36
39
else
37
40
_fallback.Write(value);
38
41
}
39
42
40
43
public override void Write(string? value)
41
44
{
42
if (s_current.Value is { } builder)
43
builder.Append(value);
45
if (s_current.Value is { } buffer)
46
buffer.Append(value);
44
47
else
45
48
_fallback.Write(value);
46
49
}
47
50
48
51
public override void WriteLine(string? value)
49
52
{
50
if (s_current.Value is { } builder)
51
builder.AppendLine(value);
53
if (s_current.Value is { } buffer)
54
buffer.AppendLine(value);
52
55
else
53
56
_fallback.WriteLine(value);
54
57
}
55
58
56
private sealed class CaptureScope(StringBuilder? previous) : IDisposable
59
private sealed class CaptureScope(CaptureBuffer? previous) : IDisposable
57
60
{
58
61
public void Dispose() => s_current.Value = previous;
59
62
}
63
64
private sealed class CaptureBuffer
65
{
66
private readonly Lock _lock = new();
67
private readonly StringBuilder _builder = new();
68
69
public void Append(char value)
70
{
71
lock (_lock)
72
_builder.Append(value);
73
}
74
75
public void Append(string? value)
76
{
77
lock (_lock)
78
_builder.Append(value);
79
}
80
81
public void AppendLine(string? value)
82
{
83
lock (_lock)
84
_builder.AppendLine(value);
85
}
86
87
public string GetText()
88
{
89
lock (_lock)
90
return _builder.ToString();
91
}
92
}
60
93
}
@@ -74,8 +74,8 @@ internal sealed class TestExecutor
74
74
}
75
75
if (test.SkipReason is not null || test.Explicit && !settings.IncludeExplicit)
76
76
{
77
results.Add(new TestCaseResult(test.Id, test.DisplayName, TestOutcome.Skipped, TimeSpan.Zero, TimeSpan.Zero, 0,
78
test.SkipReason ?? "Explicit test was not selected."));
77
results.Add(WithDescriptor(test, new TestCaseResult(test.Id, test.DisplayName, TestOutcome.Skipped, TimeSpan.Zero, TimeSpan.Zero, 0,
78
test.SkipReason ?? "Explicit test was not selected.")));
79
79
continue;
80
80
}
81
81
@@ -145,7 +145,7 @@ internal sealed class TestExecutor
145
145
}
146
146
totalWatch.Stop();
147
147
output = getOutput();
148
lastResult = new TestCaseResult(test.Id, test.DisplayName, TestOutcome.Passed, bodyDuration, totalWatch.Elapsed, attempt, Output: output);
148
lastResult = WithDescriptor(test, new TestCaseResult(test.Id, test.DisplayName, TestOutcome.Passed, bodyDuration, totalWatch.Elapsed, attempt, Output: output));
149
149
return lastResult;
150
150
}
151
151
catch (Exception exception)
@@ -176,8 +176,15 @@ internal sealed class TestExecutor
176
176
private static TestCaseResult FailedFromException(TestDescriptor test, Exception exception, TestOutcome outcome, int attempts, TimeSpan body, TimeSpan total, string? output)
177
177
{
178
178
var actual = exception is AggregateException aggregateException ? aggregateException.Flatten() : exception;
179
return new TestCaseResult(test.Id, test.DisplayName, outcome, body, total, attempts, actual.Message, actual.StackTrace, output);
179
return WithDescriptor(test, new TestCaseResult(test.Id, test.DisplayName, outcome, body, total, attempts, actual.Message, actual.StackTrace, output));
180
180
}
181
181
182
private static TestCaseResult WithDescriptor(TestDescriptor test, TestCaseResult result) => result with
183
{
184
IsLegacySingleRun = test.IsLegacySingleRun,
185
TypeName = test.TypeName,
186
MethodName = test.MethodName
187
};
188
182
189
private static ValueTask DisposeInstanceAsync(object? instance) => XfeObjectFactory.DisposeAsync(instance);
183
190
}
@@ -27,7 +27,12 @@ internal static class WorkerProcess
27
27
await EnsureStoppedAsync(process).ConfigureAwait(false);
28
28
var timedOutOutput = await stdoutTask.ConfigureAwait(false) + await stderrTask.ConfigureAwait(false);
29
29
return new TestCaseResult(descriptor.Id, descriptor.DisplayName, TestOutcome.TimedOut, TimeSpan.Zero, elapsed, 1,
30
$"Test exceeded the {timeout} ms timeout and its worker process was terminated.", Output: timedOutOutput);
30
$"Test exceeded the {timeout} ms timeout and its worker process was terminated.", Output: timedOutOutput)
31
{
32
IsLegacySingleRun = descriptor.IsLegacySingleRun,
33
TypeName = descriptor.TypeName,
34
MethodName = descriptor.MethodName
35
};
31
36
}
32
37
33
38
var stdout = await stdoutTask.ConfigureAwait(false);
@@ -39,7 +44,12 @@ internal static class WorkerProcess
39
44
return result with { Output = string.Concat(result.Output, stdout, stderr) };
40
45
}
41
46
return new TestCaseResult(descriptor.Id, descriptor.DisplayName, TestOutcome.Crashed, TimeSpan.Zero, elapsed, 1,
42
$"Worker exited with code {process.ExitCode} without producing a result.", Output: stdout + stderr);
47
$"Worker exited with code {process.ExitCode} without producing a result.", Output: stdout + stderr)
48
{
49
IsLegacySingleRun = descriptor.IsLegacySingleRun,
50
TypeName = descriptor.TypeName,
51
MethodName = descriptor.MethodName
52
};
43
53
}
44
54
finally
45
55
{
@@ -176,7 +176,12 @@ public static partial class XFERunner
176
176
if (test.SkipReason is not null || test.Explicit && !settings.Tests.IncludeExplicit)
177
177
{
178
178
results.Add(new TestCaseResult(test.Id, test.DisplayName, TestOutcome.Skipped, TimeSpan.Zero, TimeSpan.Zero, 0,
179
test.SkipReason ?? "Explicit test was not selected."));
179
test.SkipReason ?? "Explicit test was not selected.")
180
{
181
IsLegacySingleRun = test.IsLegacySingleRun,
182
TypeName = test.TypeName,
183
MethodName = test.MethodName
184
});
180
185
continue;
181
186
}
182
187
results.Add(await WorkerProcess.RunTestAsync(test, settings.Tests, cancellationToken).ConfigureAwait(false));
@@ -75,6 +75,15 @@ internal sealed class ConsolePresenter(ConsoleLocalizer text)
75
75
76
76
public void PrintTests(TestRunSummary summary)
77
77
{
78
var legacySingleRuns = summary.Results.Where(static result => result.IsLegacySingleRun).ToArray();
79
if (legacySingleRuns.Length > 0)
80
{
81
WriteSection(text.Select("SMTest single-run output", "SMTest 单次执行输出"), text.Select($"{legacySingleRuns.Length} methods", $"{legacySingleRuns.Length} 个方法"));
82
for (var index = 0; index < legacySingleRuns.Length; index++)
83
PrintLegacySingleRun(legacySingleRuns[index], index + 1);
84
Console.WriteLine();
85
}
86
78
87
WriteSection(text.Select("Test results", "测试结果"), text.Select($"{summary.Total} {(summary.Total == 1 ? "test" : "tests")}", $"{summary.Total} 个测试"));
79
88
if (summary.Results.Count == 0)
80
89
{
@@ -110,7 +119,7 @@ internal sealed class ConsolePresenter(ConsoleLocalizer text)
110
119
}
111
120
if (result.Attempts > 1)
112
121
WriteDetail(text.Select("Attempts", "尝试次数"), result.Attempts.ToString(), ConsoleColor.DarkYellow);
113
if (result.Outcome != TestOutcome.Passed && !string.IsNullOrWhiteSpace(result.Output))
122
if (result.Outcome != TestOutcome.Passed && !result.IsLegacySingleRun && !string.IsNullOrWhiteSpace(result.Output))
114
123
WriteBlock(text.Select("Captured output", "捕获的输出"), result.Output, ConsoleColor.DarkGray);
115
124
if (result.Outcome is TestOutcome.Failed or TestOutcome.TimedOut or TestOutcome.Crashed && !string.IsNullOrWhiteSpace(result.StackTrace))
116
125
WriteBlock(text.Select("Stack trace", "堆栈跟踪"), result.StackTrace, ConsoleColor.DarkGray);
@@ -121,6 +130,59 @@ internal sealed class ConsolePresenter(ConsoleLocalizer text)
121
130
PrintSlowestTests(summary);
122
131
}
123
132
133
private void PrintLegacySingleRun(TestCaseResult result, int executionIndex)
134
{
135
var width = SafeConsoleWidth();
136
var typeName = result.TypeName?.Split('.').LastOrDefault() ?? text.Select("Unknown type", "未知类型");
137
var methodName = result.MethodName ?? result.DisplayName;
138
var defaultName = $"{typeName}.{methodName}";
139
var usesCustomName = !result.DisplayName.StartsWith(defaultName, StringComparison.Ordinal);
140
var title = usesCustomName
141
? text.Select($"Name: {result.DisplayName}", $"标识名:{result.DisplayName}")
142
: text.Select($"Method: {methodName}", $"方法名:{methodName}");
143
title = TruncateDisplay(title, Math.Max(10, width - 8));
144
var titlePrefix = $"╭─ {title} ";
145
WriteColored(titlePrefix, ConsoleColor.DarkYellow);
146
WriteColoredLine(new string('─', Math.Max(1, width - DisplayWidth(titlePrefix) - 1)) + "╮", ConsoleColor.DarkYellow);
147
148
WriteColored("│ ", ConsoleColor.DarkYellow);
149
WriteBadge(text.Select("START", "开始执行"), 12, ConsoleColor.Cyan);
150
WriteColored($" {text.Select("Method", "方法")} ", ConsoleColor.DarkGray);
151
WriteColored(methodName, ConsoleColor.Yellow);
152
WriteColored($" {text.Select("Class", "类")} ", ConsoleColor.DarkGray);
153
WriteColoredLine(typeName, ConsoleColor.Green);
154
WriteColoredLine("│", ConsoleColor.DarkYellow);
155
156
if (!string.IsNullOrEmpty(result.Output))
157
{
158
Console.Write(result.Output);
159
if (!result.Output.EndsWith('\n'))
160
Console.WriteLine();
161
}
162
163
WriteColoredLine("│", ConsoleColor.DarkYellow);
164
WriteColored("│ ", ConsoleColor.DarkYellow);
165
WriteBadge(text.Select("FINISHED", "执行完成"), 12, ConsoleColor.Cyan);
166
WriteColored($" {text.Select("Run", "执行批次")} ", ConsoleColor.DarkGray);
167
WriteColored(executionIndex.ToString(), ConsoleColor.Gray);
168
WriteColored($" {text.Select("Duration", "执行时间")} ", ConsoleColor.DarkGray);
169
WriteColoredLine(FormatDuration(result.BodyDuration), ConsoleColor.Cyan);
170
171
WriteColored("│ ", ConsoleColor.DarkYellow);
172
var passed = result.Outcome == TestOutcome.Passed;
173
WriteBadge(passed ? text.Select("PASSED", "测试通过") : text.Select("FAILED", "测试失败"), 12, passed ? ConsoleColor.Green : ConsoleColor.Red);
174
if (!passed && !string.IsNullOrWhiteSpace(result.Message))
175
{
176
WriteColored($" {text.Select("Reason", "失败原因")} ", ConsoleColor.Red);
177
WriteColoredLine(text.Message(result.Message), ConsoleColor.Gray);
178
}
179
else
180
{
181
Console.WriteLine();
182
}
183
WriteColoredLine("╰" + new string('─', Math.Max(1, width - 2)) + "╯", ConsoleColor.DarkYellow);
184
}
185
124
186
public void PrintBenchmarks(BenchmarkRunSummary summary)
125
187
{
126
188
WriteSection(text.Select("Benchmark results", "基准结果"), text.Select($"{summary.Benchmarks.Count} {(summary.Benchmarks.Count == 1 ? "benchmark" : "benchmarks")}", $"{summary.Benchmarks.Count} 个基准"));