fix: CallingAssembly.Find could adopt a test runner too (#600) - #605
Merged
Conversation
jeremydmiller
force-pushed
the
gh601/skip-critter-stack-assemblies
branch
from
August 2, 2026 13:29
4a31eb5 to
84be58d
Compare
jeremydmiller
force-pushed
the
gh600b/calling-assembly-runner
branch
from
August 2, 2026 13:31
e302d11 to
65b5519
Compare
jeremydmiller
changed the base branch from
gh601/skip-critter-stack-assemblies
to
main
August 2, 2026 13:36
Follow-up to the DetermineCallingAssembly fix, on the other stack walk. AssemblyScanner.TheCallingAssembly() resolves through CallingAssembly.Find(), which only ignored "System." and "Microsoft." prefixes -- so a scan configured from an async test could adopt the runner and then scan an assembly holding none of the suite's types. Find() rendered the stack as TEXT (Environment.StackTrace) and guessed each frame's assembly by trying to Assembly.Load progressively shorter dotted prefixes of the method name. That could only ever resolve an assembly whose name lined up with its namespace, which made it both blind and wrong in different places: it silently missed frames whose namespace differs from the assembly (xUnit's Xunit.v3 vs xunit.v3.core), and it faithfully resolved -- and adopted -- ones that do line up. NUnit's NUnit.Framework namespace matches its nunit.framework assembly exactly. Read the assembly off the frame instead. Exact, no speculative loads, and it drops a static List<string> of failed load attempts that every caller mutated without synchronisation. Behaviour is otherwise preserved: [IgnoreAssembly] is still honoured (that is why Find() skips JasperFx's own frames -- JasperFx carries the attribute), and the three existing tests pin the "innermost non-ignored caller" semantics unchanged. The stand-in's namespace now matches its assembly name, because the text-based walk keys on namespace and cannot be reproduced otherwise. Verified in Release as well as Debug, since these tests depend on frames the JIT is free to inline away. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jeremydmiller
force-pushed
the
gh600b/calling-assembly-runner
branch
from
August 2, 2026 13:36
65b5519 to
34085bb
Compare
This was referenced Aug 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #602 / #604, on the other stack walk. Noted as out of scope on #602; picking it up now.
The defect
AssemblyScanner.TheCallingAssembly()resolves throughCallingAssembly.Find(), which ignored onlySystem.andMicrosoft.prefixes. Same exposure as #600: a scan configured from an async test can adopt the runner and then scan an assembly holding none of the suite's types.But the implementation made it stranger than that.
Find()rendered the stack as text (Environment.StackTrace) and guessed each frame's assembly by trying toAssembly.Loadprogressively shorter dotted prefixes of the method name:That can only ever resolve an assembly whose name lines up with its namespace, which made it blind and wrong in different places:
Xunit.v3namespace vs itsxunit.v3.coreassembly;NUnit.Frameworknamespace matches itsnunit.frameworkassembly exactly.So this is a real case, not a theoretical one, and which runner you use decides whether you hit it.
Three further problems in the same method:
_missesis a plainstatic List<string>mutated by every caller with no synchronisation — concurrent scans race on it.stacktraceLine.Trim().Substring(3)throwsArgumentOutOfRangeExceptionon any line shorter than three characters.Assembly.Load.The fix
Read the assembly off the frame, the way
JasperFxOptions.DetermineCallingAssemblyalready does:Exact, no speculative loads, no shared mutable state, and
isSystemAssemblynow sharesJasperFxOptions.IsTestRunnerAssemblyso the two walks agree on what a runner is.Behaviour preserved
[IgnoreAssembly]is still honoured — that is precisely whyFind()skips JasperFx's own frames, since JasperFx carries[assembly: IgnoreAssembly](AssemblyScanner.cs:8). The three existing tests pin the "innermost non-ignored caller" semantics and are unchanged:use_current_assembly→ the test assemblyfrom_another_assembly→Widgets1skip_ignore_assembly→Widgets5, skipping[IgnoreAssembly]-markedWidgets4I did not add the #601 Critter Stack skip here.
Find()'s contract is the immediate caller,[IgnoreAssembly]is the established opt-out for this API, and neither Wolverine nor Marten callsTheCallingAssembly()— so a name list would be redundant where an attribute already does the job better.Tests
TestRunnerStandIn's namespace now matches its assembly name. That is required, not cosmetic: the text-based walk keys on namespace, so with a mismatched one the stand-in cannot reproduce the defect at all. The comment in the file explains it, and notes that the alignment mirrors NUnit rather than being a contrivance.walks_past_a_test_runner_frame_out_to_the_calling_assembly/never_adopts_a_test_runner_as_the_calling_assembly— both fail against the old implementation (it returnsxunit.v3.stackwalk.standin) and pass against the new one.find_is_safe_to_call_concurrently— guards the_missesrace.Verified in Release as well as Debug, since these tests depend on frames the JIT is free to inline away and CI builds Release. All five suites green: CoreTests 542, EventTests 657, CommandLineTests 295, CodegenTests 419, EventStoreTests 72.
🤖 Generated with Claude Code