test: replace final fixed-delay oracles
This commit is contained in:
parent
ad7ebe9425
commit
79a4489e03
6 changed files with 101 additions and 21 deletions
|
|
@ -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 M–O | 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 M–O 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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue