Skip to content

Reflection mode: SourceRegistrar.IsEnabled reset to true by module initializers of late-loaded assemblies #7089

Closed

Description

@thomhurst

Summary

In reflection mode, SourceRegistrar.IsEnabled can switch back to true partway through a run. The engine sets it to false at startup, but any assembly compiled with the TUnit source generator that is first touched later (for example TUnit.Playwright, TUnit.FsCheck, or a user's shared test library) runs its generated module initializer, which sets SourceRegistrar.IsEnabled = true. Code that checks SourceRegistrar.IsEnabled after that point sees the wrong mode.

The clearest visible effect: in reflection mode, closed generic test classes fail with

InvalidOperationException: Cannot use reflection-based hook discovery when source generation is enabled

Steps to reproduce

  1. Use a test project that references a library built with the TUnit source generator and uses one of its attributes in a test, for example tests/TUnit.UnitTests, which references TUnit.Playwright.

  2. Add a generic test class:

    [GenerateGenericTest(typeof(int))]
    public class GenericRepro<T>
    {
        [Test]
        public void Runs() { }
    }
  3. Run it in reflection mode: dotnet run -f net10.0 -- --reflection --treenode-filter "/*/*/GenericRepro*/*".

Expected behaviour

The test passes, and SourceRegistrar.IsEnabled stays false for the whole reflection-mode run, as it does in source-generation mode, where the same test passes.

Actual behaviour

The test fails with the InvalidOperationException above. A test that reads SourceRegistrar.IsEnabled during a --reflection run gets True.

Root cause

InfrastructureGenerator emits a [ModuleInitializer] into every assembly that has the generator, and the first thing it does is global::TUnit.Core.SourceRegistrar.IsEnabled = true;. TUnitServiceProvider then sets the property to the mode it actually chose (ExecutionModeHelper.IsSourceGenerationMode). Nothing stops a later write. A module initializer runs the first time its module is touched, which can be long after startup. I traced the write in TUnit.UnitTests --reflection: reflection discovery called GetCustomAttributes on a test method, which created an attribute from TUnit.Playwright and so ran that assembly's module initializer. This happened after the engine had set the property to false.

Impact

Everything that reads SourceRegistrar.IsEnabled during discovery or execution can see the wrong mode once such an assembly loads:

  • ReflectionHookDiscoveryService.DiscoverInstanceHooksForType, which TestBuilder calls for every closed generic test class, throws, so those tests fail in reflection mode.
  • PropertyInjectionPlanBuilder.Build takes the source-generated property-injection path in reflection mode.
  • ModeDetector.IsSourceGenerationAvailable and user code that branches on SourceRegistrar.IsEnabled get the wrong answer.

Whether a run is affected depends on which assemblies happen to load, and when, so it is order dependent.

Activity

  1. added a commit that references this issue on Oct 18, 2026
    540152e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions