diff --git a/Arcade.slnx b/Arcade.slnx index b49c4bca016..3cced7ac84d 100644 --- a/Arcade.slnx +++ b/Arcade.slnx @@ -34,6 +34,7 @@ + diff --git a/src/Microsoft.DotNet.XUnitV3Extensions/src/ConditionalAssemblyAttribute.cs b/src/Microsoft.DotNet.XUnitV3Extensions/src/ConditionalAssemblyAttribute.cs new file mode 100644 index 00000000000..16cbd885aad --- /dev/null +++ b/src/Microsoft.DotNet.XUnitV3Extensions/src/ConditionalAssemblyAttribute.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.DotNet.XUnitExtensions; +using Xunit.Sdk; + +namespace Xunit +{ + /// + /// An assembly-level attribute that conditionally marks all tests in the assembly to be skipped + /// based on the evaluation of one or more static boolean members. When any of the referenced + /// condition members evaluates to false, the attribute contributes a category=failing + /// trait so that the test runner can exclude the affected tests. + /// + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class ConditionalAssemblyAttribute : Attribute, ITraitAttribute + { + [DynamicallyAccessedMembers(StaticReflectionConstants.ConditionalMemberKinds)] + public Type CalleeType { get; private set; } + public string[] ConditionMemberNames { get; private set; } + + public ConditionalAssemblyAttribute( + [DynamicallyAccessedMembers(StaticReflectionConstants.ConditionalMemberKinds)] + Type calleeType, + params string[] conditionMemberNames) + { + CalleeType = calleeType; + ConditionMemberNames = conditionMemberNames; + } + + public IReadOnlyCollection> GetTraits() + { + // If evaluated to false, skip all tests in the assembly. + if (!EvaluateParameterHelper()) + { + return [new KeyValuePair(XunitConstants.Category, XunitConstants.Failing)]; + } + + return []; + } + + internal bool EvaluateParameterHelper() + { + Type calleeType = null; + string[] conditionMemberNames = null; + + if (ConditionalTestDiscoverer.CheckInputToSkipExecution([CalleeType, ConditionMemberNames], ref calleeType, ref conditionMemberNames)) + { + return true; + } + + return DiscovererHelpers.Evaluate(calleeType, conditionMemberNames); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitV3Extensions/tests/AlwaysFalseConditionalAssemblyTests.cs b/src/Microsoft.DotNet.XUnitV3Extensions/tests/AlwaysFalseConditionalAssemblyTests.cs new file mode 100644 index 00000000000..50aadc56cf0 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitV3Extensions/tests/AlwaysFalseConditionalAssemblyTests.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +// The assembly-level ConditionalAssembly attribute below references a condition member +// that always returns false. As a result, every test in this assembly is tagged with the +// "category=failing" trait, and the test runner is configured (via the project's +// TestRunnerAdditionalArguments) to skip tests with that trait. If the attribute were +// ever broken and stopped contributing the trait, the deliberately failing test below +// would run and fail the build, catching the regression. +[assembly: ConditionalAssembly(typeof(Microsoft.DotNet.XUnitV3Extensions.AlwaysFalseConditionalAssemblyTests.Conditions), + nameof(Microsoft.DotNet.XUnitV3Extensions.AlwaysFalseConditionalAssemblyTests.Conditions.AlwaysFalse))] + +namespace Microsoft.DotNet.XUnitV3Extensions.AlwaysFalseConditionalAssemblyTests +{ + public static class Conditions + { + public static bool AlwaysFalse => false; + } + + public class FailingTests + { + [Fact] + public void AlwaysFails() + { + Assert.Fail("This test is expected to be skipped via [assembly: ConditionalAssembly]."); + } + } +} diff --git a/src/Microsoft.DotNet.XUnitV3Extensions/tests/ConditionalAttributeTests.cs b/src/Microsoft.DotNet.XUnitV3Extensions/tests/ConditionalAttributeTests.cs index 61f4c072bd2..f98b5558667 100644 --- a/src/Microsoft.DotNet.XUnitV3Extensions/tests/ConditionalAttributeTests.cs +++ b/src/Microsoft.DotNet.XUnitV3Extensions/tests/ConditionalAttributeTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Collections.Generic; using System.Reflection; using Xunit; @@ -86,6 +88,61 @@ public void ValidateConditionalTheoryTrueReceivedArgs() Assert.NotNull(GetConditionalTheoryAttribute(nameof(ConditionalTheoryTrue))); } + [Fact] + public void ConditionalAssemblyAttribute_MultipleConditions_AllTrue_ReturnsNoTraits() + { + ConditionalAssemblyAttribute attribute = new ConditionalAssemblyAttribute( + typeof(ConditionalAttributeTests), + nameof(AlwaysTrue), + nameof(AlwaysTrue)); + + Assert.Empty(attribute.GetTraits()); + } + + [Fact] + public void ConditionalAssemblyAttribute_MultipleConditions_OneFalse_ReturnsFailingCategoryTrait() + { + ConditionalAssemblyAttribute attribute = new ConditionalAssemblyAttribute( + typeof(ConditionalAttributeTests), + nameof(AlwaysTrue), + nameof(AlwaysFalse)); + + KeyValuePair trait = Assert.Single(attribute.GetTraits()); + Assert.Equal(XunitConstants.Category, trait.Key); + Assert.Equal("failing", trait.Value); + } + + [Fact] + public void ConditionalAssemblyAttribute_NoConditionMembers_ReturnsNoTraits() + { + // With no condition names supplied, the attribute is treated as "no conditions" and tests run normally. + ConditionalAssemblyAttribute attribute = new ConditionalAssemblyAttribute(typeof(ConditionalAttributeTests)); + + Assert.Empty(attribute.GetTraits()); + } + + [Fact] + public void ConditionalAssemblyAttribute_MissingMember_Throws() + { + ConditionalAssemblyAttribute attribute = new ConditionalAssemblyAttribute( + typeof(ConditionalAttributeTests), + "MemberThatDoesNotExist"); + + Assert.Throws(() => attribute.GetTraits()); + } + + [Fact] + public void ConditionalAssemblyAttribute_StoresConstructorArguments() + { + ConditionalAssemblyAttribute attribute = new ConditionalAssemblyAttribute( + typeof(ConditionalAttributeTests), + nameof(AlwaysTrue), + nameof(AlwaysFalse)); + + Assert.Equal(typeof(ConditionalAttributeTests), attribute.CalleeType); + Assert.Equal(new[] { nameof(AlwaysTrue), nameof(AlwaysFalse) }, attribute.ConditionMemberNames); + } + private static ConditionalFactAttribute GetConditionalFactAttribute(string methodName) { return (ConditionalFactAttribute)typeof(ConditionalAttributeTests) diff --git a/src/Microsoft.DotNet.XUnitV3Extensions/tests/Microsoft.DotNet.XUnitV3Extensions.AlwaysFalseConditionalAssembly.Tests.csproj b/src/Microsoft.DotNet.XUnitV3Extensions/tests/Microsoft.DotNet.XUnitV3Extensions.AlwaysFalseConditionalAssembly.Tests.csproj new file mode 100644 index 00000000000..d83212ba1a3 --- /dev/null +++ b/src/Microsoft.DotNet.XUnitV3Extensions/tests/Microsoft.DotNet.XUnitV3Extensions.AlwaysFalseConditionalAssembly.Tests.csproj @@ -0,0 +1,38 @@ + + + + $(BundledNETCoreAppTargetFramework) + XUnitV3 + Exe + + --filter-not-trait "category=failing" --ignore-exit-code 8 + $(ConditionalAssemblyFilterArguments) + $(ConditionalAssemblyFilterArguments) + + false + + + + + + + + + + + + diff --git a/src/Microsoft.DotNet.XUnitV3Extensions/tests/Microsoft.DotNet.XUnitV3Extensions.Tests.csproj b/src/Microsoft.DotNet.XUnitV3Extensions/tests/Microsoft.DotNet.XUnitV3Extensions.Tests.csproj index 4f3eb908f30..f6f3873f260 100644 --- a/src/Microsoft.DotNet.XUnitV3Extensions/tests/Microsoft.DotNet.XUnitV3Extensions.Tests.csproj +++ b/src/Microsoft.DotNet.XUnitV3Extensions/tests/Microsoft.DotNet.XUnitV3Extensions.Tests.csproj @@ -6,6 +6,11 @@ Exe + + + + + diff --git a/tests/UnitTests.proj b/tests/UnitTests.proj index fb5975fcb37..be8a47dc25b 100644 --- a/tests/UnitTests.proj +++ b/tests/UnitTests.proj @@ -49,10 +49,21 @@ + + + + --filter-not-trait "category=failing" --ignore-exit-code 8 +