Skip to content

Commit de1b5cd

Browse files
rogeralsingclaude
andcommitted
Add worker health status bar with color indicators
- Shows worker status below the panel: workers: ● ● ● ● - Colors based on time since last activity: - Green: < 25% of timeout - Yellow: 25-50% of timeout - Orange: 50-75% of timeout - Red: > 75% of timeout - Shows ⏳ during worker restart - Shows ✓ when worker completes Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent ab44d09 commit de1b5cd

File tree

2 files changed

+113
-13
lines changed

2 files changed

+113
-13
lines changed

src/Asynkron.TestRunner/LiveDisplay.cs

Lines changed: 106 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ public class LiveDisplay
2727
private string? _filter;
2828
private string? _assemblyName;
2929
private int _workerCount = 1;
30+
private int _timeoutSeconds = 30;
31+
private readonly Dictionary<int, WorkerState> _workerStates = new();
32+
33+
private class WorkerState
34+
{
35+
public DateTime LastActivity { get; set; } = DateTime.UtcNow;
36+
public bool IsRestarting { get; set; }
37+
public bool IsComplete { get; set; }
38+
}
3039

3140
public void SetTotal(int total)
3241
{
@@ -45,7 +54,47 @@ public void SetAssembly(string assemblyPath)
4554

4655
public void SetWorkerCount(int count)
4756
{
48-
lock (_lock) _workerCount = count;
57+
lock (_lock)
58+
{
59+
_workerCount = count;
60+
for (var i = 0; i < count; i++)
61+
_workerStates[i] = new WorkerState();
62+
}
63+
}
64+
65+
public void SetTimeout(int seconds)
66+
{
67+
lock (_lock) _timeoutSeconds = seconds;
68+
}
69+
70+
public void WorkerActivity(int workerIndex)
71+
{
72+
lock (_lock)
73+
{
74+
if (_workerStates.TryGetValue(workerIndex, out var state))
75+
{
76+
state.LastActivity = DateTime.UtcNow;
77+
state.IsRestarting = false;
78+
}
79+
}
80+
}
81+
82+
public void WorkerRestarting(int workerIndex)
83+
{
84+
lock (_lock)
85+
{
86+
if (_workerStates.TryGetValue(workerIndex, out var state))
87+
state.IsRestarting = true;
88+
}
89+
}
90+
91+
public void WorkerComplete(int workerIndex)
92+
{
93+
lock (_lock)
94+
{
95+
if (_workerStates.TryGetValue(workerIndex, out var state))
96+
state.IsComplete = true;
97+
}
4998
}
5099

51100
public void TestStarted(string displayName)
@@ -108,13 +157,6 @@ public void TestCrashed(string displayName)
108157
}
109158
}
110159

111-
public void WorkerRestarted(int remaining)
112-
{
113-
lock (_lock)
114-
{
115-
_running.Clear();
116-
}
117-
}
118160

119161
public IRenderable Render()
120162
{
@@ -142,20 +184,30 @@ public IRenderable Render()
142184
new Markup($"[dim]{elapsed:mm\\:ss}[/] [dim]({rate:F1}/s)[/]")
143185
);
144186

145-
var layout = new Rows(
187+
var layoutItems = new List<IRenderable>
188+
{
146189
grid,
147190
new Text(""),
148191
CreateProgressBar(completed, _total),
149192
new Text(""),
150193
CreateRunningSection()
151-
);
194+
};
195+
196+
// Add worker status bar if multiple workers
197+
if (_workerCount > 1)
198+
{
199+
layoutItems.Add(new Text(""));
200+
layoutItems.Add(CreateWorkerStatusBar());
201+
}
202+
203+
var layout = new Rows(layoutItems);
152204

153205
// Build header: show filter if set, otherwise assembly name
154206
var headerText = !string.IsNullOrEmpty(_filter)
155207
? $"[blue]filter[/] [green]\"{_filter}\"[/]"
156208
: _assemblyName ?? "Test Progress";
157209

158-
var workerText = _workerCount > 1 ? $" [dim]({_workerCount} workers)[/]" : "";
210+
var workerText = _workerCount > 1 ? "" : ""; // Removed from header, shown in bar now
159211

160212
var panel = new Panel(layout)
161213
.Header($"{headerText}{workerText} [blue]({completed}/{_total})[/]")
@@ -225,6 +277,49 @@ private IRenderable CreateRunningSection()
225277
return new Rows(lines);
226278
}
227279

280+
private IRenderable CreateWorkerStatusBar()
281+
{
282+
var parts = new List<string> { "[dim]workers:[/]" };
283+
284+
for (var i = 0; i < _workerCount; i++)
285+
{
286+
if (!_workerStates.TryGetValue(i, out var state))
287+
{
288+
parts.Add("[dim]○[/]");
289+
continue;
290+
}
291+
292+
if (state.IsComplete)
293+
{
294+
parts.Add("[green]✓[/]");
295+
continue;
296+
}
297+
298+
if (state.IsRestarting)
299+
{
300+
parts.Add("[yellow]⏳[/]");
301+
continue;
302+
}
303+
304+
// Calculate health based on time since last activity
305+
var elapsed = (DateTime.UtcNow - state.LastActivity).TotalSeconds;
306+
var ratio = Math.Min(1.0, elapsed / _timeoutSeconds);
307+
308+
// Color gradient: green → yellow → orange → red
309+
var color = ratio switch
310+
{
311+
< 0.25 => "green",
312+
< 0.5 => "yellow",
313+
< 0.75 => "orange3",
314+
_ => "red"
315+
};
316+
317+
parts.Add($"[{color}]●[/]");
318+
}
319+
320+
return new Markup(string.Join(" ", parts));
321+
}
322+
228323
private static string Truncate(string text, int maxLength)
229324
{
230325
if (text.Length <= maxLength) return text;

src/Asynkron.TestRunner/TestRunner.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ private async Task RunWithRecoveryLiveAsync(string assemblyPath, List<string> al
104104
display.SetFilter(_filter);
105105
display.SetAssembly(assemblyPath);
106106
display.SetWorkerCount(_workerCount);
107+
display.SetTimeout(_testTimeoutSeconds);
107108

108109
var failureDetails = new List<(string Name, string Message, string? Stack)>();
109110
var failureLock = new object();
@@ -205,6 +206,8 @@ private async Task RunWorkerBatchAsync(
205206
await foreach (var msg in worker.RunAsync(assemblyPath, testsToRun, _testTimeoutSeconds, cts.Token)
206207
.WithTimeout(TimeSpan.FromSeconds(_testTimeoutSeconds), cts))
207208
{
209+
display.WorkerActivity(workerIndex);
210+
208211
switch (msg)
209212
{
210213
case TestStartedEvent started:
@@ -278,7 +281,7 @@ private async Task RunWorkerBatchAsync(
278281
display.TestHanging(fqn);
279282
}
280283
worker.Kill();
281-
display.WorkerRestarted(pending.Count);
284+
display.WorkerRestarting(workerIndex);
282285
}
283286
catch (WorkerCrashedException)
284287
{
@@ -288,7 +291,7 @@ private async Task RunWorkerBatchAsync(
288291
lock (results) results.Crashed.Add(fqn);
289292
display.TestCrashed(fqn);
290293
}
291-
display.WorkerRestarted(pending.Count);
294+
display.WorkerRestarting(workerIndex);
292295
}
293296
catch (Exception)
294297
{
@@ -303,6 +306,8 @@ private async Task RunWorkerBatchAsync(
303306
lock (results) results.Crashed.Add(fqn);
304307
display.TestCrashed(fqn);
305308
}
309+
310+
display.WorkerComplete(workerIndex);
306311
}
307312

308313
private async Task RunWithRecoveryQuietAsync(string assemblyPath, List<string> allTests, TestResults results)

0 commit comments

Comments
 (0)