Test suite: add --config-dir isolation, --smoke-test alias, and a cold-start/close gate step

Adopted from Andre's RemSound-smoke-test-agent-brief.md - the gaps our pack didn't
already cover:

- --config-dir <folder>: redirect ALL user state (config, profiles, logs, cue
  sounds) to an explicit folder for this process only, applied at the very start of
  Program.Main before the layout migration runs. Lets a test exercise a real build
  without touching the user's live settings (the brief's safety rule 1). Works with
  every command. AppConfig gains SetUserDataDirectoryOverride / an override on
  UserDataDirectory; CommandLine.TryGetConfigDir parses it early.
- --smoke-test / --smoketest: alias for --selftest, matching the brief's vocabulary.
- run-tests.ps1: a cold-start + clean-close smoke (brief baseline steps 3-4) -
  launches the GUI minimized against an isolated --config-dir, confirms it stays up,
  that it used the isolated folder (real settings untouched), and that --close shuts
  it down with no orphan process. SKIPs cleanly if a RemSound instance is already
  running (machine-wide single-instance lock).

Manual + --help updated for both switches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-06-12 23:12:22 +01:00
co-authored by Claude Opus 4.8
parent 141c5e8ce1
commit 832ed40bf7
6 changed files with 93 additions and 5 deletions
+3 -1
View File
@@ -1097,7 +1097,8 @@ Option| What it does
`--help` or `-h`| Lists every option, the same as this section in short form.
`--version`| Prints which version of RemSound is installed, for example “RemSound 3.9”.
`--devices`| Lists every microphone and line-in, every speaker and headphone output, and every ASIO driver on the machine — each with its sample rate, channel count and the exact device id RemSound uses internally. This is the quickest way to confirm an interface is actually present and seen by Windows.
`--selftest`| Runs RemSound's built-in self-test and reports **PASS** or **FAIL**. It works through a list of named checks: a full audio round-trip on the machine on its own (capture → encode → send across the network layer to itself → receive → decode, for both quality settings), the audio encryption, the network packet format, saving and reloading settings and a profile, that a diagnostics report never leaks a password, and that the bundled sounds and manual are present. No sound is played out, so it is safe to run silently. Add `--seconds N` to make the audio part run for longer than the default.
`--selftest`
(or `--smoke-test`)| Runs RemSound's built-in self-test and reports **PASS** or **FAIL**. It works through a list of named checks: a full audio round-trip on the machine on its own (capture → encode → send across the network layer to itself → receive → decode, for both quality settings), the audio encryption, the network packet format, saving and reloading settings and a profile, that a diagnostics report never leaks a password, and that the bundled sounds and manual are present. No sound is played out, so it is safe to run silently. Add `--seconds N` to make the audio part run for longer than the default.
`--diagnostics`| Writes a single plain-text report file holding the version, the operating system, the current settings, the list of profiles, the full device list, a check of the Windows microphone-privacy permission, and the tail of the most recent log. With no path it saves into the **user settings and logs** folder and prints where it put it; you can also give a path, for example `--diagnostics C:\Users\you\Desktop\report.txt`. This is the file to send when asking for help — it answers most questions in one go.
### Options that change a setting or control a running copy, then exit
@@ -1116,6 +1117,7 @@ Option| What it does
`--profile "<name>"`| Starts straight into the named profile and skips the profile picker. Put the name in quotes if it contains a space, for example `--profile "Studio link"`.
`--connect <ip>`| Starts and connects to a peer at that address. You can give just an address (`--connect 192.168.1.42`) or an address and port (`--connect 192.168.1.42:47830`); with no port it uses RemSound's normal port, 47830. If you don't also give a `--profile`, it starts on a fresh blank profile already pointed at that peer.
`--minimized` or `--tray`| Starts minimized to the notification area, with no window popping up. Pair it with `--profile` or `--connect` so it has something to do without waiting at the picker.
`--config-dir <folder>`| Uses an explicit folder for this run's settings, profiles, logs and sounds, instead of the usual location. It lets you (or an automated test) run RemSound against a throwaway folder without touching your real settings. Works with any command — for example `--selftest --config-dir C:\Temp\rstest` or `--diagnostics --config-dir C:\Temp\rstest`.
### Examples
+2 -1
View File
@@ -1154,7 +1154,7 @@ Use whatever key combinations you prefer (for example Ctrl+Shift+Up / Ctrl+Shift
<tr><td><code>--help</code> or <code>-h</code></td><td>Lists every option, the same as this section in short form.</td></tr>
<tr><td><code>--version</code></td><td>Prints which version of RemSound is installed, for example &ldquo;RemSound 3.9&rdquo;.</td></tr>
<tr><td><code>--devices</code></td><td>Lists every microphone and line-in, every speaker and headphone output, and every ASIO driver on the machine &mdash; each with its sample rate, channel count and the exact device id RemSound uses internally. This is the quickest way to confirm an interface is actually present and seen by Windows.</td></tr>
<tr><td><code>--selftest</code></td><td>Runs RemSound's built-in self-test and reports <strong>PASS</strong> or <strong>FAIL</strong>. It works through a list of named checks: a full audio round-trip on the machine on its own (capture &rarr; encode &rarr; send across the network layer to itself &rarr; receive &rarr; decode, for both quality settings), the audio encryption, the network packet format, saving and reloading settings and a profile, that a diagnostics report never leaks a password, and that the bundled sounds and manual are present. No sound is played out, so it is safe to run silently. Add <code>--seconds N</code> to make the audio part run for longer than the default.</td></tr>
<tr><td><code>--selftest</code><br>(or <code>--smoke-test</code>)</td><td>Runs RemSound's built-in self-test and reports <strong>PASS</strong> or <strong>FAIL</strong>. It works through a list of named checks: a full audio round-trip on the machine on its own (capture &rarr; encode &rarr; send across the network layer to itself &rarr; receive &rarr; decode, for both quality settings), the audio encryption, the network packet format, saving and reloading settings and a profile, that a diagnostics report never leaks a password, and that the bundled sounds and manual are present. No sound is played out, so it is safe to run silently. Add <code>--seconds N</code> to make the audio part run for longer than the default.</td></tr>
<tr><td><code>--diagnostics</code></td><td>Writes a single plain-text report file holding the version, the operating system, the current settings, the list of profiles, the full device list, a check of the Windows microphone-privacy permission, and the tail of the most recent log. With no path it saves into the <strong>user settings and logs</strong> folder and prints where it put it; you can also give a path, for example <code>--diagnostics C:\Users\you\Desktop\report.txt</code>. This is the file to send when asking for help &mdash; it answers most questions in one go.</td></tr>
</table>
@@ -1172,6 +1172,7 @@ Use whatever key combinations you prefer (for example Ctrl+Shift+Up / Ctrl+Shift
<tr><td><code>--profile "&lt;name&gt;"</code></td><td>Starts straight into the named profile and skips the profile picker. Put the name in quotes if it contains a space, for example <code>--profile "Studio link"</code>.</td></tr>
<tr><td><code>--connect &lt;ip&gt;</code></td><td>Starts and connects to a peer at that address. You can give just an address (<code>--connect 192.168.1.42</code>) or an address and port (<code>--connect 192.168.1.42:47830</code>); with no port it uses RemSound's normal port, 47830. If you don't also give a <code>--profile</code>, it starts on a fresh blank profile already pointed at that peer.</td></tr>
<tr><td><code>--minimized</code> or <code>--tray</code></td><td>Starts minimized to the notification area, with no window popping up. Pair it with <code>--profile</code> or <code>--connect</code> so it has something to do without waiting at the picker.</td></tr>
<tr><td><code>--config-dir &lt;folder&gt;</code></td><td>Uses an explicit folder for this run's settings, profiles, logs and sounds, instead of the usual location. It lets you (or an automated test) run RemSound against a throwaway folder without touching your real settings. Works with any command &mdash; for example <code>--selftest --config-dir C:\Temp\rstest</code> or <code>--diagnostics --config-dir C:\Temp\rstest</code>.</td></tr>
</table>
<h3>Examples</h3>
+36
View File
@@ -113,6 +113,42 @@ foreach ($line in ($st.Text -split "`r?`n")) {
}
if ($st.Code -eq 0) { Pass "self-test passed (exit 0)" } else { Fail "self-test failed (exit $($st.Code))" }
# ---- 5. COLD START + CLEAN CLOSE, against an isolated --config-dir so the real settings are never
# touched (smoke-test brief, safety rule 1 + baseline steps 3-4) ----
Write-Host "`nCold start and clean close (isolated config):" -ForegroundColor Cyan
$already = @(Get-Process RemSound -ErrorAction SilentlyContinue)
if ($already.Count -gt 0) {
Write-Host " [SKIP] a RemSound instance is already running (machine-wide single-instance lock) - close it to run this check" -ForegroundColor Yellow
}
else {
$testCfg = Join-Path ([System.IO.Path]::GetTempPath()) ("rs-cfg-" + [guid]::NewGuid().ToString('N'))
$proc = $null
try {
$proc = Start-Process -FilePath $exe -ArgumentList @('--config-dir', $testCfg, '--connect', '127.0.0.1', '--minimized') -PassThru
Start-Sleep -Seconds 6
if (Get-Process -Id $proc.Id -ErrorAction SilentlyContinue) { Pass "GUI cold-started and stayed up (minimized to tray)" }
else { Fail "GUI exited or crashed during cold start" }
# Startup consolidates the cue sounds into UserDataDirectory; with the override that's $testCfg,
# so a populated $testCfg proves the process honoured --config-dir and left the real settings alone.
if (Test-Path (Join-Path $testCfg 'sounds')) { Pass "ran against the isolated --config-dir folder (real settings untouched)" }
else { Fail "--config-dir folder was not populated - config isolation may not be working" }
Invoke-RsCli @('--close') | Out-Null
Start-Sleep -Seconds 2
$still = Get-Process -Id $proc.Id -ErrorAction SilentlyContinue
if (-not $still) { Pass "--close shut the GUI down cleanly (no orphan process)" }
else { Fail "process still running after --close"; try { $still | Stop-Process -Force } catch { } }
}
catch {
Fail "cold-start/close smoke threw: $($_.Exception.Message)"
if ($proc) { try { Get-Process -Id $proc.Id -ErrorAction SilentlyContinue | Stop-Process -Force } catch { } }
}
finally {
Remove-Item -LiteralPath $testCfg -Recurse -Force -ErrorAction SilentlyContinue
}
}
# ---- summary ----
Remove-Item -LiteralPath $publishDir -Recurse -Force -ErrorAction SilentlyContinue
Write-Host ""
+25 -2
View File
@@ -40,6 +40,25 @@ internal static class CommandLine
[DllImport("kernel32.dll")] private static extern bool AttachConsole(int dwProcessId);
private const int ATTACH_PARENT_PROCESS = -1;
/// <summary>The folder given after <c>--config-dir</c>, or null. Read at the very start of
/// <see cref="Program"/> - before the layout migration and any config/profile/log/sound access -
/// so it can redirect ALL user state via <see cref="AppConfig.SetUserDataDirectoryOverride"/>.
/// Applies to every command (e.g. <c>--selftest --config-dir</c>, <c>--diagnostics --config-dir</c>)
/// and to a normal GUI launch, so a test can exercise a real build without touching live settings.</summary>
public static bool TryGetConfigDir(string[] args, out string dir)
{
dir = "";
for (var i = 0; i < args.Length - 1; i++)
{
if (args[i].Equals("--config-dir", StringComparison.OrdinalIgnoreCase) && !args[i + 1].StartsWith('-'))
{
dir = args[i + 1];
return !string.IsNullOrWhiteSpace(dir);
}
}
return false;
}
/// <summary>
/// Process the command line. Returns a non-null exit code when a do-and-exit command ran (the
/// caller should <c>Environment.Exit</c> it); returns null to continue into the GUI launch with
@@ -61,7 +80,7 @@ internal static class CommandLine
return WithConsole(PrintVersion);
case "--devices": case "--list-devices":
return WithConsole(() => { WriteDevices(Console.Out); return 0; });
case "--selftest": case "--self-test":
case "--selftest": case "--self-test": case "--smoke-test": case "--smoketest":
return WithConsole(() => SelfTest.Run(args));
case "--diagnostics": case "--diag":
return WithConsole(() => RunDiagnostics(ValueAfter(args, raw)));
@@ -130,7 +149,7 @@ internal static class CommandLine
Console.WriteLine(" --devices List all microphones, outputs and ASIO drivers,");
Console.WriteLine(" with their formats and device ids.");
Console.WriteLine(" --selftest [--seconds N] Run the built-in self-test - a localhost audio");
Console.WriteLine(" round-trip plus checks of encryption, the wire format,");
Console.WriteLine(" (or --smoke-test) round-trip plus checks of encryption, the wire format,");
Console.WriteLine(" settings, profiles and bundled files - and report PASS/FAIL.");
Console.WriteLine(" --diagnostics [path] Write a diagnostics report (version, config, profiles,");
Console.WriteLine(" devices, mic-privacy check, recent log) and exit. With");
@@ -145,6 +164,10 @@ internal static class CommandLine
Console.WriteLine(" --connect <ip[:port]> Start and connect to a peer at this address. With no");
Console.WriteLine(" --profile, starts on a fresh profile connected to it.");
Console.WriteLine(" --minimized, --tray Start minimized to the notification area.");
Console.WriteLine(" --config-dir <folder> Use an explicit folder for this run's settings, profiles,");
Console.WriteLine(" logs and sounds, instead of the usual location. Lets a test");
Console.WriteLine(" exercise RemSound without touching your real settings.");
Console.WriteLine(" Works with any command (e.g. --selftest --config-dir ...).");
Console.WriteLine();
Console.WriteLine("Examples:");
Console.WriteLine(" RemSound.exe --devices");
+10
View File
@@ -25,6 +25,16 @@ internal static class Program
return;
}
// --config-dir <folder> (test / portable isolation): redirect ALL user state - config,
// profiles, logs, cue sounds - to an explicit folder for THIS process only. Applied first,
// before the layout migration and sound consolidation below read or write the default
// location, so a smoke test can run a real build without touching the user's settings
// (smoke-test brief, safety rule 1).
if (CommandLine.TryGetConfigDir(args, out var configDir))
{
AppConfig.SetUserDataDirectoryOverride(configDir);
}
// SustainedLowLatency tells the GC to avoid full (gen 2) collections while audio is streaming.
// Gen 0/1 collections still happen but are sub-millisecond; the long pauses that were causing
// the receiver to fall behind in clusters of 4-5 underruns at a time were almost certainly
+17 -1
View File
@@ -226,7 +226,23 @@ public sealed class AppConfig
/// / the earlier <c>config\</c> folder so the install root stays tidy and the auto-updater can
/// exclude one folder to leave ALL user state (including custom cue WAVs) untouched.</summary>
public const string UserDataFolderName = "user settings and logs";
public static string UserDataDirectory => Path.Combine(AppContext.BaseDirectory, UserDataFolderName);
/// <summary>Process-wide override for <see cref="UserDataDirectory"/>. Null = the default
/// folder next to the exe. Set once at startup from the <c>--config-dir</c> switch so the test
/// suite (and a portable layout) can point ALL user state - config, profiles, logs, cue sounds -
/// at an explicit throwaway folder without touching the user's real settings. Must be set before
/// anything reads config/profiles/logs/sounds.</summary>
private static string? _userDataDirectoryOverride;
/// <summary>Redirect every user-state folder to <paramref name="path"/> for this process only.
/// Call before <see cref="MigrateLegacyLayoutIfNeeded"/> / any config read. Idempotent.</summary>
public static void SetUserDataDirectoryOverride(string path)
{
if (!string.IsNullOrWhiteSpace(path)) _userDataDirectoryOverride = Path.GetFullPath(path);
}
public static string UserDataDirectory =>
_userDataDirectoryOverride ?? Path.Combine(AppContext.BaseDirectory, UserDataFolderName);
/// <summary>Where the per-machine log files are written.</summary>
public static string LogsDirectory => Path.Combine(UserDataDirectory, "logs");