Skip to content

Commit 01d301a

Browse files
authored
Better exception messages when injected data sources throw (#4108)
1 parent cc78222 commit 01d301a

File tree

1 file changed

+109
-63
lines changed

1 file changed

+109
-63
lines changed

TUnit.Engine/Building/TestBuilder.cs

Lines changed: 109 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -459,45 +459,56 @@ await _objectLifecycleService.RegisterObjectAsync(
459459
const string skipReason = "Data source returned no data";
460460

461461
Type[] resolvedClassGenericArgs;
462+
Exception? genericResolutionException = null;
462463
try
463464
{
464465
resolvedClassGenericArgs = metadata.TestClassType.IsGenericTypeDefinition
465466
? TryInferClassGenericsFromDataSources(metadata)
466467
: Type.EmptyTypes;
467468
}
468-
catch
469+
catch (Exception ex)
469470
{
470471
resolvedClassGenericArgs = Type.EmptyTypes;
472+
genericResolutionException = ex;
471473
}
472474

473-
var testData = new TestData
475+
// If generic type inference failed, create a failed test instead of skipped
476+
if (genericResolutionException != null)
474477
{
475-
TestClassInstanceFactory = () => Task.FromResult<object>(SkippedTestInstance.Instance),
476-
ClassDataSourceAttributeIndex = classDataAttributeIndex,
477-
ClassDataLoopIndex = classDataLoopIndex,
478-
ClassData = classData,
479-
MethodDataSourceAttributeIndex = methodDataAttributeIndex,
480-
MethodDataLoopIndex = 1, // Use 1 since we're creating a single skipped test
481-
MethodData = [],
482-
RepeatIndex = 0,
483-
InheritanceDepth = metadata.InheritanceDepth,
484-
ResolvedClassGenericArguments = resolvedClassGenericArgs,
485-
ResolvedMethodGenericArguments = Type.EmptyTypes
486-
};
487-
488-
var testSpecificContext = new TestBuilderContext
478+
var failedTest = await CreateFailedTestForDataGenerationError(metadata, genericResolutionException);
479+
tests.Add(failedTest);
480+
}
481+
else
489482
{
490-
TestMetadata = metadata.MethodMetadata,
491-
Events = new TestContextEvents(),
492-
StateBag = new ConcurrentDictionary<string, object?>(),
493-
ClassConstructor = testBuilderContext.ClassConstructor,
494-
DataSourceAttribute = methodDataSource,
495-
InitializedAttributes = attributes
496-
};
497-
498-
var test = await BuildTestAsync(metadata, testData, testSpecificContext);
499-
test.Context.SkipReason = skipReason;
500-
tests.Add(test);
483+
var testData = new TestData
484+
{
485+
TestClassInstanceFactory = () => Task.FromResult<object>(SkippedTestInstance.Instance),
486+
ClassDataSourceAttributeIndex = classDataAttributeIndex,
487+
ClassDataLoopIndex = classDataLoopIndex,
488+
ClassData = classData,
489+
MethodDataSourceAttributeIndex = methodDataAttributeIndex,
490+
MethodDataLoopIndex = 1, // Use 1 since we're creating a single skipped test
491+
MethodData = [],
492+
RepeatIndex = 0,
493+
InheritanceDepth = metadata.InheritanceDepth,
494+
ResolvedClassGenericArguments = resolvedClassGenericArgs,
495+
ResolvedMethodGenericArguments = Type.EmptyTypes
496+
};
497+
498+
var testSpecificContext = new TestBuilderContext
499+
{
500+
TestMetadata = metadata.MethodMetadata,
501+
Events = new TestContextEvents(),
502+
StateBag = new ConcurrentDictionary<string, object?>(),
503+
ClassConstructor = testBuilderContext.ClassConstructor,
504+
DataSourceAttribute = methodDataSource,
505+
InitializedAttributes = attributes
506+
};
507+
508+
var test = await BuildTestAsync(metadata, testData, testSpecificContext);
509+
test.Context.SkipReason = skipReason;
510+
tests.Add(test);
511+
}
501512
}
502513
}
503514
}
@@ -508,45 +519,56 @@ await _objectLifecycleService.RegisterObjectAsync(
508519
const string skipReason = "Data source returned no data";
509520

510521
Type[] resolvedClassGenericArgs;
522+
Exception? genericResolutionException = null;
511523
try
512524
{
513525
resolvedClassGenericArgs = metadata.TestClassType.IsGenericTypeDefinition
514526
? TryInferClassGenericsFromDataSources(metadata)
515527
: Type.EmptyTypes;
516528
}
517-
catch
529+
catch (Exception ex)
518530
{
519531
resolvedClassGenericArgs = Type.EmptyTypes;
532+
genericResolutionException = ex;
520533
}
521534

522-
var testData = new TestData
535+
// If generic type inference failed, create a failed test instead of skipped
536+
if (genericResolutionException != null)
523537
{
524-
TestClassInstanceFactory = () => Task.FromResult<object>(SkippedTestInstance.Instance),
525-
ClassDataSourceAttributeIndex = classDataAttributeIndex,
526-
ClassDataLoopIndex = 1, // Use 1 since we're creating a single skipped test
527-
ClassData = [],
528-
MethodDataSourceAttributeIndex = 0,
529-
MethodDataLoopIndex = 0,
530-
MethodData = [],
531-
RepeatIndex = 0,
532-
InheritanceDepth = metadata.InheritanceDepth,
533-
ResolvedClassGenericArguments = resolvedClassGenericArgs,
534-
ResolvedMethodGenericArguments = Type.EmptyTypes
535-
};
536-
537-
var testSpecificContext = new TestBuilderContext
538+
var failedTest = await CreateFailedTestForDataGenerationError(metadata, genericResolutionException);
539+
tests.Add(failedTest);
540+
}
541+
else
538542
{
539-
TestMetadata = metadata.MethodMetadata,
540-
Events = new TestContextEvents(),
541-
StateBag = new ConcurrentDictionary<string, object?>(),
542-
ClassConstructor = testBuilderContext.ClassConstructor,
543-
DataSourceAttribute = classDataSource,
544-
InitializedAttributes = attributes
545-
};
546-
547-
var test = await BuildTestAsync(metadata, testData, testSpecificContext);
548-
test.Context.SkipReason = skipReason;
549-
tests.Add(test);
543+
var testData = new TestData
544+
{
545+
TestClassInstanceFactory = () => Task.FromResult<object>(SkippedTestInstance.Instance),
546+
ClassDataSourceAttributeIndex = classDataAttributeIndex,
547+
ClassDataLoopIndex = 1, // Use 1 since we're creating a single skipped test
548+
ClassData = [],
549+
MethodDataSourceAttributeIndex = 0,
550+
MethodDataLoopIndex = 0,
551+
MethodData = [],
552+
RepeatIndex = 0,
553+
InheritanceDepth = metadata.InheritanceDepth,
554+
ResolvedClassGenericArguments = resolvedClassGenericArgs,
555+
ResolvedMethodGenericArguments = Type.EmptyTypes
556+
};
557+
558+
var testSpecificContext = new TestBuilderContext
559+
{
560+
TestMetadata = metadata.MethodMetadata,
561+
Events = new TestContextEvents(),
562+
StateBag = new ConcurrentDictionary<string, object?>(),
563+
ClassConstructor = testBuilderContext.ClassConstructor,
564+
DataSourceAttribute = classDataSource,
565+
InitializedAttributes = attributes
566+
};
567+
568+
var test = await BuildTestAsync(metadata, testData, testSpecificContext);
569+
test.Context.SkipReason = skipReason;
570+
tests.Add(test);
571+
}
550572
}
551573
}
552574

@@ -1314,6 +1336,26 @@ internal class TestData
13141336
public Type[] ResolvedMethodGenericArguments { get; set; } = Type.EmptyTypes;
13151337
}
13161338

1339+
/// <summary>
1340+
/// Result of attempting to create an instance for method data sources.
1341+
/// Captures either success with an instance or failure with the exception.
1342+
/// </summary>
1343+
private readonly struct InstanceCreationResult
1344+
{
1345+
public object? Instance { get; }
1346+
public Exception? Exception { get; }
1347+
public bool Success => Exception == null;
1348+
1349+
private InstanceCreationResult(object? instance, Exception? exception)
1350+
{
1351+
Instance = instance;
1352+
Exception = exception;
1353+
}
1354+
1355+
public static InstanceCreationResult CreateSuccess(object? instance) => new(instance, null);
1356+
public static InstanceCreationResult CreateFailure(Exception exception) => new(null, exception);
1357+
}
1358+
13171359
#if NET6_0_OR_GREATER
13181360
[RequiresUnreferencedCode("Test building in reflection mode uses generic type resolution which requires unreferenced code")]
13191361
#endif
@@ -1403,14 +1445,18 @@ public async IAsyncEnumerable<AbstractExecutableTest> BuildTestsStreamingAsync(
14031445

14041446
if (needsInstanceForMethodDataSources)
14051447
{
1406-
instanceForMethodDataSources = await CreateInstanceForMethodDataSources(
1448+
var instanceResult = await CreateInstanceForMethodDataSources(
14071449
metadata, classDataAttributeIndex, classDataLoopIndex, classData);
14081450

1409-
if (instanceForMethodDataSources == null)
1451+
if (!instanceResult.Success)
14101452
{
1411-
continue; // Skip if instance creation failed
1453+
// Yield a failed test instead of silently skipping
1454+
yield return await CreateFailedTestForInstanceDataSourceError(metadata, instanceResult.Exception!);
1455+
continue;
14121456
}
14131457

1458+
instanceForMethodDataSources = instanceResult.Instance!;
1459+
14141460
// Initialize property data sources on the early instance so that
14151461
// method data sources can access fully-initialized properties.
14161462
var tempObjectBag = new ConcurrentDictionary<string, object?>();
@@ -1472,7 +1518,7 @@ await _objectLifecycleService.RegisterObjectAsync(
14721518
#if NET6_0_OR_GREATER
14731519
[RequiresUnreferencedCode("Generic type resolution for instance creation uses reflection")]
14741520
#endif
1475-
private Task<object?> CreateInstanceForMethodDataSources(
1521+
private Task<InstanceCreationResult> CreateInstanceForMethodDataSources(
14761522
TestMetadata metadata, int classDataAttributeIndex, int classDataLoopIndex, object?[] classData)
14771523
{
14781524
try
@@ -1495,22 +1541,22 @@ await _objectLifecycleService.RegisterObjectAsync(
14951541
try
14961542
{
14971543
var resolution = TestGenericTypeResolver.Resolve(metadata, tempTestData);
1498-
return Task.FromResult<object?>(metadata.InstanceFactory(resolution.ResolvedClassGenericArguments, classData));
1544+
return Task.FromResult(InstanceCreationResult.CreateSuccess(metadata.InstanceFactory(resolution.ResolvedClassGenericArguments, classData)));
14991545
}
15001546
catch (GenericTypeResolutionException) when (classData.Length == 0)
15011547
{
15021548
var resolvedTypes = TryInferClassGenericsFromDataSources(metadata);
1503-
return Task.FromResult<object?>(metadata.InstanceFactory(resolvedTypes, classData));
1549+
return Task.FromResult(InstanceCreationResult.CreateSuccess(metadata.InstanceFactory(resolvedTypes, classData)));
15041550
}
15051551
}
15061552
else
15071553
{
1508-
return Task.FromResult<object?>(metadata.InstanceFactory([], classData));
1554+
return Task.FromResult(InstanceCreationResult.CreateSuccess(metadata.InstanceFactory([], classData)));
15091555
}
15101556
}
1511-
catch
1557+
catch (Exception ex)
15121558
{
1513-
return Task.FromResult<object?>(null);
1559+
return Task.FromResult(InstanceCreationResult.CreateFailure(ex));
15141560
}
15151561
}
15161562

0 commit comments

Comments
 (0)