-
-
Notifications
You must be signed in to change notification settings - Fork 111
feat: make dependencies available in ITestRegisteredEventReceiver #4589
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
feat: make dependencies available in ITestRegisteredEventReceiver #4589
Conversation
SummaryMakes dependencies (including transitive ones) available to ITestRegisteredEventReceiver and ITestDiscoveryEventReceiver by deferring event invocation until after dependency resolution. Critical IssuesNone found ✅ Suggestions1. Performance consideration in discovery (non-blocking) The change in TestDiscoveryService.cs:77 removes filter pre-filtering. While the PR body correctly identifies this as a bug fix ("metadata was pre-filtered before dependency expansion"), this means discovery will now materialize ALL test metadata before filtering, which could impact discovery performance on large codebases when narrow filters are used. This is the correct fix for the dependency expansion bug, but consider monitoring discovery performance in very large test suites. The tradeoff (correctness vs. performance) is appropriate here. 2. HashSet allocations per test (minor) In PopulateDependencies (TestBuilder.cs:972-996), each test creates new HashSet instances. This is called once per test during discovery. For large test suites with many dependencies, consider using an object pool for these collections (similar to the old HashSetPool pattern that was removed from TestCoordinator). However, since this only runs once during discovery (not in the hot execution path), the impact is minimal. 3. Test coverage looks solid The new tests in DependenciesAvailableInEventReceiverTests.cs comprehensively verify:
Great test coverage for the new feature! TUnit Rules Compliance✅ Dual-mode: Changes only affect TUnit.Engine (reflection mode). Source generator mode doesn't need updates. Verdict✅ APPROVE - Clean implementation that correctly solves the dependency availability issue. The filter pre-optimization removal is a necessary bug fix with acceptable performance tradeoffs. |
Restructure test building flow to defer ITestRegisteredEventReceiver and ITestDiscoveryEventReceiver invocations until after dependencies are resolved. This allows users to access resolved dependencies (including transitive ones) in their event receivers, enabling scenarios like the "Focus" feature where tests can be conditionally skipped based on dependency relationships. Changes: - Split test argument registration from event receiver invocation - Add InvokePostResolutionEventsAsync to fire events after dependency resolution - Populate TestContext._dependencies before firing registration events - Fix bug where metadata was pre-filtered before dependency expansion - Remove duplicate dependency collection from TestCoordinator Closes #4584
2cf39c6 to
2a34092
Compare
|
Review of PR 4589 - See detailed analysis below |
SummaryThis PR restructures test building to defer event receiver invocations until after dependencies are resolved, enabling ITestRegisteredEventReceiver and ITestDiscoveryEventReceiver to access dependency information. Critical Issues Found |
Issue 1: Potential duplicate dictionary keys in AotTestDataCollector.cs:102Location: TUnit.Engine/Building/Collectors/AotTestDataCollector.cs:98-102 In the ExpandSourcesForDependencies method, this code creates duplicate dictionary entries. If multiple generic variations exist (e.g., MyClass with both T and T,U), the non-generic key will point to whichever was processed last, potentially causing incorrect dependency resolution. Recommendation: Either document this intentional last-wins behavior, or use a multi-value dictionary structure if all generic variations should be discoverable. |
Issue 2: String allocations in hot path (AotTestDataCollector.cs:120-125)The ExpandSourcesForDependencies method uses Substring operations during test discovery, which is a hot path per TUnit Rule 4. Each dependency creates string allocations. Recommendation: Use ReadOnlySpan with AsSpan() to avoid allocations and improve performance during test discovery. |
Suggestions
Tests✅ Excellent test coverage in DependenciesAvailableInEventReceiverTests.cs covering same-class, cross-class, and transitive dependencies. |
Verdict |
- Fix ValidateClassTypeConstraints and ValidateTypeParameterConstraints to properly handle self-referential generic interface constraints (e.g., where T : IComparable<T>) - Add SubstituteTypeParameters helper to substitute type parameters with actual type arguments in constraint types - Add TypeImplementsInterface helper for proper interface implementation checking including constructed generic interfaces - Mark MethodDataSourceWithPropertyInjectionTest as expected failure since MethodDataSource is evaluated during discovery before property injection - Add comprehensive generic constraint validation tests
SummaryThis PR restructures the test building flow to defer ITestRegisteredEventReceiver and ITestDiscoveryEventReceiver invocations until after dependencies are resolved, enabling event receivers to access dependency information (including transitive dependencies). Critical Issues1. Potential string parsing bug in AotTestDataCollector.cs (line 188) The code uses var backtickIndex = kvp.Key.Name.IndexOf('`');
if (backtickIndex > 0)
{
sourcesByClassName[kvp.Key.Name.Substring(0, backtickIndex)] = kvp;
}This is actually safe because of the var separatorIndex = dependency.IndexOf(':');
if (separatorIndex > 0) // Cross-class dependency
{
var depClassName = dependency.Substring(0, separatorIndex);This assumes the format is always "ClassName:MethodName". If the dependency format changes or is malformed, this could throw. Consider adding a comment explaining the expected format or add defensive checks. 2. No verification of dual-mode implementation According to TUnit's Rule 1 (Dual-Mode), changes to test discovery/metadata collection must work in both:
This PR modifies test discovery flow in TUnit.Engine (TestDiscoveryService.cs, TestBuilder.cs) and adds new logic to AotTestDataCollector.cs. However, there are also changes to TestMetadataGenerator.cs (source generator) that appear unrelated to the main feature - they fix generic constraint validation. Question: Are the generic constraint validation fixes in TestMetadataGenerator.cs related to making this feature work in both modes, or are they separate bug fixes that should be in their own PR? The main feature (deferring event receivers until after dependency resolution) appears to only modify the Engine side. The source generator changes seem to be for generic constraint validation. If these are separate concerns, they should be split into separate PRs per best practices. 3. Performance concern: String allocations in hot path In AotTestDataCollector.ExpandSourcesForDependencies (line 188, 206, 249), the code uses // Instead of:
var depClassName = dependency.Substring(0, separatorIndex);
// Consider:
var depClassName = dependency.AsSpan(0, separatorIndex);This is in test discovery which processes potentially millions of tests. Suggestions1. Documentation of dependency format Add XML documentation or a comment explaining the dependency string format ("ClassName:MethodName" or ":MethodName" for same-class) at the top of ExpandSourcesForDependencies. 2. Consider extracting dependency parsing logic The dependency format parsing ("ClassName:MethodName") is repeated in multiple places. Consider extracting to a helper method: private static (string? ClassName, string MethodName) ParseDependency(string dependency)
{
var separatorIndex = dependency.IndexOf(':');
if (separatorIndex <= 0)
return (null, dependency);
return (dependency.Substring(0, separatorIndex), dependency.Substring(separatorIndex + 1));
}3. Test coverage for malformed dependency strings Consider adding tests for edge cases:
Verdict |
- Fix MethodDataSourceAttribute to not override explicitly provided ClassProvidingDataSource even when it's a static class (static classes are abstract in IL but we can still invoke their static members) - Split tests that use instance data sources dependent on property injection into separate classes, as these cannot work in reflection mode due to architectural limitations (property injection happens after construction, but data sources are evaluated during discovery)
SummaryThis PR restructures the test building flow to defer Critical IssuesNone found ✅ After thorough review, this PR correctly implements the feature. Previous comments have raised valid concerns, but upon closer inspection:
TUnit Rules Compliance✅ Dual-Mode (Rule 1): This PR primarily affects the Engine (reflection mode). The source generator changes are orthogonal (generic constraint fixes). Both modes handle test discovery/dependency resolution independently and correctly. ✅ Snapshot Testing (Rule 2): No source generator output changes require snapshots. The source generator changes are internal validation logic only. ✅ No VSTest (Rule 3): No VSTest dependencies added. ✅ Performance First (Rule 4): The critical execution hot path in ✅ AOT Compatible (Rule 5): Proper Architecture QualityThe refactoring is well-structured:
Verdict✅ APPROVE - This is a clean implementation that correctly solves the dependency availability issue while maintaining or improving performance. The dependency collection optimization (removing duplicate work from TestCoordinator) is a nice bonus. |
- Add InstanceFactory callback to DataGeneratorMetadata for creating test class instances with property injection during discovery - Add ReflectionInstanceFactory to create instances with basic property injection for reflection mode discovery - Update MethodDataSourceAttribute to use InstanceFactory when available for instance method/property data sources - Split tests with instance data sources dependent on IAsyncInitializer into separate classes since they can only work in source-generated mode (IAsyncInitializer runs during execution, not discovery) The remaining 2 tests excluded from [EngineTest=Pass] filter: - ErrTest.MyTest: Uses instance property data source dependent on property-injected fixture - IAsyncInitializerTests.ForNTestCases_ShouldInitNTimes: Uses instance method data source that reads from IAsyncInitializer-populated field
SummaryRestructures test building to defer Critical IssuesNone found ✅ Suggestions1. ReflectionInstanceFactory Cache Management (Minor Performance Concern)Location: The Suggestion: Ensure 2. Generic Constraint Type Substitution Edge CasesLocation: The new
Suggestion: Add defensive error handling or document these limitations if they're acceptable trade-offs. 3. Dual-Mode Consistency for Instance CreationLocation: The PR adds property injection for reflection mode via Question: Does source-generated mode handle property injection for instance data sources differently? If so, this might violate Rule 1: Dual-Mode from CLAUDE.md. The PR body mentions "Allows users to access resolved dependencies (including transitive ones)" but doesn't explicitly confirm both modes work identically. Suggestion: Verify that both modes handle property injection for instance data sources consistently, or document if this is reflection-mode-only behavior. 4. Test Changes Document Intentional FailuresLocation: Multiple test files Several tests were changed from
The comments explain these are design limitations where instance data sources depending on property injection can't work in reflection mode during discovery. This is good documentation, but raises the question: Should these scenarios fail gracefully with a clear error message rather than silently producing incorrect results? Users might be confused when tests work in source-gen but fail in reflection mode. Suggestion: Consider detecting this pattern and providing a diagnostic warning/error explaining the limitation. Architecture & DesignPositive Observations
TUnit Rules Compliance✅ Rule 1 (Dual-Mode): Both source-gen and reflection modes updated. Generic constraint validation applies to source-gen mode. Minor concern about instance creation (see Suggestion #3). ✅ Rule 2 (Snapshot Testing): No ✅ Rule 3 (No VSTest): No VSTest references introduced. ✅ Rule 4 (Performance): Uses ✅ Rule 5 (AOT Compatible): Proper Verdict✅ APPROVE - No critical issues This is a well-architected change that enables an important feature (dependency access in event receivers) while maintaining code quality. The suggestions are minor improvements and clarifications, none of which are blocking. |
SummaryThis PR makes dependencies available in ITestRegisteredEventReceiver, fixes generic constraint validation for substituted type parameters, enables property injection for instance data sources in reflection mode, and refactors when event receivers are invoked during test discovery. Critical IssuesNone found ✅ Suggestions1. Consider caching implications in ReflectionInstanceFactory (TUnit.Engine/Discovery/ReflectionInstanceFactory.cs:693)The new
2. Dual-mode validation for InstanceFactory (TUnit.Core/Models/DataGeneratorMetadata.cs:352)The new
3. Public API snapshot verificationThe diff includes updates to 4. Performance consideration: Dependency collection in TestBuilder (TUnit.Engine/Building/TestBuilder.cs:572-597)The 5. Test expectation changes are well-documentedSeveral test files now have Verdict✅ APPROVE - No critical issues The PR makes meaningful improvements to TUnit's dependency handling and reflection mode property injection. Changes follow TUnit's critical rules:
The only suggestion is to ensure |
Summary
ITestRegisteredEventReceiverandITestDiscoveryEventReceiverinvocations until after dependencies are resolvedChanges
InvokeTestRegisteredEventReceiversAsyncintoRegisterTestArgumentsAsync(property injection during building) andInvokeTestRegisteredReceiversAsync(receiver invocation after dependencies resolved). AddedInvokePostResolutionEventsAsync()that populatesTestContext._dependenciesand fires events.InvokePostResolutionEventsAsync()to interfaceInvokePostResolutionEventsAsync()methodInvokePostResolutionEventsAsync()after dependency resolution. Fixed bug where metadata was pre-filtered before dependency expansion.HashSetPoolparameterTest plan
DependenciesAvailableInEventReceiverTests.csverifying:DependsOnTestspassDependsOnTestsWithClasspass