test: replace final fixed-delay oracles

This commit is contained in:
Erik 2026-08-18 13:12:26 +02:00
parent ad7ebe9425
commit 79a4489e03
6 changed files with 101 additions and 21 deletions

View file

@ -53,8 +53,8 @@ inventory refreshed for Batch L reports:
| Constant-truth assertion methods / sites | 0 / 0 |
| Reviewed syntactic self-comparison methods / sites | 4 / 5 |
| Reviewed diagnostic methods | 57 |
| Methods directly using `Thread.Sleep` / `Task.Delay` | 15 / 17 |
| Cancellable-infinite-only / elapsed-time methods | 5 / 27 |
| Methods directly using `Thread.Sleep` / `Task.Delay` | 15 / 14 |
| Cancellable-infinite-only / elapsed-time methods | 5 / 24 |
| Methods directly reading environment variables | 47 |
| Methods directly reading `.cs` source text | 63 |
@ -147,7 +147,7 @@ identifiable in the lane report.
| T-016 historical test taxonomy | first descriptive-identity batch active in batch J | Remove opaque AP/R/J/K/Slice codes from current test/type names while preserving provenance in comments and ledgers. The 47 `Issue###` files remain an explicit user-decision set because their IDs still connect tests to retail evidence. |
| T-017 Avalonia ownership | reopened and closed in batch D | The full gate exposed the same compositor ownership class between six newer `MainWindowViewTests` facts. Their six named assertion phases now run in one owned Avalonia application session; 25 fresh-process stress iterations and the complete gate pass. |
| T-018 stderr observer race | resolved in R2 | Live reader shares writes/deletes; 25 focused repetitions plus the complete gate. |
| T-019 remaining explicit waits | classified in batch H; six fixed-delay negative oracles resolved in batches M/N, three remain | Five methods are deterministic cancellation stubs, not wall-clock tests. Preserve intentional real-time contracts and bounded integration polling. The remaining cleanup owners are headless late-subscriber replay and two launcher installer/recovery contracts. |
| T-019 remaining explicit waits | resolved in batches MO | All nine fixed-delay negative oracles now use observed monitor/thread/lease/process state. The remaining 24 elapsed-time methods are the reviewed cancellation, real-time protocol, cooperative-yield, bounded-polling, and positive-timeout categories from Batch H. |
| T-020 exact duplicate bodies/data | resolved in batch K | Four redundant executions were removed. The remaining 11 body-equivalent groups are intentionally split theories with disjoint, meaningfully named datasets; the inventory enforces zero repeated rows within a theory or across body-equivalent theories. |
| T-021 suspicious assertion shapes | resolved in batch L | The inventory enforces zero `Assert.True(true)` / `Assert.False(false)` sites and reports syntactic self-comparisons for review. All five current self-comparisons assert meaningful determinism or stable identity. |
@ -448,7 +448,7 @@ invocation instead of treating both method-level booleans as equivalent. The
| Intentional real-time protocol/timeout contract | 4 | Keep with explicit bounds: two Live handshake race delays, the one-second net-probe cadence, and continuous unrelated shutdown drain. |
| Cooperative yield while virtual-clock/background transport work drains | 8 | Retain for now; prefer an observable receiver/worker signal when that seam exists. The virtual behavioral oracle does not derive from the sleep duration. |
| Bounded completion/readiness polling | 11 | Retain as integration polling with a terminal assertion and deadline; improve opportunistically with events, not by busy-spinning. |
| Fixed delay used to prove another operation is still blocked | 9 originally; 3 remain | Batches M/N replace six with observed thread wait state. Replace the remaining elapsed-time absence-of-completion checks with an observed wait state or test seam. |
| Fixed delay used to prove another operation is still blocked | 9 originally; 0 remain | Batches MO replace all nine with observed monitor/thread/lease/process state. |
| Positive completion timeout guard | 1 | Keep. The two-second `WhenAny` in `RuntimeCharacterStateTests` fails only if the operation does not complete; it does not delay a passing run. |
The original nine fixed-delay negative oracles are:
@ -696,3 +696,41 @@ Batch N verification:
Three fixed-delay negative oracles remain: headless concurrent late-subscriber
replay and the two launcher installer/recovery contracts.
## Batch O final fixed-delay negative oracles
Batch O removes the last three timing-based absence-of-completion tests:
- `HeadlessPluginSessionTests.LateSubscriberReplayQueuesConcurrentRegistrationExactlyOnceInOrder`
now runs registration on a dedicated thread and observes it enter
`WaitSleepJoin` while Runtime's exact replay read lease is held;
- `LauncherInstallerTests.IndependentInstallersSerializeAndWaitingCancellationTouchesNothing`
now waits until the second installer has actually encountered the held
transaction lease before cancelling it; and
- both rows of
`LauncherInstallerTests.OrphanBakeCanNeverPublishAfterRestartRecovery` now
observe publication-lock contention where applicable and wait for the exact
orphan child process to exit before checking that it cannot publish later.
The launcher observation points are internal callbacks on the existing
transaction/publication lease retry paths. They are unset in production, add no
new public API, and do not change lock, retry, cancellation, or publication
behavior. The former 150/200 ms delays and post-exit settle delay are gone.
Batch O verification:
- the one focused Headless fact and three focused Launcher cases pass;
- 25 fresh-process Headless iterations and 25 fresh-process Launcher iterations
pass, totaling 100/100 exercised cases;
- the refreshed inventory reduces direct `Task.Delay` methods from 17 to 14
and elapsed-time methods from 27 to 24;
- none of Batch H's nine fixed-delay negative-oracle methods contains a direct
sleep or delay;
- the complete 44-project Release build reports zero warnings and zero errors;
and
- the no-retry complete hermetic Release gate remains 14,382/14,382 with zero
skips or failures across all 12 test assemblies.
The 24 remaining elapsed-time methods retain their reviewed Batch H
classification: cancellation fakes, intentional real-time protocol behavior,
cooperative yields, bounded integration polling, or a positive timeout guard.

View file

@ -15,7 +15,8 @@ internal static class BakePublicationGuardContract
internal static async ValueTask<PublicationLease> AcquireAsync(
string outputPath,
CancellationToken cancellationToken = default)
CancellationToken cancellationToken = default,
Action? contentionObserved = null)
{
string lockPath = BakePublicationGuardPaths.GetPublishLockPath(
outputPath);
@ -39,6 +40,7 @@ internal static class BakePublicationGuardContract
}
catch (IOException)
{
contentionObserved?.Invoke();
await Task.Delay(RetryDelay, cancellationToken)
.ConfigureAwait(false);
}

View file

@ -22,7 +22,8 @@ internal sealed class InstallerTransactionLease : IAsyncDisposable
internal static async ValueTask<InstallerTransactionLease> AcquireAsync(
string dataDirectory,
CancellationToken cancellationToken = default)
CancellationToken cancellationToken = default,
Action? contentionObserved = null)
{
string lockPath = GetLockPath(dataDirectory);
Directory.CreateDirectory(
@ -46,6 +47,7 @@ internal sealed class InstallerTransactionLease : IAsyncDisposable
}
catch (IOException)
{
contentionObserved?.Invoke();
await Task.Delay(RetryDelay, cancellationToken)
.ConfigureAwait(false);
}

View file

@ -80,6 +80,11 @@ public sealed class LauncherInstaller : ILauncherInstaller
private LauncherInstallRecord? _verifiedRecord;
/// <summary>Test-only observation points for cross-process lease
/// contention. Production leaves both callbacks unset.</summary>
internal Action? TransactionLeaseContentionObservedForTest { get; set; }
internal Action? PublicationLeaseContentionObservedForTest { get; set; }
public LauncherInstaller(
ApplicationPathSet paths,
string bakeExecutablePath,
@ -118,7 +123,8 @@ public sealed class LauncherInstaller : ILauncherInstaller
await using InstallerTransactionLease lease =
await InstallerTransactionLease.AcquireAsync(
_recordStore.DataDirectory,
cancellationToken)
cancellationToken,
TransactionLeaseContentionObservedForTest)
.ConfigureAwait(false);
InstallRecordVerification verification =
await RecoverExistingUnderPublicationGuardAsync(
@ -152,7 +158,8 @@ public sealed class LauncherInstaller : ILauncherInstaller
await using InstallerTransactionLease lease =
await InstallerTransactionLease.AcquireAsync(
_recordStore.DataDirectory,
cancellationToken)
cancellationToken,
TransactionLeaseContentionObservedForTest)
.ConfigureAwait(false);
return await InstallCoreAsync(
datDirectory,
@ -440,7 +447,8 @@ public sealed class LauncherInstaller : ILauncherInstaller
await using BakePublicationGuardContract.PublicationLease publication =
await BakePublicationGuardContract.AcquireAsync(
outputPath,
cancellationToken)
cancellationToken,
PublicationLeaseContentionObservedForTest)
.ConfigureAwait(false);
// Any child whose parent died before it acquired this lock is now
// irrevocably stale. A child already holding the lock must finish its

View file

@ -212,24 +212,44 @@ public sealed class HeadlessPluginSessionTests
Task subscribe = Task.Run(() => host.Events.EntitySpawned += handler);
Assert.True(replayCaptured.Wait(TimeSpan.FromSeconds(10)));
using var registrationStarted = new ManualResetEventSlim();
Task registration = Task.Run(() =>
Exception? registrationError = null;
var registrationThread = new Thread(() =>
{
registrationStarted.Set();
_ = session.Runtime.EntityObjects.RegisterEntity(
Spawn(0x50000002u, 2f));
});
Assert.True(registrationStarted.Wait(TimeSpan.FromSeconds(10)));
try
{
_ = session.Runtime.EntityObjects.RegisterEntity(
Spawn(0x50000002u, 2f));
}
catch (Exception error)
{
registrationError = error;
}
})
{
IsBackground = true,
Name = "Headless plugin concurrent registration contract",
};
registrationThread.Start();
bool registrationStartedInTime = registrationStarted.Wait(
TimeSpan.FromSeconds(10));
bool registrationBlockedOnReplay = registrationStartedInTime &&
SpinWait.SpinUntil(
() => (registrationThread.ThreadState & ThreadState.WaitSleepJoin) != 0,
TimeSpan.FromSeconds(10));
try
{
await Task.Delay(TimeSpan.FromMilliseconds(100));
Assert.False(registration.IsCompleted);
Assert.True(
registrationBlockedOnReplay,
"registration never waited for the active replay read lease");
}
finally
{
releaseReplay.Set();
}
await Task.WhenAll(subscribe, registration)
.WaitAsync(TimeSpan.FromSeconds(10));
await subscribe.WaitAsync(TimeSpan.FromSeconds(10));
Assert.True(registrationThread.Join(TimeSpan.FromSeconds(10)));
Assert.Null(registrationError);
host.Events.EntitySpawned -= handler;
Assert.Equal([1_000_000u, 1_000_001u], observed);

View file

@ -340,6 +340,10 @@ public sealed class LauncherInstallerTests : IDisposable
_bakeExecutable,
recordStore: new LauncherInstallRecordStore(_paths),
processRunner: runnerB);
var leaseContended = new TaskCompletionSource(
TaskCreationOptions.RunContinuationsAsynchronously);
installerB.TransactionLeaseContentionObservedForTest =
() => leaseContended.TrySetResult();
Task<LauncherInstallResult> operationA =
installerA.InstallAsync(_dats, 1);
@ -351,7 +355,8 @@ public sealed class LauncherInstallerTests : IDisposable
_dats,
1,
cancellationToken: cancellationB.Token);
await Task.Delay(150);
await leaseContended.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.False(operationB.IsCompleted);
cancellationB.Cancel();
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => operationB);
@ -537,6 +542,7 @@ public sealed class LauncherInstallerTests : IDisposable
orphanPid = int.Parse(
await File.ReadAllTextAsync(childPid),
System.Globalization.CultureInfo.InvariantCulture);
using Process orphan = Process.GetProcessById(orphanPid);
Assert.True(File.Exists(
LauncherInstallRecordStore.GetBackupPath(
store.PreparedAssetPath)));
@ -551,12 +557,16 @@ public sealed class LauncherInstallerTests : IDisposable
_paths,
_bakeExecutable,
recordStore: new LauncherInstallRecordStore(_paths));
var publicationContended = new TaskCompletionSource(
TaskCreationOptions.RunContinuationsAsynchronously);
restarted.PublicationLeaseContentionObservedForTest =
() => publicationContended.TrySetResult();
Task<InstallRecordVerification> recovery =
restarted.LoadExistingAsync();
InstallRecordVerification recovered;
if (schedule == "holds")
{
await Task.Delay(200);
await publicationContended.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.False(recovery.IsCompleted);
File.WriteAllText(release, "release");
recovered = await recovery.WaitAsync(TimeSpan.FromSeconds(15));
@ -593,7 +603,7 @@ public sealed class LauncherInstallerTests : IDisposable
await File.ReadAllTextAsync(childExit + ".error"),
StringComparison.OrdinalIgnoreCase);
}
await Task.Delay(200);
await orphan.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(15));
Assert.Equal(
canonicalAfterRecovery,