From 09e997d12afe542d2581dbc6c54be04f2217bd64 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 8 Feb 2025 14:57:46 +0000 Subject: [PATCH] `[MatrixExclusion(...)]` attribute for excluding specific combinations of test data (#1793) --- .../MatrixTests.Test.verified.txt | 162 ++++++++++++++++++ .../MatrixTests.cs | 2 +- .../TestData/MatrixDataSourceAttribute.cs | 19 +- .../TestData/MatrixExclusionAttribute.cs | 7 + TUnit.TestProject/MatrixTests.cs | 12 ++ docs/docs/tutorial-basics/matrix-tests.md | 51 ++++++ 6 files changed, 251 insertions(+), 2 deletions(-) create mode 100644 TUnit.Core/Attributes/TestData/MatrixExclusionAttribute.cs diff --git a/TUnit.Core.SourceGenerator.Tests/MatrixTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/MatrixTests.Test.verified.txt index 3b518f8820..70dfd7ba62 100644 --- a/TUnit.Core.SourceGenerator.Tests/MatrixTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/MatrixTests.Test.verified.txt @@ -1730,4 +1730,166 @@ file partial class MatrixTests : global::TUnit.Core.Interfaces.SourceGenerator.I } } + +// +#pragma warning disable +using global::System.Linq; +using global::System.Reflection; +using global::TUnit.Core; +using global::TUnit.Core.Extensions; + +namespace TUnit.SourceGenerated; + +[global::System.Diagnostics.StackTraceHidden] +[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +file partial class MatrixTests : global::TUnit.Core.Interfaces.SourceGenerator.ITestSource +{ + [global::System.Runtime.CompilerServices.ModuleInitializer] + public static void Initialise() + { + global::TUnit.Core.SourceRegistrar.Register(new MatrixTests()); + } + public global::System.Collections.Generic.IReadOnlyList CollectTests(string sessionId) + { + return Tests0(sessionId); + } + private global::System.Collections.Generic.List Tests0(string sessionId) + { + global::System.Collections.Generic.List nodes = []; + var classDataIndex = 0; + var testMethodDataIndex = 0; + try + { + var testInformation = new global::TUnit.Core.SourceGeneratedMethodInformation + { + Type = typeof(global::TUnit.TestProject.MatrixTests), + Name = "Exclusion", + GenericTypeCount = 0, + ReturnType = typeof(global::System.Threading.Tasks.Task), + Attributes = + [ + new global::TUnit.Core.TestAttribute(), + new global::TUnit.Core.MatrixDataSourceAttribute(), + new global::TUnit.Core.MatrixExclusionAttribute(1, 1), + new global::TUnit.Core.MatrixExclusionAttribute(2, 2), + new global::TUnit.Core.MatrixExclusionAttribute(3, 3) + ], + Parameters = + [ + new global::TUnit.Core.SourceGeneratedParameterInformation + { + Name = "item", + Attributes = + [ + new global::TUnit.Core.MatrixMethodAttribute("EnumerableMethod") + ], + }, + new global::TUnit.Core.SourceGeneratedParameterInformation + { + Name = "item2", + Attributes = + [ + new global::TUnit.Core.MatrixMethodAttribute("EnumerableMethod") + ], + }, + ], + Class = global::TUnit.Core.SourceGeneratedClassInformation.GetOrAdd("global::TUnit.TestProject.MatrixTests", () => new global::TUnit.Core.SourceGeneratedClassInformation + { + Type = typeof(global::TUnit.TestProject.MatrixTests), + Assembly = global::TUnit.Core.SourceGeneratedAssemblyInformation.GetOrAdd("MatrixTests", () => new global::TUnit.Core.SourceGeneratedAssemblyInformation + { + Name = "MatrixTests", + Attributes = [], + }), + Name = "MatrixTests", + Namespace = "TUnit.TestProject", + Attributes = [], + Parameters = [], + Properties = [], + }), +}; + + var testBuilderContext = new global::TUnit.Core.TestBuilderContext(); + var testBuilderContextAccessor = new global::TUnit.Core.TestBuilderContextAccessor(testBuilderContext); + var methodArgDataGeneratorMetadata = new DataGeneratorMetadata + { + Type = global::TUnit.Core.Enums.DataGeneratorType.TestParameters, + TestBuilderContext = testBuilderContextAccessor, + TestInformation = testInformation, + MembersToGenerate = + [ + new global::TUnit.Core.SourceGeneratedParameterInformation + { + Name = "item", + Attributes = + [ + new global::TUnit.Core.MatrixMethodAttribute("EnumerableMethod") + ], + }, + new global::TUnit.Core.SourceGeneratedParameterInformation + { + Name = "item2", + Attributes = + [ + new global::TUnit.Core.MatrixMethodAttribute("EnumerableMethod") + ], + }, + ], + TestSessionId = sessionId, + }; + var methodDataAttribute = new global::TUnit.Core.MatrixDataSourceAttribute(); + + var methodArgGeneratedDataArray = methodDataAttribute.GenerateDataSources(methodArgDataGeneratorMetadata); + + foreach (var methodArgGeneratedDataAccessor in methodArgGeneratedDataArray) + { + testMethodDataIndex++; + + var methodArgGeneratedData = methodArgGeneratedDataAccessor(); + int methodArg = global::TUnit.Core.Helpers.CastHelper.Cast(methodArgGeneratedData[0]); + int methodArg1 = global::TUnit.Core.Helpers.CastHelper.Cast(methodArgGeneratedData[1]); + var resettableClassFactoryDelegate = () => new ResettableLazy(() => + new global::TUnit.TestProject.MatrixTests() + , sessionId, testBuilderContext); + + var resettableClassFactory = resettableClassFactoryDelegate(); + + nodes.Add(new TestMetadata + { + TestId = $"global::TUnit.Core.MatrixDataSourceAttribute:{testMethodDataIndex}:TL-GAC0:TUnit.TestProject.MatrixTests.Exclusion(int,int):0", + TestClassArguments = [], + TestMethodArguments = [methodArg, methodArg1], + TestClassProperties = [], + CurrentRepeatAttempt = 0, + RepeatLimit = 0, + ResettableClassFactory = resettableClassFactory, + TestMethodFactory = (classInstance, cancellationToken) => AsyncConvert.Convert(() => classInstance.Exclusion(methodArg, methodArg1)), + TestFilePath = @"", + TestLineNumber = 123, + TestMethod = testInformation, + TestBuilderContext = testBuilderContext, + }); + resettableClassFactory = resettableClassFactoryDelegate(); + testBuilderContext = new(); + testBuilderContextAccessor.Current = testBuilderContext; + } + } + catch (global::System.Exception exception) + { + nodes.Add(new global::TUnit.Core.FailedInitializationTest + { + TestId = $"global::TUnit.Core.MatrixDataSourceAttribute:{testMethodDataIndex}:TL-GAC0:TUnit.TestProject.MatrixTests.Exclusion(int,int):0", + TestClass = typeof(global::TUnit.TestProject.MatrixTests), + ReturnType = typeof(global::System.Threading.Tasks.Task), + ParameterTypeFullNames = [typeof(int), typeof(int)], + TestName = "Exclusion", + TestFilePath = @"", + TestLineNumber = 123, + Exception = exception, + }); + } + return nodes; + } + } + ] \ No newline at end of file diff --git a/TUnit.Core.SourceGenerator.Tests/MatrixTests.cs b/TUnit.Core.SourceGenerator.Tests/MatrixTests.cs index 4e9bff3c5e..b359bd07ef 100644 --- a/TUnit.Core.SourceGenerator.Tests/MatrixTests.cs +++ b/TUnit.Core.SourceGenerator.Tests/MatrixTests.cs @@ -20,6 +20,6 @@ public Task Test() => RunTest(Path.Combine(Git.RootDirectory.FullName, }, async generatedFiles => { - await Assert.That(generatedFiles.Length).IsEqualTo(11); + await Assert.That(generatedFiles.Length).IsEqualTo(12); }); } \ No newline at end of file diff --git a/TUnit.Core/Attributes/TestData/MatrixDataSourceAttribute.cs b/TUnit.Core/Attributes/TestData/MatrixDataSourceAttribute.cs index 3c4bcd118b..6869b5a796 100644 --- a/TUnit.Core/Attributes/TestData/MatrixDataSourceAttribute.cs +++ b/TUnit.Core/Attributes/TestData/MatrixDataSourceAttribute.cs @@ -1,4 +1,6 @@ -namespace TUnit.Core; +using TUnit.Core.Enums; + +namespace TUnit.Core; [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public sealed class MatrixDataSourceAttribute : NonTypedDataSourceGeneratorAttribute @@ -16,12 +18,27 @@ public sealed class MatrixDataSourceAttribute : NonTypedDataSourceGeneratorAttri throw new Exception("[MatrixDataSource] only supports parameterised tests"); } + var exclusions = GetExclusions(dataGeneratorMetadata.Type == DataGeneratorType.TestParameters + ? dataGeneratorMetadata.TestInformation.Attributes : dataGeneratorMetadata.TestInformation.Class.Attributes); + foreach (var row in GetMatrixValues(parameterInformation.Select(GetAllArguments))) { + if (exclusions.Any(e => e.SequenceEqual(row))) + { + continue; + } + yield return () => row.ToArray(); } } + private object?[][] GetExclusions(Attribute[] attributes) + { + return attributes.OfType() + .Select(x => x.Objects) + .ToArray(); + } + private IReadOnlyList GetAllArguments(SourceGeneratedParameterInformation sourceGeneratedParameterInformation) { var matrixAttribute = sourceGeneratedParameterInformation.Attributes.OfType().FirstOrDefault(); diff --git a/TUnit.Core/Attributes/TestData/MatrixExclusionAttribute.cs b/TUnit.Core/Attributes/TestData/MatrixExclusionAttribute.cs new file mode 100644 index 0000000000..7aa282a97c --- /dev/null +++ b/TUnit.Core/Attributes/TestData/MatrixExclusionAttribute.cs @@ -0,0 +1,7 @@ +namespace TUnit.Core; + +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)] +public class MatrixExclusionAttribute(params object?[]? objects) : TUnitAttribute +{ + public object?[] Objects { get; } = objects ?? [ null ]; +} \ No newline at end of file diff --git a/TUnit.TestProject/MatrixTests.cs b/TUnit.TestProject/MatrixTests.cs index 0d5bce0a72..a9e0834c13 100644 --- a/TUnit.TestProject/MatrixTests.cs +++ b/TUnit.TestProject/MatrixTests.cs @@ -119,6 +119,18 @@ public async Task Method4( { await Task.CompletedTask; } + + [Test] + [MatrixDataSource] + [MatrixExclusion(1, 1)] + [MatrixExclusion(2, 2)] + [MatrixExclusion(3, 3)] + public async Task Exclusion( + [MatrixMethod(nameof(EnumerableMethod))] int item, + [MatrixMethod(nameof(EnumerableMethod))] int item2) + { + await Task.CompletedTask; + } public enum CountToTenEnum { diff --git a/docs/docs/tutorial-basics/matrix-tests.md b/docs/docs/tutorial-basics/matrix-tests.md index 2eacc8005a..68c8c617f8 100644 --- a/docs/docs/tutorial-basics/matrix-tests.md +++ b/docs/docs/tutorial-basics/matrix-tests.md @@ -124,3 +124,54 @@ public class MyTestClass } } ``` + +## Matrix Exclusions + +You can also add a `[MatrixExclusion(...)]` attribute to your tests. +This works similar to the `[Arguments(...)]` attribute, and if objects match a generated matrix test case, it'll be ignored. + +This helps you exclude specific one-off scenarios without having to complicate your tests with `if` conditions. + +```csharp +using TUnit.Assertions; +using TUnit.Assertions.Extensions; +using TUnit.Assertions.Extensions.Is; +using TUnit.Core; + +namespace MyTestProject; + +public class MyTestClass +{ + [Test] + [MatrixDataSource] + [MatrixExclusion(1, 1)] + [MatrixExclusion(2, 2)] + [MatrixExclusion(3, 3)] + public async Task MyTest( + [MatrixRange(1, 3)] int value1, + [MatrixRange(1, 3)] int value2 + ) + { + ... + } +} +``` + +Whereas the above Matrix would usually generate: +- 1, 1 +- 1, 2 +- 1, 3 +- 2, 1 +- 2, 2 +- 2, 3 +- 3, 1 +- 3, 2 +- 3, 3 + +Because of the exclusion attributes, it'll only generate: +- 1, 2 +- 1, 3 +- 2, 1 +- 2, 3 +- 3, 1 +- 3, 2 \ No newline at end of file