Skip to content

Commit 5a2976b

Browse files
committed
Clear SynchronizationContext in ToTask()
UI frameworks (WinForms/WPF/Avalonia) install a SynchronizationContext on the test thread. Without a running message pump, async IO in Verify's pipeline would post continuations to that context and deadlock. Clearing it synchronously in ToTask() before the pipeline starts prevents the capture. Adds a test that reproduces the scenario using a recording context.
1 parent efaf9ac commit 5a2976b

2 files changed

Lines changed: 67 additions & 2 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
public class SynchronizationContextTests
2+
{
3+
// WinForms/WPF/Avalonia install a SynchronizationContext on the current thread when a control is
4+
// created. During a test that context has no running message pump, so if Verify's async IO
5+
// captured it, the continuation that writes the received file for a new or mismatched snapshot
6+
// would be posted to a pump that never runs - deadlocking the pipeline and the awaiting test.
7+
// SettingsTask.ToTask clears the ambient context so the pipeline runs free of it. This reproduces
8+
// that scenario without a UI framework: it records whether the pipeline ever posts a continuation
9+
// to the caller's context (dispatching it onward so the test itself never hangs, even on regression).
10+
[Fact]
11+
public async Task DoesNotCaptureCallerSynchronizationContext()
12+
{
13+
var context = new RecordingSynchronizationContext();
14+
var original = SynchronizationContext.Current;
15+
SynchronizationContext.SetSynchronizationContext(context);
16+
Task task;
17+
try
18+
{
19+
var settings = new VerifySettings();
20+
settings.DisableDiff();
21+
// A binary target with no baseline writes the received file via IoHelpers.WriteStream,
22+
// whose CopyToAsync onto an async FileStream suspends - the exact async IO that captured
23+
// the UI context and deadlocked. (Strings use a synchronous write path and would not.)
24+
task = Verify(new MemoryStream(new byte[1024 * 1024]), "bin", settings).ToTask();
25+
}
26+
finally
27+
{
28+
// Restore before awaiting so the test's own continuations are unaffected by the fake context.
29+
SynchronizationContext.SetSynchronizationContext(original);
30+
}
31+
32+
await Assert.ThrowsAsync<VerifyException>(() => task);
33+
34+
Assert.False(
35+
context.Posted,
36+
"Verify captured the caller's SynchronizationContext; its async IO must run free of it.");
37+
}
38+
39+
class RecordingSynchronizationContext : SynchronizationContext
40+
{
41+
public volatile bool Posted;
42+
43+
public override void Post(SendOrPostCallback callback, object? state)
44+
{
45+
Posted = true;
46+
base.Post(callback, state);
47+
}
48+
}
49+
}

src/Verify/SettingsTask.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,24 @@ public VerifySettings CurrentSettings
312312
}
313313

314314
[Pure]
315-
public Task<VerifyResult> ToTask() =>
316-
task ??= buildTask(CurrentSettings);
315+
public Task<VerifyResult> ToTask()
316+
{
317+
if (task is not null)
318+
{
319+
return task;
320+
}
321+
322+
// Verify's pipeline performs async IO without ConfigureAwait(false), so it must not run under
323+
// a caller-supplied SynchronizationContext. UI frameworks (WinForms/WPF/Avalonia) install one
324+
// on the current thread when a control is created; during a test it has no running message
325+
// pump, so capturing it would deadlock the pipeline (and the awaiting test) on the received
326+
// file write of a new or mismatched snapshot. Clearing it here - synchronously, before the
327+
// task is built and before the caller's await captures a context - keeps the pipeline and the
328+
// caller's continuation free of any context the caller already had. UI converters additionally
329+
// restore the context around rendering, since Show() re-installs one mid-pipeline.
330+
SynchronizationContext.SetSynchronizationContext(null);
331+
return task = buildTask(CurrentSettings);
332+
}
317333

318334
[Pure]
319335
public ConfiguredTaskAwaitable<VerifyResult> ConfigureAwait(bool continueOnCapturedContext) =>

0 commit comments

Comments
 (0)