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;
|
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]
|
[Fact]
|
||||||
public async Task TheChoiceIsMadeOnce_OffTheCallersThread()
|
public async Task TheChoiceIsMadeOnce_OffTheCallersThread()
|
||||||
{
|
{
|
||||||
var asking = Environment.CurrentManagedThreadId;
|
|
||||||
var probes = 0;
|
var probes = 0;
|
||||||
var probedOn = 0;
|
var releasedInTime = false;
|
||||||
|
|
||||||
|
using var release = new ManualResetEventSlim();
|
||||||
var inner = Substitute.For<IDeviceKeyStore>();
|
var inner = Substitute.For<IDeviceKeyStore>();
|
||||||
|
|
||||||
var store = new ProbedDeviceKeyStore(() =>
|
var store = new ProbedDeviceKeyStore(() =>
|
||||||
{
|
{
|
||||||
Interlocked.Increment(ref probes);
|
Interlocked.Increment(ref probes);
|
||||||
probedOn = Environment.CurrentManagedThreadId;
|
releasedInTime = release.Wait(TimeSpan.FromSeconds(10));
|
||||||
return inner;
|
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.IsAvailableAsync(Token)).ShouldBeTrue();
|
||||||
await store.TryLoadAsync(Token);
|
await store.TryLoadAsync(Token);
|
||||||
|
|
||||||
|
releasedInTime.ShouldBeTrue("the probe ran on the thread that asked, so nothing could release it");
|
||||||
probes.ShouldBe(1);
|
probes.ShouldBe(1);
|
||||||
probedOn.ShouldNotBe(asking);
|
|
||||||
|
|
||||||
// Availability is the choice, not a second probe through the store it chose.
|
// Availability is the choice, not a second probe through the store it chose.
|
||||||
await inner.DidNotReceive().IsAvailableAsync(Arg.Any<CancellationToken>());
|
await inner.DidNotReceive().IsAvailableAsync(Arg.Any<CancellationToken>());
|
||||||
|
|||||||
Reference in new issue
Block a user