Skip to content

Commit 95e2b5e

Browse files
committed
Instrument UI test harness startup
1 parent 0833c9e commit 95e2b5e

5 files changed

Lines changed: 171 additions & 0 deletions

File tree

DotPilot.UITests/BrowserAutomationBootstrap.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ internal static BrowserAutomationSettings Resolve(
6464
IReadOnlyList<string> browserBinaryCandidates,
6565
bool applyEnvironmentVariables = false)
6666
{
67+
HarnessLog.Write("Resolving browser automation settings.");
6768
var browserBinaryPath = ResolveBrowserBinaryPath(environment, browserBinaryCandidates);
6869
var driverPath = ResolveBrowserDriverPath(environment, browserBinaryPath);
6970

@@ -74,6 +75,8 @@ internal static BrowserAutomationSettings Resolve(
7475
SetEnvironmentVariableIfMissing(BrowserDriverEnvironmentVariableName, driverPath, environment);
7576
}
7677

78+
HarnessLog.Write($"Resolved browser binary path '{browserBinaryPath}'.");
79+
HarnessLog.Write($"Resolved browser driver directory '{driverPath}'.");
7780
return new BrowserAutomationSettings(driverPath, browserBinaryPath);
7881
}
7982

@@ -125,13 +128,18 @@ private static string EnsureChromeDriverDownloaded(string browserBinaryPath)
125128
var driverDirectory = Path.Combine(cacheRootPath, $"{ChromeDriverBundleNamePrefix}{driverPlatform}");
126129
var driverExecutablePath = Path.Combine(driverDirectory, GetChromeDriverExecutableFileName());
127130

131+
HarnessLog.Write($"Browser version '{browserVersion}' resolved for '{browserBinaryPath}'.");
132+
HarnessLog.Write($"Matching ChromeDriver version '{driverVersion}' on platform '{driverPlatform}'.");
133+
128134
if (File.Exists(driverExecutablePath))
129135
{
130136
EnsureDriverExecutablePermissions(driverExecutablePath);
137+
HarnessLog.Write($"Reusing cached ChromeDriver at '{driverExecutablePath}'.");
131138
return driverDirectory;
132139
}
133140

134141
Directory.CreateDirectory(cacheRootPath);
142+
HarnessLog.Write($"Downloading ChromeDriver to '{cacheRootPath}'.");
135143
DownloadChromeDriverArchive(driverVersion, driverPlatform, cacheRootPath);
136144
EnsureDriverExecutablePermissions(driverExecutablePath);
137145

@@ -155,9 +163,11 @@ private static void DownloadChromeDriverArchive(string driverVersion, string dri
155163
}
156164

157165
var downloadUrl = BuildChromeDriverDownloadUrl(driverVersion, driverPlatform, archiveName);
166+
HarnessLog.Write($"Fetching ChromeDriver archive '{downloadUrl}'.");
158167
var archiveBytes = GetResponseBytes(downloadUrl, DriverDownloadFailedMessage);
159168
File.WriteAllBytes(archivePath, archiveBytes);
160169
ZipFile.ExtractToDirectory(archivePath, cacheRootPath, overwriteFiles: true);
170+
HarnessLog.Write($"Extracted ChromeDriver archive to '{driverDirectory}'.");
161171
}
162172

163173
private static byte[] GetResponseBytes(string requestUri, string failureMessage)
@@ -287,6 +297,7 @@ private static string ResolveBrowserBinaryPath(
287297
!string.IsNullOrWhiteSpace(configuredPath) &&
288298
File.Exists(configuredPath))
289299
{
300+
HarnessLog.Write($"Using browser binary from environment variable '{environmentVariableName}'.");
290301
return configuredPath;
291302
}
292303
}
@@ -295,6 +306,7 @@ private static string ResolveBrowserBinaryPath(
295306
{
296307
if (File.Exists(candidatePath))
297308
{
309+
HarnessLog.Write($"Using browser binary candidate '{candidatePath}'.");
298310
return candidatePath;
299311
}
300312
}

DotPilot.UITests/BrowserTestHost.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,23 @@ public static void EnsureStarted(string hostUri)
4444
{
4545
if (IsReachable(hostUri))
4646
{
47+
HarnessLog.Write("Browser host is already reachable.");
4748
return;
4849
}
4950

5051
if (_hostProcess is { HasExited: false })
5152
{
53+
HarnessLog.Write("Browser host process already exists. Waiting for readiness.");
5254
WaitForHost(hostUri);
5355
return;
5456
}
5557

5658
var repoRoot = FindRepositoryRoot();
5759
var projectPath = Path.Combine(repoRoot, ProjectRelativePath);
5860

61+
HarnessLog.Write("Building browser host.");
5962
EnsureBuilt(repoRoot, projectPath);
63+
HarnessLog.Write("Starting browser host process.");
6064
StartHostProcess(repoRoot, projectPath);
6165
WaitForHost(hostUri);
6266
}
@@ -78,6 +82,8 @@ private static void EnsureBuilt(string repoRoot, string projectPath)
7882
{
7983
throw new InvalidOperationException($"{BuildFailureMessage} {result.Output}");
8084
}
85+
86+
HarnessLog.Write("Browser host build completed.");
8187
}
8288

8389
private static void StartHostProcess(string repoRoot, string projectPath)
@@ -101,6 +107,7 @@ private static void StartHostProcess(string repoRoot, string projectPath)
101107
_hostProcess.BeginOutputReadLine();
102108
_hostProcess.BeginErrorReadLine();
103109
_startedHost = true;
110+
HarnessLog.Write($"Browser host process started with PID {_hostProcess.Id}.");
104111
}
105112

106113
private static void CaptureOutput(string? line)
@@ -118,6 +125,7 @@ private static void WaitForHost(string hostUri)
118125
{
119126
if (IsReachable(hostUri))
120127
{
128+
HarnessLog.Write("Browser host responded to readiness probe.");
121129
return;
122130
}
123131

@@ -208,6 +216,7 @@ public static void Stop()
208216
{
209217
if (!_startedHost || _hostProcess is null)
210218
{
219+
HarnessLog.Write("Browser host stop requested, but no owned host process is active.");
211220
return;
212221
}
213222

@@ -218,13 +227,16 @@ public static void Stop()
218227

219228
try
220229
{
230+
HarnessLog.Write($"Stopping browser host process {hostProcess.Id}.");
221231
CancelOutputReaders(hostProcess);
222232

223233
if (!hostProcess.HasExited)
224234
{
225235
hostProcess.Kill(entireProcessTree: true);
226236
hostProcess.WaitForExit((int)HostShutdownTimeout.TotalMilliseconds);
227237
}
238+
239+
HarnessLog.Write("Browser host process stopped.");
228240
}
229241
catch
230242
{

DotPilot.UITests/HarnessLog.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace DotPilot.UITests;
2+
3+
internal static class HarnessLog
4+
{
5+
private const string Prefix = "[DotPilot.UITests]";
6+
7+
public static void Write(string message)
8+
{
9+
ArgumentException.ThrowIfNullOrWhiteSpace(message);
10+
11+
Console.WriteLine($"{Prefix} {message}");
12+
}
13+
}

DotPilot.UITests/TestBase.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ static TestBase()
2929
{
3030
if (Constants.CurrentPlatform == Platform.Browser)
3131
{
32+
HarnessLog.Write($"Browser test target URI is '{Constants.WebAssemblyDefaultUri}'.");
33+
HarnessLog.Write($"Browser binary path is '{_browserAutomation!.BrowserBinaryPath}'.");
34+
HarnessLog.Write($"Browser driver directory is '{_browserAutomation.DriverPath}'.");
35+
HarnessLog.Write("Ensuring browser test host is started.");
3236
BrowserTestHost.EnsureStarted(Constants.WebAssemblyDefaultUri);
37+
HarnessLog.Write("Browser test host is reachable.");
3338
}
3439

3540
AppInitializer.TestEnvironment.AndroidAppName = Constants.AndroidAppName;
@@ -61,23 +66,29 @@ private set
6166
[SetUp]
6267
public void SetUpTest()
6368
{
69+
HarnessLog.Write($"Starting setup for '{TestContext.CurrentContext.Test.Name}'.");
6470
App = Constants.CurrentPlatform == Platform.Browser
6571
? EnsureBrowserApp(_browserAutomation!)
6672
: AppInitializer.AttachToApp();
73+
HarnessLog.Write($"Setup completed for '{TestContext.CurrentContext.Test.Name}'.");
6774
}
6875

6976
[TearDown]
7077
public void TearDownTest()
7178
{
79+
HarnessLog.Write($"Starting teardown for '{TestContext.CurrentContext.Test.Name}'.");
7280
if (_app is not null)
7381
{
7482
TakeScreenshot("teardown");
7583
}
84+
85+
HarnessLog.Write($"Teardown completed for '{TestContext.CurrentContext.Test.Name}'.");
7686
}
7787

7888
[OneTimeTearDown]
7989
public void TearDownFixture()
8090
{
91+
HarnessLog.Write("Starting fixture cleanup.");
8192
List<Exception> cleanupFailures = [];
8293

8394
if (_app is not null && !ReferenceEquals(_app, _browserApp))
@@ -115,13 +126,17 @@ public void TearDownFixture()
115126

116127
if (cleanupFailures.Count == 1)
117128
{
129+
HarnessLog.Write("Fixture cleanup failed with a single cleanup exception.");
118130
throw cleanupFailures[0];
119131
}
120132

121133
if (cleanupFailures.Count > 1)
122134
{
135+
HarnessLog.Write("Fixture cleanup failed with multiple cleanup exceptions.");
123136
throw new AggregateException(cleanupFailures);
124137
}
138+
139+
HarnessLog.Write("Fixture cleanup completed.");
125140
}
126141

127142
public FileInfo TakeScreenshot(string stepName)
@@ -175,9 +190,11 @@ private static IApp EnsureBrowserApp(BrowserAutomationSettings browserAutomation
175190
{
176191
if (_browserApp is not null)
177192
{
193+
HarnessLog.Write("Reusing browser app instance.");
178194
return _browserApp;
179195
}
180196

197+
HarnessLog.Write("Starting browser app instance.");
181198
var configurator = Uno.UITest.Selenium.ConfigureApp.WebAssembly
182199
.Uri(new Uri(Constants.WebAssemblyDefaultUri))
183200
.UsingBrowser(Constants.WebAssemblyBrowser.ToString())
@@ -195,6 +212,7 @@ private static IApp EnsureBrowserApp(BrowserAutomationSettings browserAutomation
195212
}
196213

197214
_browserApp = configurator.StartApp();
215+
HarnessLog.Write("Browser app instance started.");
198216
return _browserApp;
199217
}
200218
}
@@ -203,10 +221,13 @@ private static void TryCleanup(Action cleanupAction, string operationName, List<
203221
{
204222
try
205223
{
224+
HarnessLog.Write($"Running cleanup for '{operationName}'.");
206225
BoundedCleanup.Run(cleanupAction, AppCleanupTimeout, operationName);
226+
HarnessLog.Write($"Cleanup completed for '{operationName}'.");
207227
}
208228
catch (Exception exception)
209229
{
230+
HarnessLog.Write($"Cleanup failed for '{operationName}': {exception.Message}");
210231
cleanupFailures.Add(exception);
211232
}
212233
}

ui-tests-ci-hang.plan.md

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# UI Tests CI Hang Plan
2+
3+
## Goal
4+
5+
Fix the `DotPilot.UITests` GitHub Actions execution path so the required UI test job returns a real completed result instead of hanging or being canceled while `Run UI Tests` is still active.
6+
7+
## Scope
8+
9+
### In Scope
10+
11+
- `DotPilot.UITests` harness code and supporting configuration
12+
- GitHub Actions validation behavior needed to expose or validate the harness fix
13+
- Durable notes for any repo-level CI implication discovered during the fix
14+
15+
### Out Of Scope
16+
17+
- New product functionality unrelated to UI test stability
18+
- Weakening, skipping, or conditionally bypassing the UI suite
19+
- Release workflow changes unrelated to the UI test blocker
20+
21+
## Constraints And Risks
22+
23+
- The UI test suite is mandatory and must stay in the normal validation workflow.
24+
- A hang, timeout, or canceled run counts as a failing harness outcome.
25+
- Prefer a deterministic harness fix over workflow-level retries or skip logic.
26+
- Keep the fix aligned with the current browserwasm test-launch path.
27+
28+
## Testing Methodology
29+
30+
- GitHub baseline:
31+
- inspect the active PR run and UI Tests job logs for the actual failure point
32+
- Local validation:
33+
- `dotnet test DotPilot.UITests/DotPilot.UITests.csproj`
34+
- any focused repro command needed to exercise the harness shutdown path
35+
- Broader validation:
36+
- `dotnet test DotPilot.slnx`
37+
- `dotnet format DotPilot.slnx --verify-no-changes`
38+
- Quality bar:
39+
- UI tests must complete with a terminal pass/fail result locally
40+
- the fix must directly address the hang/cancel path instead of hiding it
41+
42+
## Ordered Plan
43+
44+
- [x] Step 1: Capture the current failing GitHub UI test job details and relevant local baseline.
45+
Verification:
46+
- inspect the active PR workflow run and UI Tests job
47+
- run the relevant local UI test command
48+
Done when: the concrete hang symptom and likely failing code path are documented below.
49+
50+
- [x] Step 2: Trace the harness launch and shutdown flow in `DotPilot.UITests` to isolate the blocking operation.
51+
Verification:
52+
- identify the exact code path used during CI teardown or cancellation
53+
- document the likely root cause before editing
54+
Done when: the intended fix path is clear.
55+
56+
- [x] Step 3: Implement the harness fix and any needed regression coverage.
57+
Verification:
58+
- changed code remains within repo maintainability limits
59+
- local UI test execution still returns a real result
60+
Done when: the blocking behavior is removed from the identified path.
61+
62+
- [ ] Step 4: Run final validation and record outcomes.
63+
Verification:
64+
- `dotnet test DotPilot.UITests/DotPilot.UITests.csproj`
65+
- `dotnet test DotPilot.slnx`
66+
- `dotnet format DotPilot.slnx --verify-no-changes`
67+
- GitHub Actions `UI Test Suite` returns a real completed result with the instrumented harness logs available
68+
Done when: required checks are green locally and the GitHub UI test job returns a real completed result or a concrete failing signal.
69+
70+
## Full-Test Baseline Step
71+
72+
- [x] Run the relevant baseline commands after the plan is prepared:
73+
- inspect the active GitHub UI Tests job
74+
- `dotnet test DotPilot.UITests/DotPilot.UITests.csproj`
75+
Done when: the failing symptom is recorded below.
76+
77+
## Failing Tests And Checks Tracker
78+
79+
- [x] `GitHub Actions job: UI Tests`
80+
Failure symptom: the PR validation run enters `Run UI Tests`, then fails to produce a terminal result and may later surface `Attempting to cancel the build... Error: The operation was canceled.`
81+
Suspected cause: the UI test harness had unbounded teardown calls for `_browserApp.Dispose()` and `BrowserTestHost.Stop()`, so a Windows-specific cleanup hang could leave the job running until GitHub eventually canceled it.
82+
Intended fix path: bound teardown cleanup with explicit timeouts and convert cleanup hangs into deterministic test failures instead of infinite jobs.
83+
Status: bounded cleanup is fixed locally; CI still needs instrumented logs to confirm whether any remaining hang is in driver/bootstrap, browser startup, host readiness, or teardown.
84+
85+
## Baseline Notes
86+
87+
- PR `#10` currently points at GitHub Actions run `23041255695`, where `Build`, `Unit Tests`, and `Coverage` complete successfully but `UI Tests` stays in progress inside `Run UI Tests`.
88+
- The affected job is `66920240879`, which started `Run UI Tests` at `2026-03-13T07:50:38Z` and did not produce a terminal result while the other validation jobs finished.
89+
- A later PR validation run, `23043020124`, shows the same shape so far: `Quality Gate`, `Unit Test Suite`, and `Coverage Suite` completed while `UI Test Suite` job `66925873162` remained `in_progress` inside `Run UI Tests` after starting at `2026-03-13T08:47:49Z`.
90+
- Local `dotnet test DotPilot.UITests/DotPilot.UITests.csproj` passed before the fix, which indicates the main failure mode is CI-specific hang behavior rather than a consistently failing test case.
91+
- Local `dotnet test ./DotPilot.UITests/DotPilot.UITests.csproj --logger GitHubActions --blame-crash` also passed before the fix on macOS, which points further toward a Windows-specific teardown or process-cleanup issue.
92+
93+
## Validation Notes
94+
95+
- Added bounded cleanup execution in `DotPilot.UITests` so teardown now fails fast if app disposal or browser-host shutdown hangs.
96+
- Added focused regression tests for the bounded-cleanup helper to prove success, exception, and timeout behavior.
97+
- Added harness logging around browser binary resolution, ChromeDriver resolution, host startup, setup, and cleanup so the next GitHub run exposes the exact blocking stage instead of sitting silent inside `Run UI Tests`.
98+
- `dotnet test DotPilot.UITests/DotPilot.UITests.csproj --filter BoundedCleanupTests` passed.
99+
- `dotnet test DotPilot.UITests/DotPilot.UITests.csproj` passed with `9` tests green.
100+
- `dotnet test ./DotPilot.UITests/DotPilot.UITests.csproj --logger GitHubActions --blame-crash` passed with `9` tests green.
101+
- `dotnet test DotPilot.slnx` passed.
102+
- `dotnet format DotPilot.slnx --verify-no-changes` passed.
103+
104+
## Final Validation Skills
105+
106+
1. `gh-fix-ci`
107+
Reason: inspect the failing GitHub Actions run and extract the real failure signal.
108+
109+
2. `mcaf-testing`
110+
Reason: keep the UI suite mandatory and verified through real execution.
111+
112+
3. `mcaf-dotnet`
113+
Reason: ensure the harness fix stays aligned with the repo’s .NET toolchain and quality path.

0 commit comments

Comments
 (0)