test: observe monitor waits without delays
This commit is contained in:
parent
ea17bc8624
commit
fa4bdfe89f
4 changed files with 127 additions and 35 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 / 23 |
|
||||
| Cancellable-infinite-only / elapsed-time methods | 5 / 33 |
|
||||
| Methods directly using `Thread.Sleep` / `Task.Delay` | 15 / 19 |
|
||||
| Cancellable-infinite-only / elapsed-time methods | 5 / 29 |
|
||||
| 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; nine fixed-delay negative oracles need cleanup | Five of the 38 methods are deterministic cancellation stubs, not wall-clock tests. Preserve four intentional real-time contracts and bounded integration polling; replace the nine tests that infer “still blocked” from a fixed delay with observable synchronization state. |
|
||||
| T-019 remaining explicit waits | classified in batch H; four fixed-delay negative oracles resolved in batch M, five 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 landblock streamer/pool disposal, headless late-subscriber replay, and two launcher installer/recovery contracts. |
|
||||
| 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,10 +448,10 @@ 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 | Cleanup priority. Replace elapsed-time absence-of-completion with an observed wait state or test seam. |
|
||||
| Fixed delay used to prove another operation is still blocked | 9 originally; 5 remain | Batch M replaces four with observed thread wait state. Replace the remaining elapsed-time absence-of-completion checks with an observed wait state or test seam. |
|
||||
| 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 nine fixed-delay negative oracles are:
|
||||
The original nine fixed-delay negative oracles are:
|
||||
|
||||
1. `LiveSessionCommandRouterTests.ConcurrentDispose_WaitsForInFlightTransportThenMakesRouterInert`;
|
||||
2. `HostQuiescenceGateTests.ExternalStopWaitsForAdmittedCallbackToReturn`;
|
||||
|
|
@ -630,3 +630,38 @@ Batch L verification:
|
|||
The five-case reduction from Batch K is exact: five formerly hermetic diagnostic
|
||||
facts are now excluded by `Purpose=Diagnostic`; the sixth was already outside
|
||||
the portable lane because its class requires installed DATs.
|
||||
|
||||
## Batch M observable monitor-wait contracts
|
||||
|
||||
Four concurrency tests used a 50–100 ms delay and then asserted that disposal
|
||||
had not completed. That was only indirect evidence: a delayed or saturated
|
||||
ThreadPool could satisfy the assertion before the disposal path had attempted
|
||||
to enter its production monitor.
|
||||
|
||||
Batch M runs each competing disposal/stop operation on a named dedicated
|
||||
thread, waits with a bounded `SpinWait` until the runtime reports
|
||||
`ThreadState.WaitSleepJoin`, and then verifies the thread joins after the
|
||||
controlled callback, transport send, or physical detach is released. This
|
||||
changes no production code and preserves every existing completion, inertness,
|
||||
and propagated-failure assertion. The converted contracts are:
|
||||
|
||||
- `HostQuiescenceGateTests.ExternalStopWaitsForAdmittedCallbackToReturn`;
|
||||
- `LiveSessionCommandRouterTests.ConcurrentDispose_WaitsForInFlightTransportThenMakesRouterInert`;
|
||||
- `SilkWindowCallbackBindingTests.ConcurrentDisposeWaitsForPhysicalDetachToComplete`;
|
||||
and
|
||||
- `SilkWindowCallbackBindingTests.ConcurrentDisposeCannotHidePhysicalDetachFailure`.
|
||||
|
||||
Batch M verification:
|
||||
|
||||
- the four focused contracts pass;
|
||||
- 25 fresh-process focused iterations pass, for 100/100 observed-wait cases;
|
||||
- the refreshed inventory reduces direct `Task.Delay` methods from 23 to 19
|
||||
and elapsed-time methods from 33 to 29;
|
||||
- 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.
|
||||
|
||||
Five fixed-delay negative oracles remain: the landblock streamer and pool
|
||||
disposal contracts, headless concurrent late-subscriber replay, and the two
|
||||
launcher installer/recovery contracts.
|
||||
|
|
|
|||
|
|
@ -427,21 +427,38 @@ public sealed class LiveSessionCommandRouterTests
|
|||
router.Publish(new SendServerCommandCmd("@active")));
|
||||
Assert.True(sendEntered.Wait(TimeSpan.FromSeconds(5)));
|
||||
using var disposeStarted = new ManualResetEventSlim();
|
||||
Task dispose = Task.Run(() =>
|
||||
Exception? disposeError = null;
|
||||
var disposeThread = new Thread(() =>
|
||||
{
|
||||
disposeStarted.Set();
|
||||
router.Dispose();
|
||||
});
|
||||
Assert.True(disposeStarted.Wait(TimeSpan.FromSeconds(5)));
|
||||
Task firstCompletion = await Task.WhenAny(
|
||||
dispose,
|
||||
Task.Delay(TimeSpan.FromMilliseconds(100)));
|
||||
Assert.NotSame(dispose, firstCompletion);
|
||||
try
|
||||
{
|
||||
router.Dispose();
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
disposeError = error;
|
||||
}
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "LiveSessionCommandRouter concurrent dispose contract",
|
||||
};
|
||||
disposeThread.Start();
|
||||
bool disposeStartedInTime = disposeStarted.Wait(TimeSpan.FromSeconds(5));
|
||||
bool disposeBlockedOnTransport = disposeStartedInTime && SpinWait.SpinUntil(
|
||||
() => (disposeThread.ThreadState & ThreadState.WaitSleepJoin) != 0,
|
||||
TimeSpan.FromSeconds(5));
|
||||
|
||||
releaseSend.Set();
|
||||
await Task.WhenAll(publish, dispose);
|
||||
await publish;
|
||||
bool disposeJoined = disposeThread.Join(TimeSpan.FromSeconds(5));
|
||||
router.Publish(new SendServerCommandCmd("@after"));
|
||||
|
||||
Assert.True(disposeStartedInTime, "the dedicated dispose thread did not start");
|
||||
Assert.True(disposeBlockedOnTransport, "dispose never waited for the in-flight send");
|
||||
Assert.True(disposeJoined, "dispose did not finish after the send returned");
|
||||
Assert.Null(disposeError);
|
||||
Assert.Equal(["@active"], sent);
|
||||
Assert.False(router.IsActive);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,20 +20,38 @@ public sealed class HostQuiescenceGateTests
|
|||
Assert.True(callbackEntered.Wait(TimeSpan.FromSeconds(5)));
|
||||
|
||||
using var stopStarted = new ManualResetEventSlim(false);
|
||||
Task stop = Task.Run(() =>
|
||||
Exception? stopError = null;
|
||||
var stopThread = new Thread(() =>
|
||||
{
|
||||
stopStarted.Set();
|
||||
gate.StopAccepting();
|
||||
});
|
||||
Assert.True(stopStarted.Wait(TimeSpan.FromSeconds(5)));
|
||||
await Task.Delay(50);
|
||||
Assert.False(stop.IsCompleted);
|
||||
try
|
||||
{
|
||||
gate.StopAccepting();
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
stopError = error;
|
||||
}
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "HostQuiescenceGate external stop contract",
|
||||
};
|
||||
stopThread.Start();
|
||||
bool stopStartedInTime = stopStarted.Wait(TimeSpan.FromSeconds(5));
|
||||
bool stopBlockedOnCallback = stopStartedInTime && SpinWait.SpinUntil(
|
||||
() => (stopThread.ThreadState & ThreadState.WaitSleepJoin) != 0,
|
||||
TimeSpan.FromSeconds(5));
|
||||
|
||||
releaseCallback.Set();
|
||||
await callback;
|
||||
await stop;
|
||||
bool stopJoined = stopThread.Join(TimeSpan.FromSeconds(5));
|
||||
gate.Invoke(() => calls++);
|
||||
|
||||
Assert.True(stopStartedInTime, "the dedicated stop thread did not start");
|
||||
Assert.True(stopBlockedOnCallback, "stop never waited for the admitted callback");
|
||||
Assert.True(stopJoined, "stop did not finish after the callback returned");
|
||||
Assert.Null(stopError);
|
||||
Assert.Equal(1, calls);
|
||||
Assert.False(gate.IsAccepting);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -270,19 +270,30 @@ public sealed class SilkWindowCallbackBindingTests
|
|||
Task<Exception> first = Task.Run(() => Record.Exception(binding.Dispose));
|
||||
Assert.True(surface.RemoveEntered.Wait(TimeSpan.FromSeconds(5)));
|
||||
using var secondEntered = new ManualResetEventSlim(false);
|
||||
Task<Exception> second = Task.Run(() =>
|
||||
Exception? secondError = null;
|
||||
var secondThread = new Thread(() =>
|
||||
{
|
||||
secondEntered.Set();
|
||||
return Record.Exception(binding.Dispose);
|
||||
});
|
||||
Assert.True(secondEntered.Wait(TimeSpan.FromSeconds(5)));
|
||||
await Task.Delay(50);
|
||||
Assert.False(second.IsCompleted);
|
||||
secondError = Record.Exception(binding.Dispose);
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "Silk callback binding concurrent dispose contract",
|
||||
};
|
||||
secondThread.Start();
|
||||
bool secondStartedInTime = secondEntered.Wait(TimeSpan.FromSeconds(5));
|
||||
bool secondBlockedOnDetach = secondStartedInTime && SpinWait.SpinUntil(
|
||||
() => (secondThread.ThreadState & ThreadState.WaitSleepJoin) != 0,
|
||||
TimeSpan.FromSeconds(5));
|
||||
|
||||
surface.ContinueRemove.Set();
|
||||
|
||||
Assert.Null(await first);
|
||||
Assert.Null(await second);
|
||||
bool secondJoined = secondThread.Join(TimeSpan.FromSeconds(5));
|
||||
Assert.True(secondStartedInTime, "the second dispose thread did not start");
|
||||
Assert.True(secondBlockedOnDetach, "the second dispose never waited for physical detach");
|
||||
Assert.True(secondJoined, "the second dispose did not finish after detach completed");
|
||||
Assert.Null(secondError);
|
||||
Assert.True(binding.IsDetached);
|
||||
}
|
||||
|
||||
|
|
@ -306,19 +317,30 @@ public sealed class SilkWindowCallbackBindingTests
|
|||
Task<Exception> first = Task.Run(() => Record.Exception(binding.Dispose));
|
||||
Assert.True(surface.RemoveEntered.Wait(TimeSpan.FromSeconds(5)));
|
||||
using var secondEntered = new ManualResetEventSlim(false);
|
||||
Task<Exception> second = Task.Run(() =>
|
||||
Exception? secondError = null;
|
||||
var secondThread = new Thread(() =>
|
||||
{
|
||||
secondEntered.Set();
|
||||
return Record.Exception(binding.Dispose);
|
||||
});
|
||||
Assert.True(secondEntered.Wait(TimeSpan.FromSeconds(5)));
|
||||
await Task.Delay(50);
|
||||
Assert.False(second.IsCompleted);
|
||||
secondError = Record.Exception(binding.Dispose);
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "Silk callback binding detach failure contract",
|
||||
};
|
||||
secondThread.Start();
|
||||
bool secondStartedInTime = secondEntered.Wait(TimeSpan.FromSeconds(5));
|
||||
bool secondBlockedOnDetach = secondStartedInTime && SpinWait.SpinUntil(
|
||||
() => (secondThread.ThreadState & ThreadState.WaitSleepJoin) != 0,
|
||||
TimeSpan.FromSeconds(5));
|
||||
|
||||
surface.ContinueRemove.Set();
|
||||
|
||||
Assert.IsType<AggregateException>(await first);
|
||||
Assert.IsType<AggregateException>(await second);
|
||||
bool secondJoined = secondThread.Join(TimeSpan.FromSeconds(5));
|
||||
Assert.True(secondStartedInTime, "the second dispose thread did not start");
|
||||
Assert.True(secondBlockedOnDetach, "the second dispose never waited for physical detach");
|
||||
Assert.True(secondJoined, "the second dispose did not finish after detach failed");
|
||||
Assert.IsType<AggregateException>(secondError);
|
||||
Assert.False(binding.IsDetached);
|
||||
|
||||
surface.PersistentRemoveFailures.Clear();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue