Skip to content

Commit 595687c

Browse files
slang25thomhurst
authored andcommitted
Add TUnit.FsCheck library
1 parent 48ffa69 commit 595687c

13 files changed

Lines changed: 893 additions & 30 deletions

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<PackageVersion Include="CliWrap" Version="3.10.0" />
1414
<PackageVersion Include="EnumerableAsyncProcessor" Version="3.8.4" />
1515
<PackageVersion Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
16+
<PackageVersion Include="FsCheck" Version="3.3.2" />
1617
<PackageVersion Include="FSharp.Core" Version="10.0.101" />
1718
<PackageVersion Include="Humanizer" Version="3.0.1" />
1819
<PackageVersion Include="MessagePack" Version="3.1.4" />

TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs

Lines changed: 115 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
3636
.Where(static m => m is not null)
3737
.Combine(enabledProvider);
3838

39+
// Custom test attributes that inherit from BaseTestAttribute
40+
var customTestMethodsProvider = context.SyntaxProvider
41+
.CreateSyntaxProvider(
42+
predicate: static (node, _) => node is MethodDeclarationSyntax { AttributeLists.Count: > 0 },
43+
transform: static (ctx, _) => GetCustomTestMethodMetadata(ctx))
44+
.Where(static m => m is not null)
45+
.Combine(enabledProvider);
46+
3947
var inheritsTestsClassesProvider = context.SyntaxProvider
4048
.ForAttributeWithMetadataName(
4149
"TUnit.Core.InheritsTestsAttribute",
@@ -55,6 +63,17 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
5563
GenerateTestMethodSource(context, testMethod);
5664
});
5765

66+
context.RegisterSourceOutput(customTestMethodsProvider,
67+
static (context, data) =>
68+
{
69+
var (testMethod, isEnabled) = data;
70+
if (!isEnabled)
71+
{
72+
return;
73+
}
74+
GenerateTestMethodSource(context, testMethod);
75+
});
76+
5877
context.RegisterSourceOutput(inheritsTestsClassesProvider,
5978
static (context, data) =>
6079
{
@@ -67,6 +86,86 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
6786
});
6887
}
6988

89+
private static TestMethodMetadata? GetCustomTestMethodMetadata(GeneratorSyntaxContext context)
90+
{
91+
var methodSyntax = (MethodDeclarationSyntax)context.Node;
92+
var methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodSyntax) as IMethodSymbol;
93+
94+
if (methodSymbol == null)
95+
{
96+
return null;
97+
}
98+
99+
// Find the custom test attribute that inherits from BaseTestAttribute
100+
// Skip any attributes defined in TUnit.Core namespace (handled by built-in providers)
101+
AttributeData? testAttribute = null;
102+
foreach (var attr in methodSymbol.GetAttributes())
103+
{
104+
var attrType = attr.AttributeClass;
105+
if (attrType == null)
106+
{
107+
continue;
108+
}
109+
110+
// Skip built-in TUnit.Core attributes - they're handled by other providers
111+
if (attrType.ContainingNamespace?.ToDisplayString() == "TUnit.Core")
112+
{
113+
continue;
114+
}
115+
116+
var baseType = attrType.BaseType;
117+
while (baseType != null)
118+
{
119+
if (baseType.ToDisplayString() == "TUnit.Core.BaseTestAttribute")
120+
{
121+
testAttribute = attr;
122+
break;
123+
}
124+
baseType = baseType.BaseType;
125+
}
126+
if (testAttribute != null)
127+
{
128+
break;
129+
}
130+
}
131+
132+
if (testAttribute == null)
133+
{
134+
return null;
135+
}
136+
137+
var containingType = methodSymbol.ContainingType;
138+
139+
if (containingType == null)
140+
{
141+
return null;
142+
}
143+
144+
if (containingType.IsAbstract)
145+
{
146+
return null;
147+
}
148+
149+
var isGenericType = containingType is { IsGenericType: true, TypeParameters.Length: > 0 };
150+
var isGenericMethod = methodSymbol is { IsGenericMethod: true };
151+
152+
var (filePath, lineNumber) = GetTestMethodSourceLocation(methodSyntax, testAttribute);
153+
154+
return new TestMethodMetadata
155+
{
156+
MethodSymbol = methodSymbol,
157+
TypeSymbol = containingType,
158+
FilePath = filePath,
159+
LineNumber = lineNumber,
160+
TestAttribute = testAttribute,
161+
SemanticModel = context.SemanticModel,
162+
MethodSyntax = methodSyntax,
163+
IsGenericType = isGenericType,
164+
IsGenericMethod = isGenericMethod,
165+
MethodAttributes = methodSymbol.GetAttributes()
166+
};
167+
}
168+
70169
private static InheritsTestsClassMetadata? GetInheritsTestsClassMetadata(GeneratorAttributeSyntaxContext context)
71170
{
72171
var classSyntax = (ClassDeclarationSyntax)context.TargetNode;
@@ -85,7 +184,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
85184
{
86185
TypeSymbol = classSymbol,
87186
ClassSyntax = classSyntax,
88-
Context = context
187+
SemanticModel = context.SemanticModel
89188
};
90189
}
91190

@@ -120,7 +219,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
120219
FilePath = filePath,
121220
LineNumber = lineNumber,
122221
TestAttribute = context.Attributes.First(),
123-
Context = context,
222+
SemanticModel = context.SemanticModel,
124223
MethodSyntax = methodSyntax,
125224
IsGenericType = isGenericType,
126225
IsGenericMethod = isGenericMethod,
@@ -185,7 +284,7 @@ private static void GenerateInheritedTestSources(SourceProductionContext context
185284
FilePath = filePath,
186285
LineNumber = lineNumber,
187286
TestAttribute = testAttribute,
188-
Context = classInfo.Context, // Use class context to access Compilation
287+
SemanticModel = classInfo.SemanticModel, // Use class context to access Compilation
189288
MethodSyntax = null, // No syntax for inherited methods
190289
IsGenericType = typeForMetadata.IsGenericType,
191290
IsGenericMethod = (concreteMethod ?? method).IsGenericMethod,
@@ -228,14 +327,11 @@ private static void GenerateTestMethodSource(SourceProductionContext context, Te
228327
{
229328
try
230329
{
231-
if (testMethod?.MethodSymbol == null || testMethod.Context == null)
330+
if (testMethod?.MethodSymbol == null || testMethod.SemanticModel?.Compilation == null)
232331
{
233332
return;
234333
}
235334

236-
// Get compilation from semantic model instead of parameter
237-
var compilation = testMethod.Context.Value.SemanticModel.Compilation;
238-
239335
var writer = new CodeWriter();
240336
GenerateFileHeader(writer);
241337
GenerateTestMetadata(writer, testMethod);
@@ -274,7 +370,7 @@ private static void GenerateFileHeader(CodeWriter writer)
274370

275371
private static void GenerateTestMetadata(CodeWriter writer, TestMethodMetadata testMethod)
276372
{
277-
var compilation = testMethod.Context!.Value.SemanticModel.Compilation;
373+
var compilation = testMethod.SemanticModel?.Compilation!;
278374

279375
var className = testMethod.TypeSymbol.GloballyQualified();
280376
var methodName = testMethod.MethodSymbol.Name;
@@ -352,7 +448,7 @@ private static void GenerateSpecificGenericInstantiation(
352448
string combinationGuid,
353449
ImmutableArray<ITypeSymbol> typeArguments)
354450
{
355-
var compilation = testMethod.Context!.Value.SemanticModel.Compilation;
451+
var compilation = testMethod.SemanticModel?.Compilation!;
356452
var methodName = testMethod.MethodSymbol.Name;
357453
var typeArgsString = string.Join(", ", typeArguments.Select(t => t.GloballyQualified()));
358454
var instantiatedMethodName = $"{methodName}<{typeArgsString}>";
@@ -364,7 +460,7 @@ private static void GenerateSpecificGenericInstantiation(
364460
FilePath = testMethod.FilePath,
365461
LineNumber = testMethod.LineNumber,
366462
TestAttribute = testMethod.TestAttribute,
367-
Context = testMethod.Context,
463+
SemanticModel = testMethod.SemanticModel,
368464
MethodSyntax = testMethod.MethodSyntax,
369465
IsGenericType = testMethod.IsGenericType,
370466
IsGenericMethod = false, // We're creating a concrete instantiation
@@ -571,7 +667,7 @@ private static void GenerateTestMetadataInstance(CodeWriter writer, TestMethodMe
571667

572668
private static void GenerateMetadata(CodeWriter writer, TestMethodMetadata testMethod)
573669
{
574-
var compilation = testMethod.Context!.Value.SemanticModel.Compilation;
670+
var compilation = testMethod.SemanticModel?.Compilation!;
575671
var methodSymbol = testMethod.MethodSymbol;
576672

577673

@@ -617,7 +713,7 @@ private static void GenerateMetadata(CodeWriter writer, TestMethodMetadata testM
617713

618714
private static void GenerateMetadataForConcreteInstantiation(CodeWriter writer, TestMethodMetadata testMethod)
619715
{
620-
var compilation = testMethod.Context!.Value.SemanticModel.Compilation;
716+
var compilation = testMethod.SemanticModel?.Compilation!;
621717
var methodSymbol = testMethod.MethodSymbol;
622718

623719

@@ -669,7 +765,7 @@ private static void GenerateMetadataForConcreteInstantiation(CodeWriter writer,
669765

670766
private static void GenerateDataSources(CodeWriter writer, TestMethodMetadata testMethod)
671767
{
672-
var compilation = testMethod.Context!.Value.SemanticModel.Compilation;
768+
var compilation = testMethod.SemanticModel?.Compilation!;
673769
var methodSymbol = testMethod.MethodSymbol;
674770
var typeSymbol = testMethod.TypeSymbol;
675771

@@ -1573,7 +1669,7 @@ private static void GeneratePropertyInjections(CodeWriter writer, INamedTypeSymb
15731669

15741670
private static void GeneratePropertyDataSources(CodeWriter writer, TestMethodMetadata testMethod)
15751671
{
1576-
var compilation = testMethod.Context!.Value.SemanticModel.Compilation;
1672+
var compilation = testMethod.SemanticModel?.Compilation!;
15771673
var typeSymbol = testMethod.TypeSymbol;
15781674
var currentType = typeSymbol;
15791675
var processedProperties = new HashSet<string>();
@@ -2791,7 +2887,7 @@ private static void GenerateGenericTestWithConcreteTypes(
27912887
string className,
27922888
string combinationGuid)
27932889
{
2794-
var compilation = testMethod.Context!.Value.SemanticModel.Compilation;
2890+
var compilation = testMethod.SemanticModel?.Compilation!;
27952891
var methodName = testMethod.MethodSymbol.Name;
27962892

27972893
writer.AppendLine("// Create generic metadata with concrete type registrations");
@@ -4270,7 +4366,7 @@ private static void GenerateConcreteTestMetadata(
42704366
ITypeSymbol[] typeArguments,
42714367
AttributeData? specificArgumentsAttribute = null)
42724368
{
4273-
var compilation = testMethod.Context!.Value.SemanticModel.Compilation;
4369+
var compilation = testMethod.SemanticModel?.Compilation!;
42744370
var methodName = testMethod.MethodSymbol.Name;
42754371

42764372
// Separate class type arguments from method type arguments
@@ -4490,7 +4586,7 @@ private static void GenerateConcreteMetadataWithFilteredDataSources(
44904586
AttributeData? specificArgumentsAttribute,
44914587
ITypeSymbol[] typeArguments)
44924588
{
4493-
var compilation = testMethod.Context!.Value.SemanticModel.Compilation;
4589+
var compilation = testMethod.SemanticModel?.Compilation!;
44944590
var methodSymbol = testMethod.MethodSymbol;
44954591
var typeSymbol = testMethod.TypeSymbol;
44964592

@@ -4809,7 +4905,7 @@ private static void GenerateConcreteTestMetadataForNonGeneric(
48094905
AttributeData? classDataSourceAttribute,
48104906
AttributeData? methodDataSourceAttribute)
48114907
{
4812-
var compilation = testMethod.Context!.Value.SemanticModel.Compilation;
4908+
var compilation = testMethod.SemanticModel?.Compilation!;
48134909
var methodName = testMethod.MethodSymbol.Name;
48144910

48154911
writer.AppendLine($"var metadata = new global::TUnit.Core.TestMetadata<{className}>");
@@ -5002,6 +5098,6 @@ public class InheritsTestsClassMetadata
50025098
{
50035099
public required INamedTypeSymbol TypeSymbol { get; init; }
50045100
public required ClassDeclarationSyntax ClassSyntax { get; init; }
5005-
public GeneratorAttributeSyntaxContext Context { get; init; }
5101+
public SemanticModel SemanticModel { get; init; }
50065102
}
50075103

TUnit.Core.SourceGenerator/Models/TestMethodMetadata.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ public class TestMethodMetadata : IEquatable<TestMethodMetadata>
1111
{
1212
public required IMethodSymbol MethodSymbol { get; init; }
1313
public required INamedTypeSymbol TypeSymbol { get; init; }
14+
public required SemanticModel SemanticModel { get; init; }
1415
public required string FilePath { get; init; }
1516
public required int LineNumber { get; init; }
1617
public required AttributeData TestAttribute { get; init; }
17-
public GeneratorAttributeSyntaxContext? Context { get; init; }
1818
public required MethodDeclarationSyntax? MethodSyntax { get; init; }
1919
public bool IsGenericType { get; init; }
2020
public bool IsGenericMethod { get; init; }
@@ -23,7 +23,7 @@ public class TestMethodMetadata : IEquatable<TestMethodMetadata>
2323
/// All attributes on the method, stored for later use during data combination generation
2424
/// </summary>
2525
public ImmutableArray<AttributeData> MethodAttributes { get; init; } = ImmutableArray<AttributeData>.Empty;
26-
26+
2727
/// <summary>
2828
/// The inheritance depth of this test method.
2929
/// 0 = method is declared directly in the test class

TUnit.Core/TUnit.Core.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<InternalsVisibleTo Include="TUnit.Engine" />
77
<InternalsVisibleTo Include="TUnit.UnitTests" />
88
<InternalsVisibleTo Include="TUnit.AspNetCore" />
9+
<InternalsVisibleTo Include="TUnit.FsCheck" />
910
</ItemGroup>
1011

1112
<ItemGroup>

TUnit.Engine/Discovery/ReflectionTestDataCollector.cs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ private static async Task<List<TestMetadata>> DiscoverTestsInAssembly(Assembly a
382382
var testMethodsList = new List<MethodInfo>();
383383
foreach (var method in allMethods)
384384
{
385-
if (method.IsDefined(typeof(TestAttribute), inherit: false) && !method.IsAbstract)
385+
if (IsTestMethod(method) && !method.IsAbstract)
386386
{
387387
testMethodsList.Add(method);
388388
}
@@ -395,7 +395,7 @@ private static async Task<List<TestMetadata>> DiscoverTestsInAssembly(Assembly a
395395
var testMethodsList = new List<MethodInfo>(declaredMethods.Length);
396396
foreach (var method in declaredMethods)
397397
{
398-
if (method.IsDefined(typeof(TestAttribute), inherit: false) && !method.IsAbstract)
398+
if (IsTestMethod(method) && !method.IsAbstract)
399399
{
400400
testMethodsList.Add(method);
401401
}
@@ -513,14 +513,14 @@ private static async IAsyncEnumerable<TestMetadata> DiscoverTestsInAssemblyStrea
513513
{
514514
// Get all test methods including inherited ones
515515
testMethods = GetAllTestMethods(type)
516-
.Where(static m => m.IsDefined(typeof(TestAttribute), inherit: false) && !m.IsAbstract);
516+
.Where(static m => IsTestMethod(m) && !m.IsAbstract);
517517
}
518518
else
519519
{
520520
// Only get declared test methods
521521
testMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static |
522522
BindingFlags.DeclaredOnly)
523-
.Where(static m => m.IsDefined(typeof(TestAttribute), inherit: false) && !m.IsAbstract);
523+
.Where(static m => IsTestMethod(m) && !m.IsAbstract);
524524
}
525525
}
526526
catch (Exception)
@@ -583,7 +583,7 @@ private static async Task<List<TestMetadata>> DiscoverGenericTests(Type genericT
583583
var testMethodsList = new List<MethodInfo>(declaredMethods.Length);
584584
foreach (var method in declaredMethods)
585585
{
586-
if (method.IsDefined(typeof(TestAttribute), inherit: false) && !method.IsAbstract)
586+
if (IsTestMethod(method) && !method.IsAbstract)
587587
{
588588
testMethodsList.Add(method);
589589
}
@@ -672,7 +672,7 @@ private static async IAsyncEnumerable<TestMetadata> DiscoverGenericTestsStreamin
672672
var testMethodsList = new List<MethodInfo>(declaredMethods.Length);
673673
foreach (var method in declaredMethods)
674674
{
675-
if (method.IsDefined(typeof(TestAttribute), inherit: false) && !method.IsAbstract)
675+
if (IsTestMethod(method) && !method.IsAbstract)
676676
{
677677
testMethodsList.Add(method);
678678
}
@@ -1011,7 +1011,7 @@ private static bool HasTestMethods([DynamicallyAccessedMembers(DynamicallyAccess
10111011
var methods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly);
10121012
foreach (var method in methods)
10131013
{
1014-
if (method.IsDefined(typeof(TestAttribute), inherit: false))
1014+
if (IsTestMethod(method))
10151015
{
10161016
return true;
10171017
}
@@ -1025,14 +1025,20 @@ private static bool HasTestMethods([DynamicallyAccessedMembers(DynamicallyAccess
10251025
}
10261026
}
10271027

1028+
private static bool IsTestMethod(MethodInfo method)
1029+
{
1030+
// Check if method has any attribute that inherits from BaseTestAttribute
1031+
return method.GetCustomAttributes(typeof(BaseTestAttribute), inherit: false).Length > 0;
1032+
}
1033+
10281034
private static string? ExtractFilePath(MethodInfo method)
10291035
{
1030-
return method.GetCustomAttribute<TestAttribute>()?.File;
1036+
return method.GetCustomAttribute<BaseTestAttribute>()?.File;
10311037
}
10321038

10331039
private static int? ExtractLineNumber(MethodInfo method)
10341040
{
1035-
return method.GetCustomAttribute<TestAttribute>()?.Line;
1041+
return method.GetCustomAttribute<BaseTestAttribute>()?.Line;
10361042
}
10371043

10381044
private static TestMetadata CreateFailedTestMetadataForAssembly(Assembly assembly, Exception ex)

0 commit comments

Comments
 (0)