Skip to content

Commit 12f6669

Browse files
Franz-Fischbachalehander92
authored andcommitted
fix(ui-tests): fix test planner
1 parent f2082e2 commit 12f6669

File tree

10 files changed

+142
-35
lines changed

10 files changed

+142
-35
lines changed

ui-tests/Application/UiTestApplication.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ public async Task<int> RunAsync()
2929
{
3030
using var scope = _logger.BeginScope("ui-tests");
3131
_processLifecycle.ReportProcessCounts("pre-run");
32-
_processLifecycle.KillProcesses("pre-run cleanup");
33-
_processLifecycle.ReportProcessCounts("post-cleanup snapshot");
3432
var cancellationToken = _lifetime.ApplicationStopping;
3533
var exitCode = await _pipeline.ExecuteAsync(cancellationToken);
3634
_processLifecycle.ReportProcessCounts("post-run");

ui-tests/Execution/ElectronTestSessionExecutor.cs

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,21 @@ internal sealed class ElectronTestSessionExecutor : ITestSessionExecutor
1515
private readonly ICodetracerLauncher _launcher;
1616
private readonly IMonitorLayoutService _monitorLayoutService;
1717
private readonly AppSettings _settings;
18+
private readonly IProcessLifecycleManager _processLifecycle;
1819
private readonly ILogger<ElectronTestSessionExecutor> _logger;
1920

2021
public ElectronTestSessionExecutor(
2122
ICodetracerLauncher launcher,
2223
IMonitorLayoutService monitorLayoutService,
2324
IOptions<AppSettings> settings,
24-
ILogger<ElectronTestSessionExecutor> logger)
25+
ILogger<ElectronTestSessionExecutor> logger,
26+
IProcessLifecycleManager processLifecycle)
2527
{
2628
_launcher = launcher;
2729
_monitorLayoutService = monitorLayoutService;
2830
_settings = settings.Value;
2931
_logger = logger;
32+
_processLifecycle = processLifecycle;
3033
}
3134

3235
public TestMode Mode => TestMode.Electron;
@@ -87,12 +90,39 @@ private async Task<CodeTracerSession> LaunchElectronAsync(int traceId, int port,
8790
info.EnvironmentVariables.Add("CODETRACER_START_INDEX", "1");
8891

8992
var process = Process.Start(info) ?? throw new InvalidOperationException("Failed to start CodeTracer Electron process.");
90-
await WaitForCdpAsync(port, TimeSpan.FromSeconds(_settings.Electron.CdpStartupTimeoutSeconds), cancellationToken);
93+
var label = $"electron:{traceId}";
94+
_processLifecycle.RegisterProcess(process, label);
95+
try
96+
{
97+
await WaitForCdpAsync(port, TimeSpan.FromSeconds(_settings.Electron.CdpStartupTimeoutSeconds), cancellationToken);
9198

92-
var playwright = await Playwright.CreateAsync();
93-
var browser = await playwright.Chromium.ConnectOverCDPAsync($"http://localhost:{port}", new() { Timeout = _settings.Electron.CdpStartupTimeoutSeconds * 1000 });
99+
var playwright = await Playwright.CreateAsync();
100+
var browser = await playwright.Chromium.ConnectOverCDPAsync($"http://localhost:{port}", new() { Timeout = _settings.Electron.CdpStartupTimeoutSeconds * 1000 });
94101

95-
return new CodeTracerSession(process, browser, playwright);
102+
return new CodeTracerSession(process, browser, playwright, _processLifecycle, label);
103+
}
104+
catch
105+
{
106+
_processLifecycle.UnregisterProcess(process.Id);
107+
try
108+
{
109+
if (!process.HasExited)
110+
{
111+
process.Kill(entireProcessTree: true);
112+
process.WaitForExit(5000);
113+
}
114+
}
115+
catch
116+
{
117+
// ignore cleanup failures
118+
}
119+
finally
120+
{
121+
process.Dispose();
122+
}
123+
124+
throw;
125+
}
96126
}
97127

98128
private static async Task WaitForCdpAsync(int port, TimeSpan timeout, CancellationToken cancellationToken)

ui-tests/Execution/TestPlanner.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ public IReadOnlyList<TestPlanEntry> BuildPlan()
8383
continue;
8484
}
8585

86-
foreach (var mode in modes)
86+
var scenarioModes = ResolveScenarioModes(scenario, modes);
87+
foreach (var mode in scenarioModes)
8788
{
8889
plan.Add(new TestPlanEntry(descriptor, scenario, mode));
8990
}
@@ -116,4 +117,14 @@ private static IReadOnlyList<TestMode> ResolveExecutionModes(RunnerSettings runn
116117

117118
return new[] { TestMode.Electron, TestMode.Web };
118119
}
120+
121+
private static IReadOnlyList<TestMode> ResolveScenarioModes(ScenarioSettings scenario, IReadOnlyList<TestMode> runnerModes)
122+
{
123+
if (scenario.Id.StartsWith("auto-", StringComparison.OrdinalIgnoreCase))
124+
{
125+
return runnerModes;
126+
}
127+
128+
return new[] { scenario.Mode };
129+
}
119130
}

ui-tests/Execution/TestRegistry.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Linq;
33
using UiTests.Tests;
44
using UiTests.Tests.ProgramAgnostic;
5+
using UiTests.Utils;
56

67
namespace UiTests.Execution;
78

@@ -178,9 +179,21 @@ public bool TryResolve(string identifier, out UiTestDescriptor descriptor)
178179

179180
private void Register(UiTestDescriptor descriptor)
180181
{
181-
if (!_tests.TryAdd(descriptor.Id, descriptor))
182+
var wrapped = WrapWithCompletionLog(descriptor);
183+
if (!_tests.TryAdd(wrapped.Id, wrapped))
182184
{
183185
throw new InvalidOperationException($"Duplicate test identifier registered: {descriptor.Id}");
184186
}
185187
}
188+
189+
private static UiTestDescriptor WrapWithCompletionLog(UiTestDescriptor descriptor)
190+
{
191+
async Task Handler(TestExecutionContext context)
192+
{
193+
await descriptor.Handler(context);
194+
DebugLogger.Log($"{descriptor.Id}: completed");
195+
}
196+
197+
return descriptor with { Handler = Handler };
198+
}
186199
}

ui-tests/Execution/WebTestSessionExecutor.cs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,24 @@ internal sealed class WebTestSessionExecutor : ITestSessionExecutor
1818
private readonly IMonitorLayoutService _monitorLayoutService;
1919
private readonly AppSettings _settings;
2020
private readonly ILogger<WebTestSessionExecutor> _logger;
21+
private readonly IProcessLifecycleManager _processLifecycle;
2122

2223
public WebTestSessionExecutor(
2324
ICodetracerLauncher launcher,
2425
ICtHostLauncher hostLauncher,
2526
IPortAllocator portAllocator,
2627
IMonitorLayoutService monitorLayoutService,
2728
IOptions<AppSettings> settings,
28-
ILogger<WebTestSessionExecutor> logger)
29+
ILogger<WebTestSessionExecutor> logger,
30+
IProcessLifecycleManager processLifecycle)
2931
{
3032
_launcher = launcher;
3133
_hostLauncher = hostLauncher;
3234
_portAllocator = portAllocator;
3335
_monitorLayoutService = monitorLayoutService;
3436
_settings = settings.Value;
3537
_logger = logger;
38+
_processLifecycle = processLifecycle;
3639
}
3740

3841
public TestMode Mode => TestMode.Web;
@@ -53,18 +56,26 @@ public async Task ExecuteAsync(TestPlanEntry entry, CancellationToken cancellati
5356

5457
var label = $"{entry.Scenario.Id}-{entry.Mode}";
5558
var hostProcess = _hostLauncher.StartHostProcess(httpPort, backendPort, frontendPort, tracePath, label);
56-
await using var session = await CreateWebSessionAsync(hostProcess, httpPort, entry, cancellationToken);
59+
_processLifecycle.RegisterProcess(hostProcess, $"ct-host:{label}");
60+
await using var session = await CreateWebSessionAsync(hostProcess, httpPort, entry, label, cancellationToken);
61+
try
62+
{
63+
64+
if (entry.Scenario.DelaySeconds > 0)
65+
{
66+
await Task.Delay(TimeSpan.FromSeconds(entry.Scenario.DelaySeconds), cancellationToken);
67+
}
5768

58-
if (entry.Scenario.DelaySeconds > 0)
69+
var context = new TestExecutionContext(entry.Scenario, entry.Mode, session.Page, cancellationToken);
70+
await entry.Test.Handler(context);
71+
}
72+
finally
5973
{
60-
await Task.Delay(TimeSpan.FromSeconds(entry.Scenario.DelaySeconds), cancellationToken);
74+
_processLifecycle.UnregisterProcess(hostProcess.Id);
6175
}
62-
63-
var context = new TestExecutionContext(entry.Scenario, entry.Mode, session.Page, cancellationToken);
64-
await entry.Test.Handler(context);
6576
}
6677

67-
private async Task<WebTestSession> CreateWebSessionAsync(Process hostProcess, int port, TestPlanEntry entry, CancellationToken cancellationToken)
78+
private async Task<WebTestSession> CreateWebSessionAsync(Process hostProcess, int port, TestPlanEntry entry, string label, CancellationToken cancellationToken)
6879
{
6980
await _hostLauncher.WaitForServerAsync(port, TimeSpan.FromSeconds(_settings.Web.HostStartupTimeoutSeconds), entry.Scenario.Id, cancellationToken);
7081

@@ -114,7 +125,7 @@ private async Task<WebTestSession> CreateWebSessionAsync(Process hostProcess, in
114125
_logger.LogDebug("[{Scenario}] Window resize script could not adjust browser bounds.", entry.Scenario.Id);
115126
}
116127

117-
return new WebTestSession(hostProcess, playwright, browser, context, page);
128+
return new WebTestSession(hostProcess, playwright, browser, context, page, _processLifecycle, $"ct-host:{label}");
118129
}
119130

120131
}

ui-tests/Helpers/CodeTracerSession.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Diagnostics;
33
using System.Threading.Tasks;
44
using Microsoft.Playwright;
5+
using UiTests.Infrastructure;
56

67
namespace UiTests.Helpers;
78

@@ -13,15 +14,20 @@ public sealed class CodeTracerSession : IAsyncDisposable
1314
{
1415
private readonly Process _process;
1516
private readonly IPlaywright _playwright;
17+
private readonly IProcessLifecycleManager? _processLifecycle;
18+
private readonly string _processLabel;
1619
private bool _disposed;
1720

1821
public IBrowser Browser { get; }
1922

20-
internal CodeTracerSession(Process process, IBrowser browser, IPlaywright playwright)
23+
internal CodeTracerSession(Process process, IBrowser browser, IPlaywright playwright, IProcessLifecycleManager? processLifecycle = null, string? processLabel = null)
2124
{
2225
_process = process;
2326
Browser = browser;
2427
_playwright = playwright;
28+
_processLifecycle = processLifecycle;
29+
_processLabel = processLabel ?? $"electron:{process.Id}";
30+
_processLifecycle?.RegisterProcess(_process, _processLabel);
2531
}
2632

2733
public async ValueTask DisposeAsync()
@@ -66,6 +72,7 @@ public async ValueTask DisposeAsync()
6672
{
6773
if (_process.HasExited)
6874
{
75+
_processLifecycle?.UnregisterProcess(_process.Id);
6976
return;
7077
}
7178

@@ -88,6 +95,7 @@ public async ValueTask DisposeAsync()
8895
}
8996
finally
9097
{
98+
_processLifecycle?.UnregisterProcess(_process.Id);
9199
_process.Dispose();
92100
}
93101
}

ui-tests/Helpers/WebTestSession.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
using System.Diagnostics;
22
using Microsoft.Playwright;
3+
using UiTests.Infrastructure;
34

45
namespace UiTests.Helpers;
56

67
public sealed class WebTestSession : IAsyncDisposable
78
{
89
private readonly Process _hostProcess;
10+
private readonly IProcessLifecycleManager? _processLifecycle;
911
private bool _disposed;
1012

11-
public WebTestSession(Process hostProcess, IPlaywright playwright, IBrowser browser, IBrowserContext context, IPage page)
13+
internal WebTestSession(Process hostProcess, IPlaywright playwright, IBrowser browser, IBrowserContext context, IPage page, IProcessLifecycleManager? processLifecycle = null, string? processLabel = null)
1214
{
1315
_hostProcess = hostProcess;
16+
_processLifecycle = processLifecycle;
1417
Playwright = playwright;
1518
Browser = browser;
1619
Context = context;
1720
Page = page;
21+
_processLifecycle?.RegisterProcess(_hostProcess, processLabel ?? $"ct-host:{hostProcess.Id}");
1822
}
1923

2024
public IPlaywright Playwright { get; }
@@ -99,6 +103,7 @@ public async ValueTask DisposeAsync()
99103
}
100104
finally
101105
{
106+
_processLifecycle?.UnregisterProcess(_hostProcess.Id);
102107
_hostProcess.Dispose();
103108
}
104109
}

ui-tests/Infrastructure/ProcessLifecycleManager.cs

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Concurrent;
12
using System.Diagnostics;
23
using Microsoft.Extensions.Logging;
34

@@ -7,12 +8,15 @@ internal interface IProcessLifecycleManager
78
{
89
void ReportProcessCounts(string scope);
910
void KillProcesses(string reason);
11+
void RegisterProcess(Process process, string label);
12+
void UnregisterProcess(int processId);
1013
}
1114

1215
internal sealed class ProcessLifecycleManager : IProcessLifecycleManager
1316
{
1417
private readonly ILogger<ProcessLifecycleManager> _logger;
1518
private static readonly string[] ProcessNames = { "ct", "electron", "backend-manager", "virtualization-layers", "node" };
19+
private readonly ConcurrentDictionary<int, string> _registeredProcesses = new();
1620

1721
public ProcessLifecycleManager(ILogger<ProcessLifecycleManager> logger)
1822
{
@@ -37,25 +41,53 @@ public void ReportProcessCounts(string scope)
3741

3842
public void KillProcesses(string reason)
3943
{
40-
foreach (var name in ProcessNames)
44+
foreach (var entry in _registeredProcesses.ToArray())
4145
{
42-
foreach (var process in Process.GetProcessesByName(name))
46+
var pid = entry.Key;
47+
try
4348
{
49+
Process? process = null;
4450
try
4551
{
46-
_logger.LogInformation("[{Reason}] Killing process {Name} (PID {Pid}).", reason, name, process.Id);
47-
process.Kill(entireProcessTree: true);
48-
process.WaitForExit(5000);
49-
}
50-
catch (Exception ex)
51-
{
52-
_logger.LogDebug(ex, "[{Reason}] Failed to terminate process {Name} (PID {Pid}).", reason, name, process.Id);
52+
process = Process.GetProcessById(pid);
5353
}
54-
finally
54+
catch (ArgumentException)
5555
{
56-
process.Dispose();
56+
_logger.LogDebug("[{Reason}] Registered process (PID {Pid}) already exited.", reason, pid);
57+
continue;
5758
}
59+
60+
_logger.LogInformation("[{Reason}] Killing process {Label} (PID {Pid}).", reason, entry.Value, pid);
61+
process.Kill(entireProcessTree: true);
62+
process.WaitForExit(5000);
63+
}
64+
catch (Exception ex)
65+
{
66+
_logger.LogDebug(ex, "[{Reason}] Failed to terminate registered process (PID {Pid}).", reason, pid);
67+
}
68+
finally
69+
{
70+
_registeredProcesses.TryRemove(pid, out _);
5871
}
5972
}
6073
}
74+
75+
public void RegisterProcess(Process process, string label)
76+
{
77+
if (process is null)
78+
{
79+
return;
80+
}
81+
82+
var name = string.IsNullOrWhiteSpace(label) ? process.ProcessName : label;
83+
if (_registeredProcesses.TryAdd(process.Id, name))
84+
{
85+
_logger.LogDebug("Registered process {Label} (PID {Pid}) for cleanup.", name, process.Id);
86+
}
87+
}
88+
89+
public void UnregisterProcess(int processId)
90+
{
91+
_registeredProcesses.TryRemove(processId, out _);
92+
}
6193
}

ui-tests/Tests/ProgramSpecific/NoirSpaceShipTests.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -895,8 +895,7 @@ public static async Task JumpToAllEvents(IPage page)
895895
throw new FailedTestException("Event log did not render any events.");
896896
}
897897

898-
var limit = Math.Min(events.Count, 10);
899-
for (var i = 0; i < limit; i++)
898+
for (var i = 0; i < events.Count; i++)
900899
{
901900
var row = events[i];
902901
await row.ClickAsync();

ui-tests/appsettings.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@
106106
},
107107
"flaky-tests": {
108108
"Tests": [
109-
"NoirSpaceShip.LoopIterationSliderTracksRemainingShield",
110109
"NoirSpaceShip.TraceLogRecordsDamageRegeneration",
111110
"NoirSpaceShip.RemainingShieldHistoryChronology",
112111
"NoirSpaceShip.ScratchpadCompareIterations",
@@ -119,7 +118,8 @@
119118
"NoirSpaceShip.CallTraceContextMenuOptions",
120119
"NoirSpaceShip.FlowContextMenuOptions",
121120
"NoirSpaceShip.TraceLogContextMenuOptions",
122-
"NoirSpaceShip.ValueHistoryContextMenuOptions"
121+
"NoirSpaceShip.ValueHistoryContextMenuOptions",
122+
"NoirSpaceShip.LoopIterationSliderTracksRemainingShield"
123123
]
124124
}
125125
},

0 commit comments

Comments
 (0)