Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 18 additions & 14 deletions test/Capacitor.App.Tests.Unit/AppStartupTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -303,26 +303,28 @@ public async Task WireDialogCancellation_after_normal_resolution_ignores_a_later
/// forever until its ct is cancelled (RestartLoopAsync/DisposeAsync's normal teardown path)
/// — enough to prove a DaemonClientService actually has a LIVE loop to dispose.
sealed class ForeverRunClient {
public int LiveEnumerations;
readonly TaskCompletionSource _started = new(TaskCreationOptions.RunContinuationsAsynchronously);
readonly TaskCompletionSource _ended = new(TaskCreationOptions.RunContinuationsAsynchronously);

int _live;

public int LiveEnumerations => Volatile.Read(ref _live);
public Task Started => _started.Task;
public Task Ended => _ended.Task;

public async IAsyncEnumerable<LocalControlEvent> Run([EnumeratorCancellation] CancellationToken ct) {
Interlocked.Increment(ref LiveEnumerations);
Interlocked.Increment(ref _live);
_started.TrySetResult();
try {
yield return new LocalControlEvent.Connecting();
await Task.Delay(Timeout.Infinite, ct);
} finally {
Interlocked.Decrement(ref LiveEnumerations);
if (Interlocked.Decrement(ref _live) == 0) _ended.TrySetResult();
}
}
}

static async Task WaitUntilAsync(Func<bool> condition, TimeSpan? timeout = null) {
var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(5));
while (!condition()) {
if (DateTime.UtcNow > deadline) throw new TimeoutException("condition not met in time");
await Task.Delay(10);
}
}
static readonly TimeSpan LoopSignalBound = TimeSpan.FromSeconds(30);

/// A startup failure AFTER service.Start()/_service assignment (e.g. BuildAndShowMainWindow
/// throwing) must dispose the live IPC pump/socket before ShowStartupError: closing the error
Expand All @@ -338,7 +340,7 @@ public async Task HandleStartupFailureAsync_disposes_the_live_service_before_sho
var runClient = new ForeverRunClient();
var service = new DaemonClientService("daemon-a", runClient.Run, _ => Task.FromResult<MutationOutcome>(new MutationOutcome.Refused("cli_not_found", RecoverySurface.Attention)));
service.Start();
await WaitUntilAsync(() => runClient.LiveEnumerations >= 1);
await runClient.Started.WaitAsync(LoopSignalBound);

var shutdown = new CancellationTokenSource();

Expand All @@ -353,7 +355,8 @@ await AppUnderTest.HandleStartupFailureAsync(
await Assert.That(shutdown.IsCancellationRequested).IsTrue();
await Assert.That(modeAfterShow).IsEqualTo(ShutdownMode.OnExplicitShutdown);
await Assert.That(mainWindowAssigned).IsTrue();
await WaitUntilAsync(() => runClient.LiveEnumerations == 0, TimeSpan.FromSeconds(5));
await runClient.Ended.WaitAsync(LoopSignalBound);
await Assert.That(runClient.LiveEnumerations).IsEqualTo(0);

// A second DisposeAsync (mirroring the real catch path's `_service = null` guard against
// a later OnShutdownRequested double-dispose) must be a safe no-op, not a throw.
Expand Down Expand Up @@ -404,7 +407,7 @@ public async Task DisposeAndConfirmShutdownAsync_disposes_then_confirms_then_car
var runClient = new ForeverRunClient();
var service = new DaemonClientService("daemon-a", runClient.Run, _ => Task.FromResult<MutationOutcome>(new MutationOutcome.Refused("cli_not_found", RecoverySurface.Attention)));
service.Start();
await WaitUntilAsync(() => runClient.LiveEnumerations >= 1);
await runClient.Started.WaitAsync(LoopSignalBound);

var (desktop, fake) = FakeClassicDesktopLifetime.Create();
// Ordering pin: markConfirmed must observably run BEFORE TryShutdown is called — proven
Expand All @@ -419,7 +422,8 @@ await AppUnderTest.DisposeAndConfirmShutdownAsync(

await Assert.That(confirmedBeforeShutdownCall).IsTrue();
await Assert.That(fake.ShutdownCalls).IsEquivalentTo([1], CollectionOrdering.Matching);
await WaitUntilAsync(() => runClient.LiveEnumerations == 0, TimeSpan.FromSeconds(5));
await runClient.Ended.WaitAsync(LoopSignalBound);
await Assert.That(runClient.LiveEnumerations).IsEqualTo(0);
}

/// A throwing disposeAsync must still confirm and shut down: otherwise _shutdownConfirmed stays
Expand Down
Loading