Forge
csharp615df5ae
1using System.Text.RegularExpressions;
2
3namespace DotNetBuildTestParsers;
4
5/// <summary>Результат одного теста: имя и при падении — сообщение и опционально время.</summary>
6public sealed record TestResultItem(string Name, bool Passed, string? Message = null, int? DurationMs = null);
7
8/// <summary>Результат разбора вывода dotnet test (console logger).</summary>
9public sealed record TestParseResult(
10 int Total,
11 int Passed,
12 int Failed,
13 int Skipped,
14 IReadOnlyList<TestResultItem> FailedTests)
15{
16 public bool Success => Failed == 0;
17}
18
19public static class TestOutputParser
20{
21 private static readonly Regex ResultLineRegex = new(
22 @"^\s*(Passed|Failed|Skipped)\s+(.+?)(?:\s+\[(\d+)\s*ms\])?\s*$",
23 RegexOptions.Compiled);
24
25 private static readonly Regex MessageLineRegex = new(
26 @"^\s*(?:Error Message|Message|Stack Trace):\s*(.*)$",
27 RegexOptions.Compiled);
28
29 /// <summary>Разбирает вывод dotnet test (logger console;verbosity=normal или detailed).</summary>
30 public static TestParseResult Parse(string output)
31 {
32 if (string.IsNullOrEmpty(output))
33 return new TestParseResult(0, 0, 0, 0, []);
34
35 var passed = 0;
36 var failed = 0;
37 var skipped = 0;
38 var failedTests = new List<TestResultItem>();
39 var lines = output.Split('\n');
40 string? lastFailedName = null;
41 int? lastFailedDuration = null;
42 var messageLines = new List<string>();
43
44 void FlushFailed()
45 {
46 if (lastFailedName is null) return;
47 var msg = messageLines.Count > 0 ? string.Join(" ", messageLines).Trim() : null;
48 if (string.IsNullOrWhiteSpace(msg)) msg = null;
49 failedTests.Add(new TestResultItem(lastFailedName, false, msg, lastFailedDuration));
50 lastFailedName = null;
51 lastFailedDuration = null;
52 messageLines.Clear();
53 }
54
55 foreach (var line in lines)
56 {
57 var m = ResultLineRegex.Match(line.Trim());
58 if (m.Success)
59 {
60 FlushFailed();
61 var outcome = m.Groups[1].Value;
62 var name = m.Groups[2].Value.Trim();
63 var duration = m.Groups[3].Success && m.Groups[3].Value.Length > 0 ? int.Parse(m.Groups[3].ValueSpan) : (int?)null;
64
65 switch (outcome)
66 {
67 case "Passed":
68 passed++;
69 break;
70 case "Failed":
71 failed++;
72 lastFailedName = name;
73 lastFailedDuration = duration;
74 break;
75 case "Skipped":
76 skipped++;
77 break;
78 }
79 continue;
80 }
81
82 var msgMatch = MessageLineRegex.Match(line);
83 if (msgMatch.Success && lastFailedName is not null)
84 messageLines.Add(msgMatch.Groups[1].Value.Trim());
85 }
86
87 FlushFailed();
88
89 var summaryMatch = Regex.Match(output, @"(?:Failed|Passed)!\s*-\s*Failed:\s*(\d+),\s*Passed:\s*(\d+),\s*Skipped:\s*(\d+)", RegexOptions.IgnoreCase);
90 if (summaryMatch.Success)
91 {
92 failed = int.Parse(summaryMatch.Groups[1].ValueSpan);
93 passed = int.Parse(summaryMatch.Groups[2].ValueSpan);
94 skipped = int.Parse(summaryMatch.Groups[3].ValueSpan);
95 }
96 else
97 {
98 if (passed + failed + skipped == 0 && failedTests.Count > 0)
99 failed = failedTests.Count;
100 }
101
102 var totalCount = passed + failed + skipped;
103 return new TestParseResult(totalCount, passed, failed, skipped, failedTests);
104 }
105}
106
View only · write via MCP/CIDE