Skip to content

Commit bedd847

Browse files
rogeralsingclaude
andcommitted
Add live stats panel using Spectre.Console
- Real-time progress bar with pass/fail/skip counts - Shows currently running tests - Displays elapsed time and tests/second rate - Panel updates as each test completes - Failure details shown after completion - Use --quiet flag for minimal output mode Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent e081e6d commit bedd847

File tree

2 files changed

+334
-52
lines changed

2 files changed

+334
-52
lines changed
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
using System.Diagnostics;
2+
using Spectre.Console;
3+
using Spectre.Console.Rendering;
4+
5+
namespace Asynkron.TestRunner;
6+
7+
/// <summary>
8+
/// Live console display for test execution progress
9+
/// </summary>
10+
public class LiveDisplay
11+
{
12+
private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
13+
private readonly object _lock = new();
14+
15+
private int _total;
16+
private int _passed;
17+
private int _failed;
18+
private int _skipped;
19+
private int _hanging;
20+
private int _crashed;
21+
private readonly HashSet<string> _running = new();
22+
private string? _lastCompleted;
23+
private string? _lastStatus;
24+
25+
public void SetTotal(int total)
26+
{
27+
lock (_lock) _total = total;
28+
}
29+
30+
public void TestStarted(string displayName)
31+
{
32+
lock (_lock) _running.Add(Truncate(displayName, 60));
33+
}
34+
35+
public void TestPassed(string displayName)
36+
{
37+
lock (_lock)
38+
{
39+
_passed++;
40+
_running.Remove(Truncate(displayName, 60));
41+
_lastCompleted = displayName;
42+
_lastStatus = "[green]✓[/]";
43+
}
44+
}
45+
46+
public void TestFailed(string displayName)
47+
{
48+
lock (_lock)
49+
{
50+
_failed++;
51+
_running.Remove(Truncate(displayName, 60));
52+
_lastCompleted = displayName;
53+
_lastStatus = "[red]✗[/]";
54+
}
55+
}
56+
57+
public void TestSkipped(string displayName)
58+
{
59+
lock (_lock)
60+
{
61+
_skipped++;
62+
_running.Remove(Truncate(displayName, 60));
63+
_lastCompleted = displayName;
64+
_lastStatus = "[yellow]○[/]";
65+
}
66+
}
67+
68+
public void TestHanging(string displayName)
69+
{
70+
lock (_lock)
71+
{
72+
_hanging++;
73+
_running.Remove(Truncate(displayName, 60));
74+
_lastCompleted = displayName;
75+
_lastStatus = "[red]⏱[/]";
76+
}
77+
}
78+
79+
public void TestCrashed(string displayName)
80+
{
81+
lock (_lock)
82+
{
83+
_crashed++;
84+
_running.Remove(Truncate(displayName, 60));
85+
_lastCompleted = displayName;
86+
_lastStatus = "[red]💥[/]";
87+
}
88+
}
89+
90+
public void WorkerRestarted(int remaining)
91+
{
92+
lock (_lock)
93+
{
94+
_running.Clear();
95+
}
96+
}
97+
98+
public IRenderable Render()
99+
{
100+
lock (_lock)
101+
{
102+
var completed = _passed + _failed + _skipped + _hanging + _crashed;
103+
var elapsed = _stopwatch.Elapsed;
104+
var rate = elapsed.TotalSeconds > 0 ? completed / elapsed.TotalSeconds : 0;
105+
106+
var grid = new Grid();
107+
grid.AddColumn(new GridColumn().NoWrap());
108+
grid.AddColumn(new GridColumn().NoWrap());
109+
grid.AddColumn(new GridColumn().NoWrap());
110+
grid.AddColumn(new GridColumn().NoWrap());
111+
grid.AddColumn(new GridColumn().NoWrap());
112+
grid.AddColumn(new GridColumn().NoWrap());
113+
114+
// Stats row
115+
grid.AddRow(
116+
new Markup($"[green]{_passed}[/] passed"),
117+
new Markup($"[red]{_failed}[/] failed"),
118+
new Markup($"[yellow]{_skipped}[/] skipped"),
119+
new Markup($"[red]{_hanging}[/] hanging"),
120+
new Markup($"[red]{_crashed}[/] crashed"),
121+
new Markup($"[dim]{elapsed:mm\\:ss}[/] [dim]({rate:F1}/s)[/]")
122+
);
123+
124+
var layout = new Rows(
125+
grid,
126+
new Text(""),
127+
CreateProgressBar(completed, _total),
128+
new Text(""),
129+
CreateRunningSection()
130+
);
131+
132+
return new Panel(layout)
133+
.Header($"[bold]Test Progress[/] [dim]({completed}/{_total})[/]")
134+
.Border(BoxBorder.Rounded)
135+
.BorderColor(Color.Grey);
136+
}
137+
}
138+
139+
private IRenderable CreateProgressBar(int completed, int total)
140+
{
141+
if (total == 0) return new Text("");
142+
143+
var percentage = (double)completed / total;
144+
var width = Math.Min(60, Console.WindowWidth - 10);
145+
var filled = (int)(percentage * width);
146+
var empty = width - filled;
147+
148+
var color = _failed > 0 || _crashed > 0 || _hanging > 0 ? "red" : "green";
149+
var bar = $"[{color}]{new string('█', filled)}[/][dim]{new string('░', empty)}[/]";
150+
151+
return new Markup($"{bar} [dim]{percentage:P0}[/]");
152+
}
153+
154+
private IRenderable CreateRunningSection()
155+
{
156+
var lines = new List<IRenderable>();
157+
158+
if (_lastCompleted != null && _lastStatus != null)
159+
{
160+
lines.Add(new Markup($"{_lastStatus} {Markup.Escape(Truncate(_lastCompleted, 70))}"));
161+
}
162+
163+
if (_running.Count > 0)
164+
{
165+
var runningList = _running.Take(3).ToList();
166+
foreach (var test in runningList)
167+
{
168+
lines.Add(new Markup($"[dim]► {Markup.Escape(test)}[/]"));
169+
}
170+
if (_running.Count > 3)
171+
{
172+
lines.Add(new Markup($"[dim] ...and {_running.Count - 3} more running[/]"));
173+
}
174+
}
175+
176+
if (lines.Count == 0)
177+
{
178+
lines.Add(new Markup("[dim]Waiting for tests...[/]"));
179+
}
180+
181+
return new Rows(lines);
182+
}
183+
184+
private static string Truncate(string text, int maxLength)
185+
{
186+
if (text.Length <= maxLength) return text;
187+
return text[..(maxLength - 3)] + "...";
188+
}
189+
190+
public (int passed, int failed, int skipped, int hanging, int crashed) GetCounts()
191+
{
192+
lock (_lock)
193+
{
194+
return (_passed, _failed, _skipped, _hanging, _crashed);
195+
}
196+
}
197+
}

0 commit comments

Comments
 (0)