Public Access
Tests: prove the device-key probe runs off the caller's thread by blocking it, not by comparing thread ids
TheChoiceIsMadeOnce_OffTheCallersThread failed on CI with the code doing the right thing. It compared the probe's managed thread id with the test's, but the test runs on a pool thread, and that same thread is free to pick up the queued probe as soon as the test awaits. The probe now waits on a release the test only gives once construction has returned. A probe run on the constructing thread could never be released, and the test fails after ten seconds instead of passing by luck. Twenty runs pass; an inline probe fails it.
This commit is contained in:
1 parent
96dd176121
commit
e240c0cf26
1 file changed
+16
-5
@@ -15,28 +15,39 @@ public sealed class ProbedDeviceKeyStoreTests
|
||||
{
|
||||
private static CancellationToken Token => TestContext.Current.CancellationToken;
|
||||
|
||||
/// <remarks>
|
||||
/// "Off the caller's thread" is proved by blocking, not by comparing thread ids. The probe waits for a
|
||||
/// release the test only gives once construction has returned, so a probe run on the thread that
|
||||
/// constructed the store could never be released and times out instead. Thread ids proved nothing: the
|
||||
/// test itself runs on a pool thread, and that same thread is free to pick up the queued probe the
|
||||
/// moment the test awaits, which is what failed on CI with the code doing exactly the right thing.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheChoiceIsMadeOnce_OffTheCallersThread()
|
||||
{
|
||||
var asking = Environment.CurrentManagedThreadId;
|
||||
var probes = 0;
|
||||
var probedOn = 0;
|
||||
var releasedInTime = false;
|
||||
|
||||
using var release = new ManualResetEventSlim();
|
||||
var inner = Substitute.For<IDeviceKeyStore>();
|
||||
|
||||
var store = new ProbedDeviceKeyStore(() =>
|
||||
{
|
||||
Interlocked.Increment(ref probes);
|
||||
probedOn = Environment.CurrentManagedThreadId;
|
||||
releasedInTime = release.Wait(TimeSpan.FromSeconds(10));
|
||||
return inner;
|
||||
});
|
||||
|
||||
(await store.IsAvailableAsync(Token)).ShouldBeTrue();
|
||||
var available = store.IsAvailableAsync(Token);
|
||||
available.IsCompleted.ShouldBeFalse("nothing has released the probe yet");
|
||||
release.Set();
|
||||
|
||||
(await available).ShouldBeTrue();
|
||||
(await store.IsAvailableAsync(Token)).ShouldBeTrue();
|
||||
await store.TryLoadAsync(Token);
|
||||
|
||||
releasedInTime.ShouldBeTrue("the probe ran on the thread that asked, so nothing could release it");
|
||||
probes.ShouldBe(1);
|
||||
probedOn.ShouldNotBe(asking);
|
||||
|
||||
// Availability is the choice, not a second probe through the store it chose.
|
||||
await inner.DidNotReceive().IsAvailableAsync(Arg.Any<CancellationToken>());
|
||||
|
||||
Reference in new issue
Block a user