Skip to content

fix: never adopt a test runner as the application assembly (#600) - #602

Merged
jeremydmiller merged 1 commit into
mainfrom
gh600/skip-test-runner-assemblies
Aug 2, 2026
Merged

fix: never adopt a test runner as the application assembly (#600)#602
jeremydmiller merged 1 commit into
mainfrom
gh600/skip-test-runner-assemblies

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Fixes #600.

The defect

JasperFxOptions.DetermineCallingAssembly() walks out of JasperFx to the first frame that isn't System* / Microsoft* / ReSharperTestRunner*. Under an async test fixture the frames between JasperFx and the test class belong to the test runner, so xunit.v3.core satisfies that filter and is returned. Every consumer that uses JasperFxOptions.ApplicationAssembly for type discovery then scans an assembly containing none of the suite's types.

The ReSharperTestRunner entry shows this class of problem was already known — xUnit and friends were simply never added. The HasReferenceToJasperFxTool(Assembly.GetEntryAssembly()) short-circuit above the walk doesn't save you either: it returns False for an ordinary test project, so the stack walk always runs.

Root cause of JasperFx/wolverine#3776. Wolverine has been defended at its own boundary; Marten and Polecat hosts have the same exposure.

The fix

Extend the skip list to the common runners, factored into JasperFxOptions.IsTestRunnerAssembly:

xunit · nunit · TUnit · MSTest · testhost · ReSharperTestRunner · JetBrains. · NCrunch.

Skipping the runner lets the walk continue out to the test assembly; where nothing else matches, the existing Assembly.GetEntryAssembly() fallback is the test assembly anyway.

The regression test

The check the issue asks for needs a runner frame to actually be on the stack, which a direct call from a test method never produces — I confirmed a naive await Task.Yield() + direct-call test passes on the unfixed code, so it would not have caught this.

src/TestRunnerStandIn is a tiny project whose only purpose is its assembly name — xunit.v3.stackwalk.standin. Calling DetermineCallingAssembly through it reproduces the layout deterministically:

JasperFx                        DetermineCallingAssembly   <- anchor
xunit.v3.stackwalk.standin      RunnerFrame.Invoke         <- was adopted; now skipped
CoreTests                       the test method            <- correct answer

Verified both directions: walks_past_a_test_runner_frame_out_to_the_test_assembly and never_adopts_a_test_runner_as_the_calling_assembly fail against the old walk and pass against the new one. Full CoreTests suite green (517 passed, 1 skipped).

Note, not fixed here

JasperFx.Core.TypeScanning.CallingAssembly.Find() — used by AssemblyScanner.TheCallingAssembly() — has the same defect (it only ignores System. / Microsoft. prefixes). Left alone deliberately: different API, different consumers, and out of scope for #600. Happy to fold it in or file it separately.

🤖 Generated with Claude Code

JasperFxOptions.DetermineCallingAssembly walks out of JasperFx to the first frame that
isn't System*/Microsoft*/ReSharperTestRunner*. Under an async test fixture the frames
between JasperFx and the test class belong to the test *runner*, so "xunit.v3.core"
satisfied that filter and was adopted as ApplicationAssembly. Every consumer that uses
it for type discovery then scanned an assembly containing none of the suite's types.

The symptom is entirely downstream and gives no hint of the cause -- in Wolverine's case
IndeterminateRoutesException; for Marten it would be a document or projection type simply
not being discovered. It is also intermittent rather than consistently wrong, because a
stack walk over async continuations is sensitive to frame layout.

Extends the skip list to the common runners (xunit, nunit, TUnit, MSTest, testhost,
ReSharper, JetBrains, NCrunch) so the walk continues out to the test assembly.

The regression test needs the runner frame to actually be on the stack, which a direct
call from a test method never produces. TestRunnerStandIn is a tiny project whose only
purpose is its assembly name -- "xunit.v3.stackwalk.standin" -- so calling JasperFx
through it reproduces the layout deterministically. Both new behaviour tests fail on the
old walk and pass on the new one.

Co-Authored-By: Claude Opus 5 (1M context) <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.

DetermineCallingAssembly adopts the test-runner assembly, so type discovery scans a runner instead of the test assembly

1 participant