diff --git a/Tests/TryAtSoftware.CleanTests.UnitTests/ConfigureCleanTestsFrameworkAttributeTests.cs b/Tests/TryAtSoftware.CleanTests.UnitTests/Attributes/ConfigureCleanTestsFrameworkAttributeTests.cs similarity index 100% rename from Tests/TryAtSoftware.CleanTests.UnitTests/ConfigureCleanTestsFrameworkAttributeTests.cs rename to Tests/TryAtSoftware.CleanTests.UnitTests/Attributes/ConfigureCleanTestsFrameworkAttributeTests.cs diff --git a/Tests/TryAtSoftware.CleanTests.UnitTests/CleanUtilitiesDistributionTests.cs b/Tests/TryAtSoftware.CleanTests.UnitTests/CleanUtilitiesDistributionTests.cs index 296c3f3..f40e600 100644 --- a/Tests/TryAtSoftware.CleanTests.UnitTests/CleanUtilitiesDistributionTests.cs +++ b/Tests/TryAtSoftware.CleanTests.UnitTests/CleanUtilitiesDistributionTests.cs @@ -12,14 +12,14 @@ public class CleanUtilitiesDistributionTests { - private const string Category = "_"; + private const string DefaultCategory = "_"; [Fact(Timeout = UnitTestConstants.Timeout)] public async Task TestCasesShouldNotBeDiscoveredWhenDemandsFilterOutAllRequiredUtilities() { var reflectionMocks = ReflectionMocks.MockReflectionSuite(Assembly.GetExecutingAssembly(), typeof(TestDefiningUnfulfillableDemands)); var testComponentMocks = reflectionMocks.MockTestComponentsSuite(); - var cleanUtilityDescriptor = new CleanUtilityDescriptor(Category, typeof(StandardUtility), "Matching utility", isGlobal: false, characteristics: ["available"]); + var cleanUtilityDescriptor = new CleanUtilityDescriptor(DefaultCategory, typeof(StandardUtility), "Matching utility", isGlobal: false, characteristics: ["available"]); var assemblyData = new CleanTestAssemblyData([cleanUtilityDescriptor]); var testCases = await reflectionMocks.AssemblyInfo.DiscoverTestCasesAsync(assemblyData, testComponentMocks); @@ -28,7 +28,7 @@ public async Task TestCasesShouldNotBeDiscoveredWhenDemandsFilterOutAllRequiredU } [Fact(Timeout = UnitTestConstants.Timeout)] - public async Task TestCasesShouldBeDiscoveredOnceWhenNoUtilitiesAreRequired() + public async Task TestCasesShouldBeDiscoveredEvenIfTheyDoNotRequireAnyUtilities() { var reflectionMocks = ReflectionMocks.MockReflectionSuite(Assembly.GetExecutingAssembly(), typeof(TestConsumingNoUtilities)); var testComponentMocks = reflectionMocks.MockTestComponentsSuite(); @@ -46,7 +46,7 @@ public async Task UtilitiesDistributionShouldHaveProperErrorHandling(bool isGlob var reflectionMocks = ReflectionMocks.MockReflectionSuite(Assembly.GetExecutingAssembly(), testClass); var testComponentMocks = reflectionMocks.MockTestComponentsSuite(); - var cleanUtilityDescriptor = new CleanUtilityDescriptor(Category, typeof(InconclusiveUtility), "Inconclusive utility", isGlobal); + var cleanUtilityDescriptor = new CleanUtilityDescriptor(DefaultCategory, typeof(InconclusiveUtility), "Inconclusive utility", isGlobal); var assemblyData = new CleanTestAssemblyData(new[] { cleanUtilityDescriptor }); var testCases = await reflectionMocks.AssemblyInfo.DiscoverTestCasesAsync(assemblyData, testComponentMocks); @@ -59,6 +59,33 @@ public async Task UtilitiesDistributionShouldHaveProperErrorHandling(bool isGlob Assert.Equal(1, executionResult.Total); } + [Fact(Timeout = UnitTestConstants.Timeout)] + public async Task FrameworkDiscoveryShouldProduceATestCaseForEachMatchingUtilityAttribute() + { + var typesMap = new Dictionary(); + var assemblyInfo = Assembly.GetExecutingAssembly().MockReflectionAssemblyInfo(typesMap); + + var cleanTestTypeInfo = typeof(TestConsumingMultiUtilities).MockReflectionTypeInfo(assemblyInfo); + var utilityTypeInfo = typeof(MultiUseCleanUtility).MockReflectionTypeInfo(assemblyInfo); + + typesMap[typeof(TestConsumingMultiUtilities).AssemblyQualifiedName!] = cleanTestTypeInfo; + typesMap[typeof(MultiUseCleanUtility).AssemblyQualifiedName!] = utilityTypeInfo; + + var reflectionMocks = new ReflectionMocksSuite(assemblyInfo, cleanTestTypeInfo); + var testComponentMocks = reflectionMocks.MockTestComponentsSuite(); + + var messageSink = testComponentMocks.DiagnosticMessageSink; + var framework = new CleanTestFramework(messageSink); + var discoverer = framework.GetDiscoverer(assemblyInfo); + + var testCases = await discoverer.DiscoverTestCasesAsync(testComponentMocks); + var testCase = Assert.IsType(Assert.Single(testCases)); + + Assert.Equal(2, testCase.CleanTestCaseData.CleanUtilities.Count); + Assert.Single(testCase.CleanTestCaseData.CleanUtilities, x => x.Id == "c:Multi-Category #1|n:Utility A"); + Assert.Single(testCase.CleanTestCaseData.CleanUtilities, x => x.Id == "c:Multi-Category #2|n:Utility A"); + } + private class InconclusiveUtility(string unresolvableParameter) { public string UnresolvableParameter { get; } = unresolvableParameter; @@ -68,9 +95,15 @@ private class StandardUtility { } + [CleanUtility("Multi-Category #1", "Utility A")] + [CleanUtility("Multi-Category #2", "Utility A")] + private class MultiUseCleanUtility + { + } + private class TestConsumingGlobalUtilities(ITestOutputHelper testOutputHelper) : CleanTest(testOutputHelper) { - [CleanFact, WithRequirements(Category)] + [CleanFact, WithRequirements(DefaultCategory)] public void Test() { _ = this.GetGlobalService(); @@ -80,7 +113,7 @@ public void Test() private class TestConsumingLocalUtilities(ITestOutputHelper testOutputHelper) : CleanTest(testOutputHelper) { - [CleanFact, WithRequirements(Category)] + [CleanFact, WithRequirements(DefaultCategory)] public void Test() { _ = this.GetService(); @@ -90,7 +123,7 @@ public void Test() private class TestDefiningUnfulfillableDemands(ITestOutputHelper testOutputHelper) : CleanTest(testOutputHelper) { - [CleanFact, WithRequirements(Category), TestDemands(Category, "missing")] + [CleanFact, WithRequirements(DefaultCategory), TestDemands(DefaultCategory, "missing")] public void Test() => Assert.Fail("It is not expected that this test will be executed. It is just a mean to validate correct test case discovery."); } @@ -99,4 +132,11 @@ private class TestConsumingNoUtilities(ITestOutputHelper testOutputHelper) : Cle [CleanFact] public void Test() => Assert.Fail("It is not expected that this test will be executed. It is just a mean to validate correct test case discovery."); } + + private class TestConsumingMultiUtilities(ITestOutputHelper testOutputHelper) : CleanTest(testOutputHelper) + { + [CleanFact] + [WithRequirements("Multi-Category #1", "Multi-Category #2")] + public void Test() => Assert.Fail("It is not expected that this test will be executed. It is just a mean to validate correct test case discovery."); + } } \ No newline at end of file diff --git a/Tests/TryAtSoftware.CleanTests.UnitTests/Extensions/DiscoveryExtensions.cs b/Tests/TryAtSoftware.CleanTests.UnitTests/Extensions/DiscoveryExtensions.cs index 0b02ee4..098f6c9 100644 --- a/Tests/TryAtSoftware.CleanTests.UnitTests/Extensions/DiscoveryExtensions.cs +++ b/Tests/TryAtSoftware.CleanTests.UnitTests/Extensions/DiscoveryExtensions.cs @@ -11,17 +11,26 @@ internal static class DiscoveryExtensions { private const int DelayBetweenDiscoveryRetries = 50; private const int MaxDiscoveryRetries = 20; - - internal static async Task> DiscoverTestCasesAsync(this IAssemblyInfo assembly, CleanTestAssemblyData assemblyData, TestComponentMocksSuite testComponentMocks) + + internal static Task> DiscoverTestCasesAsync(this IAssemblyInfo assembly, CleanTestAssemblyData assemblyData, TestComponentMocksSuite testComponentMocks) { Assert.NotNull(assembly); Assert.NotNull(assemblyData); Assert.NotNull(testComponentMocks); - + + var testFrameworkDiscoverer = new CleanTestFrameworkDiscoverer(assembly, testComponentMocks.SourceInformationProvider, testComponentMocks.DiagnosticMessageSink, assemblyData); + return testFrameworkDiscoverer.DiscoverTestCasesAsync(testComponentMocks); + } + + internal static async Task> DiscoverTestCasesAsync(this ITestFrameworkDiscoverer testFrameworkDiscoverer, TestComponentMocksSuite testComponentMocks) + { + Assert.NotNull(testFrameworkDiscoverer); + Assert.NotNull(testComponentMocks); + var discoveryIsOver = false; var discoveredTestCases = new List(); - - var discoveryMessageSink = Substitute.For(); + + var discoveryMessageSink = TestComponentMocks.MockMessageSink(); discoveryMessageSink.OnMessage(Arg.Any()).Returns(true); discoveryMessageSink.OnMessage(Arg.Any()).Returns(true) .AndDoes(x => @@ -32,7 +41,6 @@ internal static async Task> DiscoverTestCasesAsync(t discoveryMessageSink.OnMessage(Arg.Any()).Returns(true) .AndDoes(_ => discoveryIsOver = true); - var testFrameworkDiscoverer = new CleanTestFrameworkDiscoverer(assembly, testComponentMocks.SourceInformationProvider, testComponentMocks.DiagnosticMessageSink, assemblyData); testFrameworkDiscoverer.Find(includeSourceInformation: true, discoveryMessageSink, testComponentMocks.TestFrameworkDiscoveryOptions); var retryId = 0; @@ -41,7 +49,7 @@ internal static async Task> DiscoverTestCasesAsync(t await Task.Delay(DelayBetweenDiscoveryRetries); retryId++; } - + Assert.True(discoveryIsOver, $"The discovery process did not finish in time after {MaxDiscoveryRetries} retries."); return discoveredTestCases; } diff --git a/Tests/TryAtSoftware.CleanTests.UnitTests/GenericAutomationTests.cs b/Tests/TryAtSoftware.CleanTests.UnitTests/GenericAutomationTests.cs index 3805647..f1d043a 100644 --- a/Tests/TryAtSoftware.CleanTests.UnitTests/GenericAutomationTests.cs +++ b/Tests/TryAtSoftware.CleanTests.UnitTests/GenericAutomationTests.cs @@ -22,7 +22,7 @@ public async Task TestCasesShouldFailForIncompleteGenericTestClasses() var testCases = await reflectionMocks.AssemblyInfo.DiscoverTestCasesAsync(assemblyData, testComponentMocks); Assert.Empty(testCases); } - + [Fact(Timeout = UnitTestConstants.Timeout)] public async Task TestCasesShouldPassForCompleteGenericTestClasses() { @@ -40,14 +40,9 @@ public async Task TestCasesShouldPassForCompleteGenericTestClasses() Assert.Equal(4, executionResult.Total); } - private class IncompleteGenericTestClass : CleanTest + private class IncompleteGenericTestClass(ITestOutputHelper testOutputHelper) : CleanTest(testOutputHelper) where T : new() { - public IncompleteGenericTestClass(ITestOutputHelper testOutputHelper) - : base(testOutputHelper) - { - } - [CleanFact] public static void Test() => Assert.NotNull(new T()); } @@ -56,21 +51,16 @@ public IncompleteGenericTestClass(ITestOutputHelper testOutputHelper) private class TestGenericParameterAttribute : Attribute { } - -// To be removed when upgrading `TryAtSoftware.Extensions.Reflection`. + + // To be removed when upgrading `TryAtSoftware.Extensions.Reflection`. #nullable disable [TestSuiteGenericTypeMapping(typeof(TestGenericParameterAttribute), typeof(object))] - private class CompleteGenericTestClass<[TestGenericParameter] T> : CleanTest + private class CompleteGenericTestClass<[TestGenericParameter] T>(ITestOutputHelper testOutputHelper) : CleanTest(testOutputHelper) where T : new() { - public CompleteGenericTestClass(ITestOutputHelper testOutputHelper) - : base(testOutputHelper) - { - } - [Fact] public static void Fact() => Assert.NotNull(new T()); - + [Theory] [MemberData(nameof(GetTheoryData))] public static void Theory(int iterations) diff --git a/TryAtSoftware.CleanTests.Core/Attributes/CleanUtilityAttribute.cs b/TryAtSoftware.CleanTests.Core/Attributes/CleanUtilityAttribute.cs index 4fdd2d0..1d7f255 100644 --- a/TryAtSoftware.CleanTests.Core/Attributes/CleanUtilityAttribute.cs +++ b/TryAtSoftware.CleanTests.Core/Attributes/CleanUtilityAttribute.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using TryAtSoftware.Extensions.Collections; -[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public class CleanUtilityAttribute : Attribute { public string Name { get; } diff --git a/TryAtSoftware.CleanTests.Core/XUnit/CleanTestFramework.cs b/TryAtSoftware.CleanTests.Core/XUnit/CleanTestFramework.cs index 728075e..114e0f6 100644 --- a/TryAtSoftware.CleanTests.Core/XUnit/CleanTestFramework.cs +++ b/TryAtSoftware.CleanTests.Core/XUnit/CleanTestFramework.cs @@ -15,7 +15,7 @@ public class CleanTestFramework(IMessageSink messageSink) : XunitTestFramework(messageSink) { - private readonly Dictionary _utilityDescriptorsByAssembly = new (); + private readonly Dictionary _utilityDescriptorsByAssembly = new(); protected override ITestFrameworkExecutor CreateExecutor(AssemblyName assemblyName) { @@ -35,9 +35,9 @@ protected override ITestFrameworkDiscoverer CreateDiscoverer(IAssemblyInfo assem private CleanTestAssemblyData ExtractAssemblyData(IAssemblyInfo assemblyInfo) { if (this._utilityDescriptorsByAssembly.TryGetValue(assemblyInfo.Name, out var memoizedResult)) return memoizedResult; - + var utilitiesCollection = new List(); - + RegisterUtilitiesFromAssembly(assemblyInfo, utilitiesCollection); var sharedUtilitiesAttributes = assemblyInfo.GetCustomAttributes(typeof(SharesUtilitiesWithAttribute)); foreach (var sharedUtilitiesAttribute in sharedUtilitiesAttributes) @@ -48,7 +48,7 @@ private CleanTestAssemblyData ExtractAssemblyData(IAssemblyInfo assemblyInfo) } var assemblyData = new CleanTestAssemblyData(utilitiesCollection); - + var configurationAttribute = assemblyInfo.GetCustomAttributes(typeof(ConfigureCleanTestsFrameworkAttribute)).FirstOrDefault(); if (configurationAttribute is not null) { @@ -56,10 +56,10 @@ private CleanTestAssemblyData ExtractAssemblyData(IAssemblyInfo assemblyInfo) assemblyData.UtilitiesPresentations = configurationAttribute.GetNamedArgument(nameof(ConfigureCleanTestsFrameworkAttribute.UtilitiesPresentations)); assemblyData.GenericTypeMappingPresentations = configurationAttribute.GetNamedArgument(nameof(ConfigureCleanTestsFrameworkAttribute.GenericTypeMappingPresentations)); } - + this._utilityDescriptorsByAssembly[assemblyInfo.Name] = assemblyData; return assemblyData; - } + } private static void RegisterUtilitiesFromAssembly(IAssemblyInfo assemblyInfo, List utilitiesCollection) { @@ -67,8 +67,8 @@ private static void RegisterUtilitiesFromAssembly(IAssemblyInfo assemblyInfo, Li { if (type.IsAbstract) continue; - var initializationUtilityAttributes = type.GetCustomAttributes(typeof(CleanUtilityAttribute)).ToArray(); - if (initializationUtilityAttributes.Length == 0) continue; + var cleanUtilityAttributes = type.GetCustomAttributes(typeof(CleanUtilityAttribute)).ToArray(); + if (cleanUtilityAttributes.Length == 0) continue; var decoratedType = new DecoratedType(type); var externalDemands = decoratedType.ExtractDemands(); @@ -76,19 +76,19 @@ private static void RegisterUtilitiesFromAssembly(IAssemblyInfo assemblyInfo, Li var outerDemands = decoratedType.ExtractDemands(); var requirements = ExtractRequirements(type); - foreach (var utilityAttribute in initializationUtilityAttributes.OrEmptyIfNull().IgnoreNullValues()) + foreach (var utilityAttribute in cleanUtilityAttributes.OrEmptyIfNull().IgnoreNullValues()) { var categoryArgument = utilityAttribute.GetNamedArgument(nameof(CleanUtilityAttribute.Category)); var nameArgument = utilityAttribute.GetNamedArgument(nameof(CleanUtilityAttribute.Name)); var isGlobalArgument = utilityAttribute.GetNamedArgument(nameof(CleanUtilityAttribute.IsGlobal)); var characteristicsArgument = utilityAttribute.GetNamedArgument>(nameof(CleanUtilityAttribute.Characteristics)); - var initializationUtility = new CleanUtilityDescriptor(categoryArgument, type.ToRuntimeType(), nameArgument, isGlobalArgument, characteristicsArgument, requirements); - externalDemands.CopyTo(initializationUtility.ExternalDemands); - internalDemands.CopyTo(initializationUtility.InternalDemands); - outerDemands.CopyTo(initializationUtility.OuterDemands); + var cleanUtility = new CleanUtilityDescriptor(categoryArgument, type.ToRuntimeType(), nameArgument, isGlobalArgument, characteristicsArgument, requirements); + externalDemands.CopyTo(cleanUtility.ExternalDemands); + internalDemands.CopyTo(cleanUtility.InternalDemands); + outerDemands.CopyTo(cleanUtility.OuterDemands); - utilitiesCollection.Add(initializationUtility); + utilitiesCollection.Add(cleanUtility); } } }