XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEExtension.NetCore.XUnit

【DLL】提供方便快捷的测试,无需编写Main方法,可直接添加特性在类或方法上进行测试

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/XFEExtension.NetCore.XUnit

恢复 SMTest 等旧特性单次执行行为

恢复 SMTest/SMNTest/SMRTest/SMNRTest 旧 3.x 单方法计时特性的默认“单次执行”行为,默认模式下单次运行并完整捕获标准输出/错误输出,专属面板展示,且不再作为 Benchmark 运行。BenchmarkAttribute 现代行为保持不变,仅通过 --benchmarks 显式运行。调整生成器、分析器、测试用例和控制台输出逻辑,支持详细输出。TestCaseResult 和 TestDescriptor 增加兼容性字段。更新 README 说明行为变化和迁移建议。增强控制台输出捕获,支持并发和多通道。版本号升至 4.0.1,ReleaseNotes 记录兼容性修复。

e760c24
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

15 个文件 +255 -49
Modified README.md +1 -1
@@ -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.
Modified README.zh-CN.md +1 -1
@@ -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 删除。
Modified XFEExtension.NetCore.XUnit.Analyzer/Diagnostics/XUnitCodeAnalyzer.cs +1 -1
@@ -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 };
Modified XFEExtension.NetCore.XUnit.Analyzer/Generator/XUnitCodeGenerator.cs +15 -10
@@ -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(",");
Modified XFEExtension.NetCore.XUnit.Test/LegacyCompatibilityTests.cs +6 -1
@@ -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
Modified XFEExtension.NetCore.XUnit.Test/Program.cs +58 -0
@@ -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]
Modified XFEExtension.NetCore.XUnit/Attributes/LegacyAttributes.cs +9 -9
@@ -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>
Modified XFEExtension.NetCore.XUnit/Execution/AsyncLocalConsoleCapture.cs +48 -15
@@ -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 }
Modified XFEExtension.NetCore.XUnit/Execution/TestExecutor.cs +11 -4
@@ -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 }
Modified XFEExtension.NetCore.XUnit/Execution/WorkerProcess.cs +12 -2
@@ -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 {
Modified XFEExtension.NetCore.XUnit/Execution/XfeRunner.cs +6 -1
@@ -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));
Modified XFEExtension.NetCore.XUnit/Reporting/ConsolePresenter.cs +63 -1
@@ -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} 个基准"));
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