-
-
Notifications
You must be signed in to change notification settings - Fork 111
fix: force synchronous library module initializer completion #4630
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Conversation
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
Fixes timing issue where hooks were collected before library module initializers completed, resulting in 0 hooks found. **Problem:** Module initializers from different assemblies run asynchronously. When test assembly's module initializer referenced library types via typeof(), the library loaded but its module initializer was queued, not executed synchronously. HookDelegateBuilder.InitializeAsync() then ran before library hooks were registered to Sources.* collections. **Solution:** Use RuntimeHelpers.RunClassConstructor(type.TypeHandle) to force library module initializers to complete synchronously. Static constructors can only run AFTER module initializers, so this blocks until initialization finishes. AOT-compatible (uses statically-known types). **Changes:** - InfrastructureGenerator: Replace typeof() with RunClassConstructor() - Add logging for assembly initialization completion - Update documentation explaining the fix - Enhance Claude Code Review workflow to always comment and suggest architectural improvements **Snapshot Tests:** Updated verified snapshots for fully qualified type names in generated code.
Enhances logging to include the assembly name in all module initializer log messages, making it easy to identify which assembly's module initializer is running and when library assemblies are being loaded. Example output: [ModuleInitializer:TUnit.TestProject] Loading assembly containing: ... [ModuleInitializer:TUnit.TestProject] Assembly initialized: ... [ModuleInitializer:TUnit.TestProject.Library] TUnit infrastructure initializing... This helps debug timing issues between test and library assemblies.
Prevents TypeInitializationException in third-party packages that reference TUnit by ensuring TUnit.Core assembly is loaded before the module initializer attempts to access GlobalContext or other TUnit.Core types. This is cleaner than defensive try-catch as it ensures dependencies are properly loaded and logging actually works.
Wraps all logging statements in try-catch blocks for defense in depth. The explicit TUnit.Core loading (typeof) still fails fast if there's a real problem, but logging failures won't crash initialization. This provides: - Fail-fast on real issues (TUnit.Core can't load) - Graceful degradation (logging failures don't crash) - Better debugging (logs when available) - Safety for edge cases and third-party packages
…ption The explicit TUnit.Core loading was causing FileNotFoundException in third-party packages (like Verify.TUnit) during their build/test phase when TUnit.Core.dll isn't available yet in the output directory. By wrapping the loading in try-catch, we gracefully handle cases where TUnit.Core is unavailable while still providing the early-loading benefits for normal test execution.
MinVer automatically sets assembly version based on git tags, ensuring that locally built assemblies have matching versions with published packages. This fixes version mismatch issues where third-party packages like Verify.TUnit expect specific TUnit.Core versions (e.g., 1.12.65.0).
MinVer should only be a GlobalPackageReference, not also a PackageVersion.
The root cause: Third-party NuGet packages (like Verify.TUnit) may be built with older versions of TUnit.Core.SourceGenerator. When they run in CI with newer TUnit.Core versions, any unguarded TUnit.Core type access causes FileNotFoundException or version mismatch errors. The solution: Wrap EVERY TUnit.Core reference (logging, SourceRegistrar) in individual try-catch blocks. This makes the generated code resilient to: - TUnit.Core version mismatches - TUnit.Core not being available (transitive dependencies) - Build order issues (assembly not loaded yet) Critical changes: - SourceRegistrar.IsEnabled set FIRST before any logging (most important) - Each logging call wrapped separately so one failure doesn't block others - Assembly loading (RunClassConstructor) still works even if logging fails - Clear error messages in catch blocks explain why operations were skipped This ensures library assemblies with hooks can still load other assemblies even when TUnit.Core has version mismatches or isn't available.
This was referenced Feb 2, 2026
Open
Open
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.
Summary
Fixes the timing issue where hooks from library assemblies were collected before their module initializers completed, resulting in 0 hooks being found.
Problem
When using Microsoft.Testing.Platform, module initializers from different assemblies run asynchronously. The sequence was:
_ = typeof(LibraryType)HookDelegateBuilder.InitializeAsync()runs and collects hooksSources.*collectionsDebug output showed:
Solution
Use
RuntimeHelpers.RunClassConstructor(type.TypeHandle)to force library module initializers to complete synchronously. Static constructors can only run AFTER module initializers, so calling this blocks until initialization finishes.Generated code now:
✅ AOT Compatible - Uses only statically-known types and AOT-safe RuntimeHelpers API
Changes
InfrastructureGenerator.cs
_ = typeof(typeName)withRunClassConstructor()for synchronous initializationclaude-code-review.yml
Snapshot Tests
Test Plan
RunClassConstructor()Related Issues
Related to the recent fix in #4599 for library assembly hooks not executing.