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
+