Skip to content

Stop the STA dispatcher shutdown killing a theory's later rows - #609

Open
kellylford wants to merge 1 commit into
mainfrom
claude/hint-test-fix
Open

Stop the STA dispatcher shutdown killing a theory's later rows#609
kellylford wants to merge 1 commit into
mainfrom
claude/hint-test-fix

Conversation

@kellylford

Copy link
Copy Markdown
Owner

Xunit.StaFact gives each test method a fresh STA thread, but every data row of one [StaTheory] shares that thread. Measured directly with a probe: both rows of a two-row StaTheory report the same managed thread id, while each StaFact gets its own.

ShutDownStaDispatcherAttribute runs after every test case, so it shut that thread's Dispatcher down after the first row. The remaining rows then ran on a thread whose Dispatcher was dead, and their first WPF operation threw:

System.InvalidOperationException : Cannot perform requested operation because the Dispatcher shut down.
   at System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)

That is how AccountDialogHintTests.TheSmtpSslHintNamesBothPorts failed on its second row while passing on its first — reproducibly, 1 in 16, every time those classes were run as a subset:

QuickMail.Tests  Total: 16, Failed: 1   (x3 runs, same test every time)

It hides in full-suite runs, which is the worst shape for a bug like this: it looks like flakiness in whatever else happened to be running.

The fix

Shut down after the last row instead of after every one. The issue #211 protection is unchanged — the thread still never outlives its Dispatcher — and the rows in between keep a working one.

Rows are counted rather than detected: xUnit exposes no "is this the last case" flag, and IXunitTestMethod has no test-case collection to ask (checked its full property surface). When the count cannot be worked out — any data source other than [InlineData], whose rows are only known after running it — the old behaviour stands, since leaking a Dispatcher onto a dying thread is exactly the hazard #211 is about.

Verification

  • The two affected classes, which failed 1/16 on three consecutive runs before: 16/16 on three consecutive runs after.
  • Full suite: 3182 passed, 0 failed.
  • Only two [StaTheory] methods exist in the suite today, but this removes the trap for any future one whose later rows touch WPF — which fails silently and looks like someone else's flake.

🤖 Generated with Claude Code

Xunit.StaFact gives each test METHOD a fresh STA thread, but every data row of
one [StaTheory] shares that thread. Measured directly: both rows of a two-row
StaTheory report the same managed thread id, while each StaFact gets its own.

ShutDownStaDispatcherAttribute runs after every test case, so it shut that
thread's Dispatcher down after the FIRST row. The remaining rows then ran on a
thread whose Dispatcher was dead, and their first WPF operation threw "Cannot
perform requested operation because the Dispatcher shut down".

That is how AccountDialogHintTests.TheSmtpSslHintNamesBothPorts failed on its
second row while passing on its first - reproducibly, 1 in 16, whenever those
classes were run as a subset. It hid in full-suite runs because the two rows
happened to be scheduled so that the failure did not surface, which is the worst
shape for a bug like this: it looks like flakiness in whatever else was running.

Shut down after the LAST row instead. The issue #211 protection is unchanged -
the thread still never outlives its Dispatcher - and the rows in between keep a
working one. Rows are counted because xUnit exposes no "is this the last case"
flag and IXunitTestMethod has no test-case collection to ask; when the count
cannot be worked out (any data source other than [InlineData], whose rows are
only known after running it) the old behaviour stands, since leaking a
Dispatcher onto a dying thread is the hazard #211 is about.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant