Build 730 (creator): - Left pill while recording: solid #2ECC71 dot pulsing 1.0 <-> 0.2 on the existing one-second timer, and "REC: hh:mm:ss" in Consolas so the digits cannot jitter the pill's width. Idle face unchanged. - Right pill while recording: a stop square and "Stop Recording"; clicking it runs the normal stop, which halts and resets the clock and returns the pair to "Local MP4" / "Start Recording". - The separate reality line (dot + REC/LIVE + elapsed + End) is now LIVE-ONLY: a recording would otherwise print the same three facts twice. RunningVisible is IsLive, and ShowOutputStage no longer hides itself for a recording (it must be on screen to carry the clock and the stop). Two rules collided and are now separated rather than merged: a creator's Stop on a recording restores the Local MP4 pair (build 730), while a pipeline FAILURE still clears both pills (the 2026-09-01 zombie rule). StopStream(afterFailure:) carries the difference, and the pump-failure path passes it. RealAppHost.Run was rethrowing with `throw failure;`, which destroyed the original stack — a failing assertion inside the action reported the wrapper's line instead of its own. ExceptionDispatchInfo now preserves it; that is how the two failures above were located in one pass instead of five. 680 pass (one audio-pipe instance flake under full-suite load, passes alone), 0 warnings. Scope justification: Assets/starting-background.jpg is outside this unit — the creator's replacement art, still awaiting their visual check.
112 lines
3.5 KiB
C#
112 lines
3.5 KiB
C#
using System;
|
|
using System.Threading;
|
|
using System.Windows.Threading;
|
|
using Xunit;
|
|
|
|
namespace ytLive.Tests;
|
|
|
|
/// <summary>
|
|
/// Tests that spin up the real WPF App need exactly one Application instance per
|
|
/// AppDomain (WPF enforces it) — so they share this serial collection and a
|
|
/// single App created on one dedicated STA thread. Marshal test bodies onto it
|
|
/// via <see cref="RealAppHost.Run"/>, never `new App()` per test.
|
|
/// </summary>
|
|
[CollectionDefinition("RealApp", DisableParallelization = true)]
|
|
public sealed class RealAppCollection : ICollectionFixture<RealAppHost>
|
|
{
|
|
}
|
|
|
|
/// <summary>Owns the one-and-only WPF App on a dedicated STA thread.</summary>
|
|
public sealed class RealAppHost : IDisposable
|
|
{
|
|
private readonly Thread _thread;
|
|
private readonly Dispatcher _dispatcher;
|
|
|
|
public RealAppHost()
|
|
{
|
|
Exception? boot = null;
|
|
Dispatcher? dispatcher = null;
|
|
_thread = new Thread(() =>
|
|
{
|
|
try
|
|
{
|
|
var app = new App();
|
|
app.InitializeComponent();
|
|
dispatcher = Dispatcher.CurrentDispatcher;
|
|
Dispatcher.Run();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
boot = ex;
|
|
}
|
|
});
|
|
_thread.SetApartmentState(ApartmentState.STA);
|
|
_thread.Start();
|
|
while (dispatcher == null && boot == null)
|
|
Thread.Sleep(5);
|
|
if (boot != null)
|
|
throw new Xunit.Sdk.XunitException("Real WPF App failed to start: " + boot);
|
|
_dispatcher = dispatcher!;
|
|
}
|
|
|
|
/// <summary>Runs <paramref name="action"/> on the App's STA thread and rethrows any failure.</summary>
|
|
public void Run(Action action)
|
|
{
|
|
Exception? failure = null;
|
|
_dispatcher.Invoke(() =>
|
|
{
|
|
try
|
|
{
|
|
action();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
failure = ex;
|
|
}
|
|
});
|
|
// Rethrow preserving the ORIGINAL stack: `throw failure;` reset it to this line, which hid
|
|
// which assertion actually failed inside the passed action.
|
|
if (failure != null)
|
|
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(failure).Throw();
|
|
}
|
|
|
|
/// <summary>Awaits <paramref name="action"/> on the App's STA thread and rethrows
|
|
/// any failure.
|
|
/// <para>A test that drives the frame pump MUST use this and <c>await</c> — never
|
|
/// <c>Run()</c> around a blocking wait. <see cref="Run"/> occupies the one STA
|
|
/// thread this collection shares, so a blocking call inside it deadlocks the pump
|
|
/// against the dispatcher and hangs the entire run with no output: the frame pump's
|
|
/// <c>StartAsync</c> needs to keep pumping while the test waits for a frame. Awaiting
|
|
/// yields the thread instead of holding it.</para></summary>
|
|
public async Task RunAsync(Func<Task> action)
|
|
{
|
|
Exception? failure = null;
|
|
await _dispatcher.InvokeAsync(async () =>
|
|
{
|
|
try
|
|
{
|
|
await action();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
failure = ex;
|
|
}
|
|
});
|
|
if (failure != null)
|
|
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(failure).Throw();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
try
|
|
{
|
|
_dispatcher.InvokeShutdown();
|
|
}
|
|
catch
|
|
{
|
|
// The App never ran its message loop — shutdown is best-effort.
|
|
}
|
|
_thread.Join(2000);
|
|
}
|
|
}
|