-
-
Notifications
You must be signed in to change notification settings - Fork 108
perf: lazy test materialization with two-phase discovery #4300
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
Conversation
Add comprehensive design document for optimizing test discovery through lazy materialization. Key improvements: - Lightweight TestDescriptor struct for fast enumeration - Two-phase discovery: filter first, materialize matching tests only - Pre-computed filter hints at compile time - Deferred data source evaluation This architectural change targets ~20% improvement in single test execution time by avoiding eager materialization of tests that won't run due to filtering. Co-Authored-By: Claude Opus 4.5 <[email protected]>
…zation (Phase 1) Introduce lightweight test descriptor infrastructure to enable two-phase test discovery: 1. Fast enumeration via ITestDescriptorSource.EnumerateTestDescriptors() - Returns TestDescriptor structs with minimal identity/filter data - Pre-computed categories and properties for filtering - Lazy Materializer delegate for deferred full metadata creation 2. ITestSource updated with documentation for optional ITestDescriptorSource - Backward compatible: existing implementations work unchanged - New implementations can opt into optimization by implementing both TestDescriptor struct includes: - Test identity (TestId, ClassName, MethodName, FullyQualifiedName) - Location info (FilePath, LineNumber) - Filter hints (Categories, Properties) pre-extracted at compile time - HasDataSource flag for parameterized tests - RepeatCount from RepeatAttribute - Materializer delegate for lazy TestMetadata creation This is Phase 1 of the lazy test materialization redesign. Next: Update source generator to emit descriptors (Phase 2). Co-Authored-By: Claude Opus 4.5 <[email protected]>
Update TestMetadataGenerator to: - Generate ITestDescriptorSource implementation alongside ITestSource - Add EnumerateTestDescriptors() method for fast test enumeration - Pre-extract filter hints at compile time: - Categories from CategoryAttribute - Properties from PropertyAttribute - HasDataSource flag for parameterized tests - RepeatCount from RepeatAttribute Generated TestDescriptor includes: - Test identity (TestId, ClassName, MethodName, FullyQualifiedName) - Source location (FilePath, LineNumber) - Pre-computed filter arrays (no runtime attribute instantiation) - Materializer delegate referencing GetTestsAsync This enables two-phase discovery: 1. Fast enumeration via EnumerateTestDescriptors (filter before materialize) 2. Lazy materialization only for matching tests Co-Authored-By: Claude Opus 4.5 <[email protected]>
Add EnumerateDescriptors() and MaterializeFromDescriptorsAsync() to ITestDataCollector interface, enabling two-phase test discovery: 1. Phase 1: Fast enumeration via lightweight TestDescriptor structs 2. Phase 2: Lazy materialization only for tests that pass filtering Implementation: - AotTestDataCollector leverages ITestDescriptorSource when available - ReflectionTestDataCollector provides fallback descriptor enumeration - Both collectors support streaming materialization from descriptors This prepares the infrastructure for filter-before-materialize optimization. Co-Authored-By: Claude Opus 4.5 <[email protected]>
When filters with extractable hints are present, use two-phase discovery: 1. Enumerate lightweight TestDescriptors (no delegate/array allocations) 2. Filter descriptors using pre-computed hints from source generator 3. Only materialize TestMetadata for tests that pass filtering This optimization avoids creating full TestMetadata objects (with all their delegates, arrays, and allocations) for tests that won't run due to filters. Key changes: - Add CouldDescriptorMatch() to FilterHints for descriptor-level filtering - Update AotTestDataCollector to use two-phase discovery when available - Fallback to traditional collection for legacy sources or no filter hints Co-Authored-By: Claude Opus 4.5 <[email protected]>
…sion Adds DependsOn property to TestDescriptor containing pre-extracted dependency information from DependsOnAttribute. This enables dependency expansion during two-phase discovery, ensuring that when filtering tests, their dependencies are also materialized. Changes: - Add DependsOn string array to TestDescriptor - Extract DependsOn attributes at compile time in source generator - Expand dependencies transitively at descriptor level before materialization - Restore method-name filtering in CouldDescriptorMatch This fixes the issue where filtering to a single test would not include its dependencies (e.g., filtering to "DependentTest" would not include "BaseTest" that it depends on). Co-Authored-By: Claude Opus 4.5 <[email protected]>
SummaryImplements lazy test materialization through a two-phase discovery architecture (descriptors → filter → materialize) to improve performance when running filtered tests. Critical IssuesNone found ✅ Suggestions1. Missing DependsOn information in ReflectionTestDataCollector descriptorsIn ReflectionTestDataCollector.EnumerateDescriptors(), the generated descriptors have empty DependsOn arrays. This means dependency expansion in two-phase discovery won't work correctly for reflection mode. Why this matters: The two-phase discovery in AotTestDataCollector.CollectTestsWithTwoPhaseDiscoveryAsync() relies on descriptor.DependsOn to expand dependencies before materialization. Without this, filtered tests won't include their dependencies in reflection mode. Suggested fix: Either extract DependsOn values, document that dependency expansion is source-gen-only, or add a TODO comment explaining the limitation. 2. Consider ConfigureAwait consistencyIn AotTestDataCollector.CollectTestsWithTwoPhaseDiscoveryAsync(), the code uses ConfigureAwait(false) but the method signature doesn't accept a CancellationToken parameter itself. TUnit Rules Compliance✅ Rule 1 - Dual-Mode: Both AotTestDataCollector and ReflectionTestDataCollector updated Verdict✅ APPROVE - No critical issues Excellent performance optimization that maintains backward compatibility. The architecture is clean, implementation is thorough, and all snapshot tests updated properly. |
- Combine enumeration, indexing, and filtering into a single pass - Eliminate separate iteration over allDescriptors list - Add hasDependencies flag to skip dependency expansion when not needed - Use for-loop instead of foreach for DependsOn array iteration - Reduce memory allocations by avoiding intermediate list Co-Authored-By: Claude Opus 4.5 <[email protected]>
SummaryThis PR implements lazy test materialization through a two-phase discovery architecture that defers creation of full TestMetadata objects until after filtering, improving performance when running filtered tests. Critical IssuesNone found - all TUnit critical rules are properly followed. Suggestions
TUnit Rules Compliance
Architecture Highlights
VerdictApprove - Excellent performance optimization with proper dual-mode support, AOT compatibility, and backward compatibility. |
- Filter out assemblies that don't transitively reference TUnit.Core - Skip TUnit framework assemblies (TUnit.Core, TUnit.Engine, etc.) - Build reverse dependency graph to find all assemblies that could contain tests - Reduces assembly registrations from 26 to 1 in typical test projects This optimization avoids loading and scanning assemblies that cannot possibly contain TUnit tests (e.g., AutoFixture, Polly, Humanizer). Co-Authored-By: Claude Opus 4.5 <[email protected]>
SummaryThis PR implements lazy test materialization through a two-phase discovery architecture to improve test discovery performance when running filtered tests. Critical IssuesNone found ✅ ObservationsExcellent Design ✅
Code Quality
Verdict✅ APPROVE - No critical issues. This is a well-designed performance optimization that follows all TUnit mandatory rules. |
Add new public API entries for: - TestDescriptor struct - lazy materialization container - ITestDescriptorSource interface - source generation contract Co-Authored-By: Claude Opus 4.5 <[email protected]>
SummaryThis PR implements lazy test materialization through a two-phase discovery architecture that improves test discovery performance when running filtered tests. Critical IssuesNone found ✅ ObservationsTUnit Rules Compliance✅ Dual-Mode Implementation - The PR correctly implements the feature in both:
✅ Snapshot Testing - All .verified.txt files updated correctly (372 source generator snapshots). No .received.txt files committed. ✅ Public API Changes - New public types properly tracked:
✅ AOT Compatible - Proper [DynamicallyAccessedMembers] annotations on reflection code:
✅ Performance First - Excellent optimization:
Architecture Strengths
Minor Observations
Verdict✅ APPROVE - No critical issues This is a well-architected performance optimization that:
The implementation demonstrates careful attention to performance, backward compatibility, and TUnit's architectural constraints. |
Summary
This PR implements lazy test materialization through a two-phase discovery architecture that significantly improves test discovery performance, especially when running filtered tests.
Key Changes
TestDescriptorstruct - Lightweight test descriptor containing only filter-relevant metadata (class name, method name, categories, properties, dependencies) without full test materializationITestDescriptorSourceinterface - Enables test sources to provide fast descriptor enumeration viaEnumerateTestDescriptors()EnumerateTestDescriptors()implementation with pre-computed filter hints extracted at compile timeDependsOnproperty on descriptors enables dependency resolution before materialization, ensuring filtered tests include their dependenciesPerformance Impact
When running filtered tests (e.g.,
--treenode-filter "/*/*/MyClass/MyTest"), only tests matching the filter (plus their dependencies) are fully materialized. This avoids the cost of:TestMetadataobjectsFor unfiltered runs, the traditional collection path is used.
Backward Compatibility
ITestSourcecontinue to work (fallback to full materialization)Test plan
FilteredDependencyTests)🤖 Generated with Claude Code