test: observe landblock worker joins

This commit is contained in:
Erik 2026-08-18 13:03:30 +02:00
parent fa4bdfe89f
commit ad7ebe9425
3 changed files with 107 additions and 17 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 / 19 |
| Cancellable-infinite-only / elapsed-time methods | 5 / 29 |
| Methods directly using `Thread.Sleep` / `Task.Delay` | 15 / 17 |
| Cancellable-infinite-only / elapsed-time methods | 5 / 27 |
| 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; 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-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-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; 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. |
| 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. |
| 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:
@ -665,3 +665,34 @@ Batch M verification:
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.
## Batch N observable landblock-worker joins
The two landblock disposal contracts used 50100 ms delays to infer that
`LandblockStreamer.Dispose` was waiting for its dedicated worker threads.
Batch N replaces those delays with direct observation of the disposer threads:
- `LandblockStreamerTests.DisposeAndConcurrentDisposeWaitForInFlightLoad`
observes the primary disposer blocked in a worker `Join`, then observes the
concurrent disposer blocked behind the disposal monitor; and
- `LandblockStreamerPoolTests.Dispose_JoinsEveryWorkerInThePool` observes the
disposer blocked while all three controlled worker lanes remain in their
loader callbacks.
Both tests then release the loaders, require every disposal thread to join
within the existing bound, and retain their exception and post-disposal
inertness checks. Production code is unchanged.
Batch N verification:
- both focused contracts pass;
- 25 fresh-process iterations of each pass, for 50/50 observed-wait cases;
- the refreshed inventory reduces direct `Task.Delay` methods from 19 to 17
and elapsed-time methods from 29 to 27;
- 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.
Three fixed-delay negative oracles remain: headless concurrent late-subscriber
replay and the two launcher installer/recovery contracts.

View file

@ -377,7 +377,7 @@ public sealed class LandblockStreamerPoolTests
}
[Fact]
public async Task Dispose_JoinsEveryWorkerInThePool()
public void Dispose_JoinsEveryWorkerInThePool()
{
const int workerCount = 3;
using var release = new ManualResetEventSlim();
@ -408,13 +408,31 @@ public sealed class LandblockStreamerPoolTests
Assert.True(entered.Wait(TimeSpan.FromSeconds(5)));
Assert.Equal(workerCount, loaderThreads.Count);
Task dispose = Task.Run(streamer.Dispose);
await Task.Delay(100);
// Dispose must be blocked on the still-building workers.
Assert.False(dispose.IsCompleted);
Exception? disposeError = null;
var disposeThread = new Thread(() =>
{
try
{
streamer.Dispose();
}
catch (Exception error)
{
disposeError = error;
}
})
{
IsBackground = true,
Name = "LandblockStreamer pool dispose contract",
};
disposeThread.Start();
Assert.True(SpinWait.SpinUntil(
() => (disposeThread.ThreadState & ThreadState.WaitSleepJoin) != 0,
TimeSpan.FromSeconds(5)),
"dispose never waited for the still-building workers");
release.Set();
await dispose.WaitAsync(TimeSpan.FromSeconds(5));
Assert.True(disposeThread.Join(TimeSpan.FromSeconds(5)));
Assert.Null(disposeError);
Assert.Throws<ObjectDisposedException>(
() => streamer.EnqueueLoad(0x1234FFFFu, LandblockStreamJobKind.LoadFar));

View file

@ -342,7 +342,7 @@ public class LandblockStreamerTests
}
[Fact]
public async Task DisposeAndConcurrentDisposeWaitForInFlightLoad()
public void DisposeAndConcurrentDisposeWaitForInFlightLoad()
{
using var entered = new ManualResetEventSlim();
using var release = new ManualResetEventSlim();
@ -359,14 +359,55 @@ public class LandblockStreamerTests
streamer.EnqueueLoad(0x12340000u);
Assert.True(entered.Wait(TimeSpan.FromSeconds(2)));
Task firstDispose = Task.Run(streamer.Dispose);
Task secondDispose = Task.Run(streamer.Dispose);
await Task.Delay(50);
Assert.False(firstDispose.IsCompleted);
Assert.False(secondDispose.IsCompleted);
Exception? firstError = null;
var firstDisposeThread = new Thread(() =>
{
try
{
streamer.Dispose();
}
catch (Exception error)
{
firstError = error;
}
})
{
IsBackground = true,
Name = "LandblockStreamer primary dispose contract",
};
firstDisposeThread.Start();
Assert.True(SpinWait.SpinUntil(
() => (firstDisposeThread.ThreadState & ThreadState.WaitSleepJoin) != 0,
TimeSpan.FromSeconds(2)),
"the primary dispose never waited for the in-flight load");
Exception? secondError = null;
var secondDisposeThread = new Thread(() =>
{
try
{
streamer.Dispose();
}
catch (Exception error)
{
secondError = error;
}
})
{
IsBackground = true,
Name = "LandblockStreamer concurrent dispose contract",
};
secondDisposeThread.Start();
Assert.True(SpinWait.SpinUntil(
() => (secondDisposeThread.ThreadState & ThreadState.WaitSleepJoin) != 0,
TimeSpan.FromSeconds(2)),
"the concurrent dispose never waited for primary disposal");
release.Set();
await Task.WhenAll(firstDispose, secondDispose).WaitAsync(TimeSpan.FromSeconds(2));
Assert.True(firstDisposeThread.Join(TimeSpan.FromSeconds(2)));
Assert.True(secondDisposeThread.Join(TimeSpan.FromSeconds(2)));
Assert.Null(firstError);
Assert.Null(secondError);
}
finally
{