Skip to content

Commit aa5f18d

Browse files
authored
fix IAsyncInitializer order when using nested property injection (#4036)
* fix IAsyncInitializer order when using nested property injection * fix: enhance direct property detection for injected properties in object graph discovery
1 parent e4cc4a2 commit aa5f18d

3 files changed

Lines changed: 498 additions & 2 deletions

File tree

CLAUDE.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,17 @@ dotnet test TUnit.Core.SourceGenerator.Tests
147147
148148
**Rule**: Only run TUnit.TestProject with explicit `--treenode-filter` to target specific tests or classes.
149149
150+
**IMPORTANT: Run filters ONE AT A TIME!** Using OR patterns (`Pattern1|Pattern2`) can match thousands of unintended tests. Always run one specific filter per command:
151+
152+
```bash
153+
# ❌ WRONG - OR patterns can match too broadly
154+
--treenode-filter "/*/*/ClassA/*|/*/*/ClassB/*"
155+
156+
# ✅ CORRECT - Run separate commands for each class
157+
dotnet run -- --treenode-filter "/*/*/ClassA/*"
158+
dotnet run -- --treenode-filter "/*/*/ClassB/*"
159+
```
160+
150161
---
151162
152163
### Most Common Commands

TUnit.Core/Discovery/ObjectGraphDiscoverer.cs

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,12 @@ private static void TraverseInitializerProperties(
476476
/// Collects root-level objects (class args, method args, properties) from test details.
477477
/// Eliminates duplicate loops in DiscoverObjectGraph and DiscoverAndTrackObjects.
478478
/// </summary>
479+
/// <remarks>
480+
/// For injected properties, only DIRECT test class properties (including inherited) are added at depth 0.
481+
/// Nested properties (properties of injected objects) are discovered through normal
482+
/// graph traversal at appropriate depths (1+), ensuring correct initialization order
483+
/// for nested IAsyncInitializer dependencies. See GitHub issue #4032.
484+
/// </remarks>
479485
private static void CollectRootObjects(
480486
TestDetails testDetails,
481487
TryAddObjectFunc tryAdd,
@@ -488,8 +494,70 @@ private static void CollectRootObjects(
488494
// Process method arguments
489495
ProcessRootCollection(testDetails.TestMethodArguments, tryAdd, onRootObjectAdded, cancellationToken);
490496

491-
// Process injected property values
492-
ProcessRootCollection(testDetails.TestClassInjectedPropertyArguments.Values, tryAdd, onRootObjectAdded, cancellationToken);
497+
// Build set of types in the test class hierarchy (for identifying direct properties)
498+
var hierarchyTypes = GetTypeHierarchy(testDetails.ClassType);
499+
500+
// Process ONLY direct test class injected properties at depth 0.
501+
// Nested properties will be discovered through normal graph traversal at depth 1+.
502+
// This ensures proper initialization order for nested IAsyncInitializer dependencies.
503+
foreach (var kvp in testDetails.TestClassInjectedPropertyArguments)
504+
{
505+
cancellationToken.ThrowIfCancellationRequested();
506+
507+
if (kvp.Value == null)
508+
{
509+
continue;
510+
}
511+
512+
// Check if this property belongs to the test class hierarchy (not nested object properties)
513+
// Cache key format: "{DeclaringType.FullName}.{PropertyName}"
514+
if (IsDirectProperty(kvp.Key, hierarchyTypes))
515+
{
516+
if (tryAdd(kvp.Value, 0))
517+
{
518+
onRootObjectAdded(kvp.Value);
519+
}
520+
}
521+
}
522+
}
523+
524+
/// <summary>
525+
/// Gets all types in the inheritance hierarchy from the given type up to (but not including) object.
526+
/// </summary>
527+
private static HashSet<string> GetTypeHierarchy(Type type)
528+
{
529+
var result = new HashSet<string>();
530+
var currentType = type;
531+
532+
while (currentType != null && currentType != typeof(object))
533+
{
534+
if (currentType.FullName != null)
535+
{
536+
result.Add(currentType.FullName);
537+
}
538+
539+
currentType = currentType.BaseType;
540+
}
541+
542+
return result;
543+
}
544+
545+
/// <summary>
546+
/// Determines if a cache key represents a direct property (belonging to test class hierarchy)
547+
/// vs a nested property (belonging to an injected object).
548+
/// Cache key format: "{DeclaringType.FullName}.{PropertyName}"
549+
/// </summary>
550+
private static bool IsDirectProperty(string cacheKey, HashSet<string> hierarchyTypes)
551+
{
552+
// Find the last dot to separate type from property name
553+
var lastDotIndex = cacheKey.LastIndexOf('.');
554+
if (lastDotIndex <= 0)
555+
{
556+
return true; // Malformed key, treat as direct
557+
}
558+
559+
var declaringTypeName = cacheKey.Substring(0, lastDotIndex);
560+
return hierarchyTypes.Contains(declaringTypeName);
493561
}
494562

495563
/// <summary>

0 commit comments

Comments
 (0)