From e31aacfaede3a6ab1c59127ef07a35885c8179ff Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 14 May 2026 22:18:44 +0000
Subject: [PATCH 1/4] Add ConditionalAssemblyAttribute and tests for xunit v3
Agent-Logs-Url: https://github.com/dotnet/arcade/sessions/f511792c-b425-4a96-ae81-93e3e1e733ed
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
---
.../ConditionalAssemblyAttribute.cs | 60 ++++++++++++
...ft.DotNet.XUnitExtensions.Shared.projitems | 1 +
.../tests/ConditionalAttributeTests.cs | 92 +++++++++++++++++++
3 files changed, 153 insertions(+)
create mode 100644 src/Microsoft.DotNet.XUnitExtensions.Shared/Attributes/ConditionalAssemblyAttribute.cs
diff --git a/src/Microsoft.DotNet.XUnitExtensions.Shared/Attributes/ConditionalAssemblyAttribute.cs b/src/Microsoft.DotNet.XUnitExtensions.Shared/Attributes/ConditionalAssemblyAttribute.cs
new file mode 100644
index 00000000000..ca13d99a883
--- /dev/null
+++ b/src/Microsoft.DotNet.XUnitExtensions.Shared/Attributes/ConditionalAssemblyAttribute.cs
@@ -0,0 +1,60 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#if USES_XUNIT_3
+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);
+ }
+ }
+}
+#endif
diff --git a/src/Microsoft.DotNet.XUnitExtensions.Shared/Microsoft.DotNet.XUnitExtensions.Shared.projitems b/src/Microsoft.DotNet.XUnitExtensions.Shared/Microsoft.DotNet.XUnitExtensions.Shared.projitems
index 9807b89e6ad..63ea580b417 100644
--- a/src/Microsoft.DotNet.XUnitExtensions.Shared/Microsoft.DotNet.XUnitExtensions.Shared.projitems
+++ b/src/Microsoft.DotNet.XUnitExtensions.Shared/Microsoft.DotNet.XUnitExtensions.Shared.projitems
@@ -10,6 +10,7 @@
+
diff --git a/src/Microsoft.DotNet.XUnitV3Extensions/tests/ConditionalAttributeTests.cs b/src/Microsoft.DotNet.XUnitV3Extensions/tests/ConditionalAttributeTests.cs
index 61f4c072bd2..4d6382a7472 100644
--- a/src/Microsoft.DotNet.XUnitV3Extensions/tests/ConditionalAttributeTests.cs
+++ b/src/Microsoft.DotNet.XUnitV3Extensions/tests/ConditionalAttributeTests.cs
@@ -1,6 +1,9 @@
// 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.Linq;
using System.Reflection;
using Xunit;
@@ -86,6 +89,95 @@ public void ValidateConditionalTheoryTrueReceivedArgs()
Assert.NotNull(GetConditionalTheoryAttribute(nameof(ConditionalTheoryTrue)));
}
+ [Fact]
+ public void ConditionalAssemblyAttribute_TrueCondition_ReturnsNoTraits()
+ {
+ ConditionalAssemblyAttribute attribute = new ConditionalAssemblyAttribute(typeof(ConditionalAttributeTests), nameof(AlwaysTrue));
+
+ IReadOnlyCollection> traits = attribute.GetTraits();
+
+ Assert.Empty(traits);
+ }
+
+ [Fact]
+ public void ConditionalAssemblyAttribute_FalseCondition_ReturnsFailingCategoryTrait()
+ {
+ ConditionalAssemblyAttribute attribute = new ConditionalAssemblyAttribute(typeof(ConditionalAttributeTests), nameof(AlwaysFalse));
+
+ IReadOnlyCollection> traits = attribute.GetTraits();
+
+ KeyValuePair trait = Assert.Single(traits);
+ Assert.Equal(XunitConstants.Category, trait.Key);
+ Assert.Equal("failing", trait.Value);
+ }
+
+ [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);
+ }
+
+ [Fact]
+ public void ConditionalAssemblyAttribute_AttributeUsage_TargetsAssemblyOnly()
+ {
+ AttributeUsageAttribute usage = typeof(ConditionalAssemblyAttribute)
+ .GetCustomAttributes(typeof(AttributeUsageAttribute), inherit: true)
+ .Cast()
+ .Single();
+
+ Assert.Equal(AttributeTargets.Assembly, usage.ValidOn);
+ Assert.True(usage.AllowMultiple);
+ }
+
private static ConditionalFactAttribute GetConditionalFactAttribute(string methodName)
{
return (ConditionalFactAttribute)typeof(ConditionalAttributeTests)
From ae71aca7ab3b60f15a52cf6c070b08bcd56913dd Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 15 May 2026 00:09:41 +0000
Subject: [PATCH 2/4] Move ConditionalAssemblyAttribute to V3 src and add
end-to-end test project
Agent-Logs-Url: https://github.com/dotnet/arcade/sessions/c7a5c7aa-5ae1-4d02-a32a-068fb71c1535
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
---
Arcade.slnx | 1 +
...ft.DotNet.XUnitExtensions.Shared.projitems | 1 -
.../src}/ConditionalAssemblyAttribute.cs | 2 --
.../AlwaysFalseConditionalAssemblyTests.cs | 30 ++++++++++++++++
.../tests/ConditionalAttributeTests.cs | 35 -------------------
...lwaysFalseConditionalAssembly.Tests.csproj | 32 +++++++++++++++++
...soft.DotNet.XUnitV3Extensions.Tests.csproj | 5 +++
7 files changed, 68 insertions(+), 38 deletions(-)
rename src/{Microsoft.DotNet.XUnitExtensions.Shared/Attributes => Microsoft.DotNet.XUnitV3Extensions/src}/ConditionalAssemblyAttribute.cs (98%)
create mode 100644 src/Microsoft.DotNet.XUnitV3Extensions/tests/AlwaysFalseConditionalAssemblyTests.cs
create mode 100644 src/Microsoft.DotNet.XUnitV3Extensions/tests/Microsoft.DotNet.XUnitV3Extensions.AlwaysFalseConditionalAssembly.Tests.csproj
diff --git a/Arcade.slnx b/Arcade.slnx
index ad6355b7062..7259cd2e32a 100644
--- a/Arcade.slnx
+++ b/Arcade.slnx
@@ -34,6 +34,7 @@
+
diff --git a/src/Microsoft.DotNet.XUnitExtensions.Shared/Microsoft.DotNet.XUnitExtensions.Shared.projitems b/src/Microsoft.DotNet.XUnitExtensions.Shared/Microsoft.DotNet.XUnitExtensions.Shared.projitems
index 63ea580b417..9807b89e6ad 100644
--- a/src/Microsoft.DotNet.XUnitExtensions.Shared/Microsoft.DotNet.XUnitExtensions.Shared.projitems
+++ b/src/Microsoft.DotNet.XUnitExtensions.Shared/Microsoft.DotNet.XUnitExtensions.Shared.projitems
@@ -10,7 +10,6 @@
-
diff --git a/src/Microsoft.DotNet.XUnitExtensions.Shared/Attributes/ConditionalAssemblyAttribute.cs b/src/Microsoft.DotNet.XUnitV3Extensions/src/ConditionalAssemblyAttribute.cs
similarity index 98%
rename from src/Microsoft.DotNet.XUnitExtensions.Shared/Attributes/ConditionalAssemblyAttribute.cs
rename to src/Microsoft.DotNet.XUnitV3Extensions/src/ConditionalAssemblyAttribute.cs
index ca13d99a883..16cbd885aad 100644
--- a/src/Microsoft.DotNet.XUnitExtensions.Shared/Attributes/ConditionalAssemblyAttribute.cs
+++ b/src/Microsoft.DotNet.XUnitV3Extensions/src/ConditionalAssemblyAttribute.cs
@@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-#if USES_XUNIT_3
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
@@ -57,4 +56,3 @@ internal bool EvaluateParameterHelper()
}
}
}
-#endif
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 4d6382a7472..f98b5558667 100644
--- a/src/Microsoft.DotNet.XUnitV3Extensions/tests/ConditionalAttributeTests.cs
+++ b/src/Microsoft.DotNet.XUnitV3Extensions/tests/ConditionalAttributeTests.cs
@@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using System.Reflection;
using Xunit;
@@ -89,28 +88,6 @@ public void ValidateConditionalTheoryTrueReceivedArgs()
Assert.NotNull(GetConditionalTheoryAttribute(nameof(ConditionalTheoryTrue)));
}
- [Fact]
- public void ConditionalAssemblyAttribute_TrueCondition_ReturnsNoTraits()
- {
- ConditionalAssemblyAttribute attribute = new ConditionalAssemblyAttribute(typeof(ConditionalAttributeTests), nameof(AlwaysTrue));
-
- IReadOnlyCollection> traits = attribute.GetTraits();
-
- Assert.Empty(traits);
- }
-
- [Fact]
- public void ConditionalAssemblyAttribute_FalseCondition_ReturnsFailingCategoryTrait()
- {
- ConditionalAssemblyAttribute attribute = new ConditionalAssemblyAttribute(typeof(ConditionalAttributeTests), nameof(AlwaysFalse));
-
- IReadOnlyCollection> traits = attribute.GetTraits();
-
- KeyValuePair trait = Assert.Single(traits);
- Assert.Equal(XunitConstants.Category, trait.Key);
- Assert.Equal("failing", trait.Value);
- }
-
[Fact]
public void ConditionalAssemblyAttribute_MultipleConditions_AllTrue_ReturnsNoTraits()
{
@@ -166,18 +143,6 @@ public void ConditionalAssemblyAttribute_StoresConstructorArguments()
Assert.Equal(new[] { nameof(AlwaysTrue), nameof(AlwaysFalse) }, attribute.ConditionMemberNames);
}
- [Fact]
- public void ConditionalAssemblyAttribute_AttributeUsage_TargetsAssemblyOnly()
- {
- AttributeUsageAttribute usage = typeof(ConditionalAssemblyAttribute)
- .GetCustomAttributes(typeof(AttributeUsageAttribute), inherit: true)
- .Cast()
- .Single();
-
- Assert.Equal(AttributeTargets.Assembly, usage.ValidOn);
- Assert.True(usage.AllowMultiple);
- }
-
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..7fb79c56f7c
--- /dev/null
+++ b/src/Microsoft.DotNet.XUnitV3Extensions/tests/Microsoft.DotNet.XUnitV3Extensions.AlwaysFalseConditionalAssembly.Tests.csproj
@@ -0,0 +1,32 @@
+
+
+
+ $(BundledNETCoreAppTargetFramework)
+ XUnitV3
+ Exe
+
+ --filter-not-trait "category=failing" --ignore-exit-code 8
+
+ 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
+
+
+
+
+
From 224172d91e4674fb26367f0b817079bf3a4eef99 Mon Sep 17 00:00:00 2001
From: Andriy Svyryd
Date: Thu, 14 May 2026 17:34:26 -0700
Subject: [PATCH 3/4] Also use additional arguments for Helix
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
...Extensions.AlwaysFalseConditionalAssembly.Tests.csproj | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
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
index 7fb79c56f7c..d83212ba1a3 100644
--- a/src/Microsoft.DotNet.XUnitV3Extensions/tests/Microsoft.DotNet.XUnitV3Extensions.AlwaysFalseConditionalAssembly.Tests.csproj
+++ b/src/Microsoft.DotNet.XUnitV3Extensions/tests/Microsoft.DotNet.XUnitV3Extensions.AlwaysFalseConditionalAssembly.Tests.csproj
@@ -10,8 +10,14 @@
tests with that trait so the deliberately failing test is skipped, which validates
that ConditionalAssemblyAttribute is applied to all tests in the assembly. After the
filter, no tests run, so ignore the MTP "zero tests" exit code (8).
+
+ Keep the argument string in one property and surface it through both
+ TestRunnerAdditionalArguments and Arguments so it applies to direct execution and
+ to Helix XUnitV3 work item creation paths that only propagate Arguments.
-->
- --filter-not-trait "category=failing" --ignore-exit-code 8
+ --filter-not-trait "category=failing" --ignore-exit-code 8
+ $(ConditionalAssemblyFilterArguments)
+ $(ConditionalAssemblyFilterArguments)
+
+
+
+ --filter-not-trait "category=failing" --ignore-exit-code 8
+