Skip to content

Commit c9d3e77

Browse files
Franz-Fischbachalehander92
authored andcommitted
feat(ui-tests): improve logging
1 parent 12f6669 commit c9d3e77

40 files changed

+1859
-81
lines changed

.agents/codebase-insights.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@
4141
- Electron and Web builds use different keyboard shortcuts for command palette and Monaco actions; until we emit platform-aware chords, keep those program-agnostic tests commented out in `Execution/TestRegistry`.
4242
- Call trace navigation currently requires expanding the parent entry; even with expanded children the UI may ignore single clicks, so tests now fall back to double-clicks/forced clicks and still rely on Monaco updates—further instrumentation of the renderer may be needed.
4343
- Use `UiTests.Utils.DebugLogger` (default `ui-tests/bin/Debug/net8.0/ui-tests-debug.log`, override with `UITESTS_DEBUG_LOG`) to trace retry loops and click attempts—`RetryHelpers` and key page objects now emit detailed timestamps.
44+
- Scenario configs now support `\"verboseLogging\": true` to opt-in to DebugLogger + console chatter; otherwise the harness runs quietly and only prints the final color-coded summary unless a test fails. Runner-level `Runner.VerboseConsole` (toggle via `--verbose-console`) also enables verbose lifecycle logging globally.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
using System.Diagnostics;
2+
using System.Globalization;
3+
using System.Net.Http;
4+
5+
namespace UiTestsPlayground.Helpers;
6+
7+
/// <summary>
8+
/// Encapsulates the logic for launching and monitoring <c>ct host</c> processes used by the browser-based UI tests.
9+
/// </summary>
10+
internal static class CtHostLauncher
11+
{
12+
/// <summary>
13+
/// Starts a <c>ct host</c> process that serves the CodeTracer UI over HTTP and WebSockets.
14+
/// </summary>
15+
public static Process StartHostProcess(int port, int backendPort, int frontendPort, string tracePath, string label)
16+
{
17+
var psi = new ProcessStartInfo(CodetracerLauncher.CtPath)
18+
{
19+
WorkingDirectory = CodetracerLauncher.CtInstallDir,
20+
UseShellExecute = false,
21+
RedirectStandardOutput = true,
22+
RedirectStandardError = true,
23+
};
24+
25+
psi.ArgumentList.Add("host");
26+
psi.ArgumentList.Add($"--port={port.ToString(CultureInfo.InvariantCulture)}");
27+
psi.ArgumentList.Add($"--backend-socket-port={backendPort.ToString(CultureInfo.InvariantCulture)}");
28+
psi.ArgumentList.Add($"--frontend-socket={frontendPort.ToString(CultureInfo.InvariantCulture)}");
29+
psi.ArgumentList.Add(tracePath);
30+
31+
var process = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start ct host.");
32+
33+
_ = Task.Run(async () =>
34+
{
35+
try
36+
{
37+
string? line;
38+
while ((line = await process.StandardOutput.ReadLineAsync()) is not null)
39+
{
40+
Console.WriteLine($"[ct host:{label}] {line}");
41+
}
42+
}
43+
catch
44+
{
45+
// Ignore logging failures - the process lifetime is monitored elsewhere.
46+
}
47+
});
48+
49+
_ = Task.Run(async () =>
50+
{
51+
try
52+
{
53+
string? line;
54+
while ((line = await process.StandardError.ReadLineAsync()) is not null)
55+
{
56+
Console.Error.WriteLine($"[ct host:{label}] {line}");
57+
}
58+
}
59+
catch
60+
{
61+
// Ignore logging failures.
62+
}
63+
});
64+
65+
return process;
66+
}
67+
68+
/// <summary>
69+
/// Polls the host HTTP endpoint until it responds successfully or the timeout expires.
70+
/// </summary>
71+
public static async Task WaitForServerAsync(int port, TimeSpan timeout, string label)
72+
{
73+
using var client = new HttpClient();
74+
var deadline = DateTime.UtcNow + timeout;
75+
while (DateTime.UtcNow < deadline)
76+
{
77+
try
78+
{
79+
using var response = await client.GetAsync($"http://localhost:{port}");
80+
if (response.IsSuccessStatusCode)
81+
{
82+
return;
83+
}
84+
}
85+
catch
86+
{
87+
// keep retrying
88+
}
89+
90+
await Task.Delay(250);
91+
}
92+
93+
throw new TimeoutException($"[{label}] ct host did not become ready on port {port} within {timeout.TotalSeconds} seconds.");
94+
}
95+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
using System.Diagnostics;
2+
using System.Globalization;
3+
using System.Text.RegularExpressions;
4+
5+
namespace UiTestsPlayground.Helpers;
6+
7+
/// <summary>
8+
/// Utilities for discovering monitor layouts and translating them into Playwright launch arguments.
9+
/// </summary>
10+
internal static class MonitorUtilities
11+
{
12+
internal readonly record struct MonitorInfo(string Name, int Width, int Height, int X, int Y, bool IsPrimary);
13+
14+
/// <summary>
15+
/// Retrieves connected monitor metadata via <c>xrandr --query</c>. Returns an empty collection when
16+
/// the command is unavailable or fails.
17+
/// </summary>
18+
public static IReadOnlyList<MonitorInfo> DetectMonitors()
19+
{
20+
try
21+
{
22+
var psi = new ProcessStartInfo("bash")
23+
{
24+
RedirectStandardOutput = true,
25+
RedirectStandardError = true,
26+
UseShellExecute = false
27+
};
28+
psi.ArgumentList.Add("-lc");
29+
psi.ArgumentList.Add("xrandr --query");
30+
31+
using var process = Process.Start(psi);
32+
if (process is null)
33+
{
34+
return Array.Empty<MonitorInfo>();
35+
}
36+
37+
var output = process.StandardOutput.ReadToEnd();
38+
process.WaitForExit(2000);
39+
40+
var monitors = new List<MonitorInfo>();
41+
foreach (var line in output.Split('\n', StringSplitOptions.RemoveEmptyEntries))
42+
{
43+
if (!line.Contains(" connected", StringComparison.Ordinal))
44+
{
45+
continue;
46+
}
47+
48+
var parts = line.Split(" connected", 2, StringSplitOptions.TrimEntries);
49+
if (parts.Length < 2)
50+
{
51+
continue;
52+
}
53+
54+
var name = parts[0].Trim();
55+
var details = parts[1];
56+
var isPrimary = details.Contains("primary", StringComparison.OrdinalIgnoreCase);
57+
58+
var match = Regex.Match(details, @"(?<width>\d+)x(?<height>\d+)\+(?<x>-?\d+)\+(?<y>-?\d+)");
59+
if (!match.Success)
60+
{
61+
continue;
62+
}
63+
64+
var width = int.Parse(match.Groups["width"].Value, CultureInfo.InvariantCulture);
65+
var height = int.Parse(match.Groups["height"].Value, CultureInfo.InvariantCulture);
66+
var x = int.Parse(match.Groups["x"].Value, CultureInfo.InvariantCulture);
67+
var y = int.Parse(match.Groups["y"].Value, CultureInfo.InvariantCulture);
68+
69+
monitors.Add(new MonitorInfo(name, width, height, x, y, isPrimary));
70+
}
71+
72+
return monitors;
73+
}
74+
catch
75+
{
76+
return Array.Empty<MonitorInfo>();
77+
}
78+
}
79+
80+
/// <summary>
81+
/// Constructs Chromium command-line parameters that control window position and size. The method
82+
/// prefers explicit environment overrides but falls back to the detected monitor geometry so each
83+
/// browser instance occupies a deterministic area of the desktop.
84+
/// </summary>
85+
public static List<string> BuildBrowserLaunchArgs(string? positionOverride, string? sizeOverride, MonitorInfo? monitor)
86+
{
87+
var args = new List<string> { "--start-maximized" };
88+
89+
int? posX = null, posY = null, width = null, height = null;
90+
91+
if (!TryParsePair(positionOverride, out posX, out posY) && monitor.HasValue)
92+
{
93+
posX = monitor.Value.X;
94+
posY = monitor.Value.Y;
95+
}
96+
97+
if (!TryParsePair(sizeOverride, out width, out height) && monitor.HasValue)
98+
{
99+
width = monitor.Value.Width;
100+
height = monitor.Value.Height;
101+
}
102+
103+
if (posX.HasValue && posY.HasValue)
104+
{
105+
args.Add($"--window-position={posX.Value},{posY.Value}");
106+
}
107+
108+
if (width.HasValue && height.HasValue)
109+
{
110+
args.Add($"--window-size={width.Value},{height.Value}");
111+
}
112+
113+
return args;
114+
}
115+
116+
/// <summary>
117+
/// Attempts to parse a <c>"number,number"</c> string (e.g. axis or width/height) used for positioning
118+
/// browser windows. The method is intentionally tolerant: it returns <c>false</c> when parsing fails and
119+
/// leaves the nullable out parameters unset so the caller can fall back to monitor defaults.
120+
/// </summary>
121+
private static bool TryParsePair(string? value, out int? first, out int? second)
122+
{
123+
first = null;
124+
second = null;
125+
if (string.IsNullOrWhiteSpace(value))
126+
{
127+
return false;
128+
}
129+
130+
var parts = value.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
131+
if (parts.Length != 2)
132+
{
133+
return false;
134+
}
135+
136+
if (int.TryParse(parts[0], out var firstValue) && int.TryParse(parts[1], out var secondValue))
137+
{
138+
first = firstValue;
139+
second = secondValue;
140+
return true;
141+
}
142+
143+
return false;
144+
}
145+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System.Net;
2+
using System.Net.Sockets;
3+
4+
namespace UiTestsPlayground.Helpers;
5+
6+
/// <summary>
7+
/// Networking helpers for reserving local TCP ports when orchestrating multiple CodeTracer instances.
8+
/// </summary>
9+
internal static class NetworkUtilities
10+
{
11+
/// <summary>
12+
/// Reserves a free TCP port on localhost and returns it. The listener is closed immediately after
13+
/// discovery, so callers should still prepare for the rare case where the port is claimed by another
14+
/// process before use.
15+
/// </summary>
16+
public static int GetFreeTcpPort()
17+
{
18+
using var listener = new TcpListener(IPAddress.Loopback, 0);
19+
listener.Start();
20+
int port = ((IPEndPoint)listener.LocalEndpoint).Port;
21+
listener.Stop();
22+
return port;
23+
}
24+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using System.Diagnostics;
2+
3+
namespace UiTestsPlayground.Helpers;
4+
5+
/// <summary>
6+
/// Provides helpers for inspecting and terminating stray CodeTracer processes between test runs.
7+
/// </summary>
8+
internal static class ProcessUtilities
9+
{
10+
private static readonly string[] ProcessNames =
11+
{ "ct", "electron", "backend-manager", "virtualization-layers", "node" };
12+
13+
/// <summary>
14+
/// Logs the number of running processes that could interfere with UI test runs.
15+
/// </summary>
16+
public static void ReportProcessCounts()
17+
{
18+
foreach (var name in ProcessNames)
19+
{
20+
var count = Process.GetProcessesByName(name).Length;
21+
Console.WriteLine($"Process '{name}': {count} instance(s).");
22+
}
23+
}
24+
25+
/// <summary>
26+
/// Forcefully terminates lingering CodeTracer related processes to guarantee a clean slate
27+
/// before or after executing UI scenarios.
28+
/// </summary>
29+
/// <param name="reason">Context string used to annotate the console output.</param>
30+
public static void KillProcesses(string reason)
31+
{
32+
foreach (var name in ProcessNames)
33+
{
34+
foreach (var process in Process.GetProcessesByName(name))
35+
{
36+
try
37+
{
38+
Console.WriteLine($"[{reason}] Killing process {name} (PID {process.Id}).");
39+
process.Kill(entireProcessTree: true);
40+
process.WaitForExit(5000);
41+
}
42+
catch
43+
{
44+
// Ignore failures while best-effort killing processes.
45+
}
46+
finally
47+
{
48+
process.Dispose();
49+
}
50+
}
51+
}
52+
}
53+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Microsoft Visual Studio Solution File, Format Version 12.00
2+
# Visual Studio Version 17
3+
VisualStudioVersion = 17.5.2.0
4+
MinimumVisualStudioVersion = 10.0.40219.1
5+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Playground", "Playground.csproj", "{6A9A0FF1-B1C7-B473-2247-6E2812719A3D}"
6+
EndProject
7+
Global
8+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
9+
Debug|Any CPU = Debug|Any CPU
10+
Release|Any CPU = Release|Any CPU
11+
EndGlobalSection
12+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
13+
{6A9A0FF1-B1C7-B473-2247-6E2812719A3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
14+
{6A9A0FF1-B1C7-B473-2247-6E2812719A3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
15+
{6A9A0FF1-B1C7-B473-2247-6E2812719A3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
16+
{6A9A0FF1-B1C7-B473-2247-6E2812719A3D}.Release|Any CPU.Build.0 = Release|Any CPU
17+
EndGlobalSection
18+
GlobalSection(SolutionProperties) = preSolution
19+
HideSolutionNode = FALSE
20+
EndGlobalSection
21+
GlobalSection(ExtensibilityGlobals) = postSolution
22+
SolutionGuid = {2591002F-6A3B-47AC-A10A-86C90F29E2CE}
23+
EndGlobalSection
24+
EndGlobal

0 commit comments

Comments
 (0)