diff --git a/.github/labeler.yml b/.github/labeler.yml
index 92c6fef5..b8224c19 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -51,3 +51,10 @@
- any-glob-to-any-file:
- "src/Allure.TestingPlatform/**"
- "tests/Allure.TestingPlatform.Tests/**"
+
+"theme:xunit.v3":
+ - changed-files:
+ - any-glob-to-any-file:
+ - "src/Allure.Xunit.v3/**"
+ - "src/Allure.Xunit.v3.Generator/**"
+ - "tests/Allure.Xunit.v3.Tests/**"
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index c492b6a1..0faf1525 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -3,6 +3,7 @@ name: Test
env:
SOLUTION_PATH: allure-csharp.slnx
BUILD_CONFIGURATION: 'Release'
+ PACK_CONFIGURATION: 'Publish'
on:
workflow_dispatch:
@@ -53,7 +54,7 @@ jobs:
dotnet pack ${{ env.SOLUTION_PATH }}\
--no-restore\
--no-build\
- --configuration ${{ env.BUILD_CONFIGURATION }}
+ --configuration ${{ env.PACK_CONFIGURATION }}
- name: Build test samples
run: |
@@ -62,6 +63,7 @@ jobs:
-p:Allure_SampleConfiguration=${{ env.BUILD_CONFIGURATION }}
- name: 'Run test samples'
+ continue-on-error: true
run: |
dotnet msbuild ${{ env.SOLUTION_PATH }}\
-t:Allure_RunTestSamples\
@@ -85,6 +87,10 @@ jobs:
--no-restore\
--no-build\
--configuration ${{ env.BUILD_CONFIGURATION }}
+ npx -y allure@3 run --config ./.allurerc.mjs --rerun 2 --environment="${{ env.BUILD_CONFIGURATION }}" --dump="allure-results-xunit-v3" -- dotnet run --project ./tests/Allure.Xunit.v3.Tests\
+ --no-restore\
+ --no-build\
+ --configuration ${{ env.BUILD_CONFIGURATION }}
npx -y allure@3 run --config ./.allurerc.mjs --rerun 2 --environment="${{ env.BUILD_CONFIGURATION }}" --dump="allure-results-specflow" -- dotnet test ./tests/Allure.SpecFlow.Tests\
--no-restore\
--no-build\
diff --git a/allure-csharp.slnx b/allure-csharp.slnx
index e6bda7d1..6e96f624 100644
--- a/allure-csharp.slnx
+++ b/allure-csharp.slnx
@@ -67,6 +67,12 @@
+
+
+
+
+
+
@@ -96,6 +102,9 @@
+
+
+
diff --git a/build/Allure.Build.Tasks/FillSampleMetadata.cs b/build/Allure.Build.Tasks/FillSampleMetadata.cs
index dc42ffd1..02818ffd 100644
--- a/build/Allure.Build.Tasks/FillSampleMetadata.cs
+++ b/build/Allure.Build.Tasks/FillSampleMetadata.cs
@@ -20,6 +20,9 @@ public class FillSampleMetadata : Task
[Required]
public string ProjectDirectory { get; set; }
+ [Required]
+ public string TestingPlatform { get; set; }
+
[Required]
public string SampleSolutionDirectory { get; set; }
@@ -145,6 +148,9 @@ void SetResultsDirectory(ITaskItem2 item)
this.BinDirectory,
item.GetMetadata("ProjectName"),
$"{configuration}_{targetFramework}",
+ this.TestingPlatform is "MicrosoftTestingPlatform"
+ ? "TestResults"
+ : "",
"allure-results"
);
item.SetMetadataValueLiteral("ResultsDirectory", defaultResultsDirectory);
diff --git a/build/Allure.Build.Tasks/Functions/Files.cs b/build/Allure.Build.Tasks/Functions/Files.cs
index c1a8cb5c..526275d3 100644
--- a/build/Allure.Build.Tasks/Functions/Files.cs
+++ b/build/Allure.Build.Tasks/Functions/Files.cs
@@ -60,6 +60,7 @@ public static GeneratedFileSource DirectoryBuildProps(
string solutionDir,
string targetFrameworks,
string outputType,
+ IEnumerable<(string name, string value)> properties,
IEnumerable imports,
IEnumerable packages,
IEnumerable projects,
@@ -70,6 +71,7 @@ IEnumerable analyzerProjects
imports: imports,
targetFrameworks: targetFrameworks,
outputType: outputType,
+ properties: properties,
packages: packages,
projects: projects,
analyzerProjects: analyzerProjects
diff --git a/build/Allure.Build.Tasks/Functions/XmlDefinitions.cs b/build/Allure.Build.Tasks/Functions/XmlDefinitions.cs
index a70ddf0d..64330227 100644
--- a/build/Allure.Build.Tasks/Functions/XmlDefinitions.cs
+++ b/build/Allure.Build.Tasks/Functions/XmlDefinitions.cs
@@ -106,6 +106,7 @@ public static XDocument DirectoryBuildProps(
IEnumerable imports,
string targetFrameworks,
string outputType,
+ IEnumerable<(string name, string value)> properties,
IEnumerable packages,
IEnumerable projects,
IEnumerable analyzerProjects
@@ -120,7 +121,8 @@ IEnumerable analyzerProjects
("OutputType", outputType),
("EnableDefaultItems", "false"),
("IsTestProject", "true"),
- ("AspectInjector_Enabled", "false")
+ ("AspectInjector_Enabled", "false"),
+ ..properties,
]),
MsBuild.GetItemGroup("Compile", [("Include", Path.Combine("**", "*.cs"))]),
MsBuild.GetItemGroup("Clean", [
diff --git a/build/Allure.Build.Tasks/GenerateSampleSolution.cs b/build/Allure.Build.Tasks/GenerateSampleSolution.cs
index 7bca0e93..b92a797f 100644
--- a/build/Allure.Build.Tasks/GenerateSampleSolution.cs
+++ b/build/Allure.Build.Tasks/GenerateSampleSolution.cs
@@ -22,6 +22,9 @@ public class GenerateSampleSolution : Task
[Required]
public ITaskItem[] SampleProjectReferences { get; set; }
+ [Required]
+ public ITaskItem[] SampleProperties { get; set; }
+
[Required]
public string ProjectDirectory { get; set; }
@@ -50,6 +53,8 @@ public class GenerateSampleSolution : Task
IEnumerable SampleSources2 => this.SampleSources.Cast();
+ IEnumerable SampleProperties2 => this.SampleProperties.Cast();
+
IEnumerable SamplePackageReferences2 =>
this.SamplePackageReferences.Cast();
@@ -125,6 +130,9 @@ GeneratedFileSource PrepareDirectoryBuildProps(MsBuildImportFiles imports) =>
solutionDir: this.SampleSolutionDir,
targetFrameworks: this.SampleTargetFrameworks,
outputType: Projects.ResolveOutputType(this.TestingPlatform),
+ properties: this.SampleProperties2.Select(
+ (item) => (item.EvaluatedIncludeEscaped, item.GetMetadataValueEscaped("Value"))
+ ),
imports.PropsFiles,
packages: this.CommonPackageNames,
projects: this.SampleProjectReferences.Select((item) =>
diff --git a/src/Allure.TestingPlatform/Functions/ConfigurationFunctions.cs b/src/Allure.TestingPlatform/Functions/ConfigurationFunctions.cs
index 3b9c964c..cb7a2944 100644
--- a/src/Allure.TestingPlatform/Functions/ConfigurationFunctions.cs
+++ b/src/Allure.TestingPlatform/Functions/ConfigurationFunctions.cs
@@ -48,14 +48,14 @@ public static AllureConfiguration ReadConfiguration(
);
}
+ configPath ??= Path.Combine(
+ AppDomain.CurrentDomain.BaseDirectory,
+ AllureConstants.CONFIG_FILENAME
+ );
+
JObject configJson = File.Exists(configPath)
? JObject.Parse(File.ReadAllText(configPath))
- : Path.Combine(
- AppDomain.CurrentDomain.BaseDirectory,
- AllureConstants.CONFIG_FILENAME
- ) is { } defaultConfigPath && File.Exists(defaultConfigPath)
- ? JObject.Parse(File.ReadAllText(defaultConfigPath))
- : [];
+ : [];
var allureSection = configJson["allure"] is { } allureObject
? allureObject
diff --git a/src/Allure.TestingPlatform/Functions/ModelFunctionExtensions.cs b/src/Allure.TestingPlatform/Functions/ModelFunctionExtensions.cs
index 946f50b8..8426b442 100644
--- a/src/Allure.TestingPlatform/Functions/ModelFunctionExtensions.cs
+++ b/src/Allure.TestingPlatform/Functions/ModelFunctionExtensions.cs
@@ -8,6 +8,7 @@
using Allure.Net.Commons.Configuration;
using Allure.Net.Commons.Functions;
using Allure.Net.Commons.Sdk;
+using Allure.TestingPlatform.Sdk.Properties;
using Microsoft.Testing.Platform.Extensions.Messages;
namespace Allure.TestingPlatform.Functions;
@@ -16,8 +17,7 @@ static class ModelFunctionExtensions
{
extension (ModelFunctions)
{
- public static TestResult CreateTestResult(AllureConfiguration config) =>
- new()
+ public static TestResult CreateTestResult(AllureConfiguration config) => new()
{
uuid = IdFunctions.CreateUUID(),
labels = [
@@ -202,5 +202,11 @@ public static void AddGlobalAttachmentFile(IAllureResultsWriter writer, string?
}
);
}
+
+ public static bool IsCancelled(TestResult result) =>
+ result.labels.Any(IsCancellationMarker);
+
+ public static bool IsCancellationMarker(Label label) =>
+ label.name == AllureCancelProperty.CANCEL_LABEL_NAME && label.value == "true";
}
}
diff --git a/src/Allure.TestingPlatform/Registration/IStandaloneAllureRegistrationContext.cs b/src/Allure.TestingPlatform/Registration/IStandaloneAllureRegistrationContext.cs
index 2bf07db8..2288c8a1 100644
--- a/src/Allure.TestingPlatform/Registration/IStandaloneAllureRegistrationContext.cs
+++ b/src/Allure.TestingPlatform/Registration/IStandaloneAllureRegistrationContext.cs
@@ -3,7 +3,6 @@
using Allure.Net.Commons;
using Allure.Net.Commons.Configuration;
using Allure.Net.Commons.Sdk;
-using Microsoft.Testing.Platform.Logging;
namespace Allure.TestingPlatform.Registration;
diff --git a/src/Allure.TestingPlatform/Sdk/Properties/AllureCancelProperty.cs b/src/Allure.TestingPlatform/Sdk/Properties/AllureCancelProperty.cs
new file mode 100644
index 00000000..f00fd914
--- /dev/null
+++ b/src/Allure.TestingPlatform/Sdk/Properties/AllureCancelProperty.cs
@@ -0,0 +1,21 @@
+using Allure.Net.Commons;
+using Allure.TestingPlatform.Sdk.Runtime;
+
+namespace Allure.TestingPlatform.Sdk.Properties;
+
+///
+/// Cancels the test result precluding it from being written to the results directory.
+///
+public sealed class AllureCancelProperty() : IAllureProperty
+{
+ ///
+ public void Apply(LiveAllureTestingPlatformRuntime _, TestResult target)
+ {
+ target.labels.Add(new(){ name = CANCEL_LABEL_NAME, value = "true" });
+ }
+
+ ///
+ /// The name of the marker label that signals the test result was cancelled.
+ ///
+ public const string CANCEL_LABEL_NAME = "__allure_cancelled__";
+}
diff --git a/src/Allure.TestingPlatform/Sdk/TestingPlatformExtensions/AllureDataConsumer.cs b/src/Allure.TestingPlatform/Sdk/TestingPlatformExtensions/AllureDataConsumer.cs
index af3c1f1a..122c5c38 100644
--- a/src/Allure.TestingPlatform/Sdk/TestingPlatformExtensions/AllureDataConsumer.cs
+++ b/src/Allure.TestingPlatform/Sdk/TestingPlatformExtensions/AllureDataConsumer.cs
@@ -180,17 +180,7 @@ async Task ConsumeTestNodeUpdateMessage(CorrelationUid correlationUid, TestNodeU
runningTestContext = state.ForkContext(testContextUid, runningTestContext, this.StartTest);
}
- state.ReleaseContext(
- testContextUid,
- () => this.Lifecycle
- .UpdateTestCase((testResult) =>
- {
- this.ApplyProperties(testResult, node);
- ApplyFallbacks(testResult, node);
- })
- .StopTestCase()
- .WriteTestCase()
- );
+ state.ReleaseContext(testContextUid, () => this.FinalizeTestResult(node));
}
async Task ConsumeSessionFileArtifactMessage(SessionFileArtifact message) =>
@@ -337,4 +327,25 @@ static TestResult ApplyTestMethodIdentifierProperty(TestResult testResult, TestM
ModelFunctions.ApplyIdentityAsFallback(testResult, identifierProperty);
return testResult;
}
+
+ void FinalizeTestResult(TestNode node)
+ {
+ bool isCancelled = false;
+ this.Lifecycle.UpdateTestCase((testResult) =>
+ {
+ this.ApplyProperties(testResult, node);
+ if (ModelFunctions.IsCancelled(testResult))
+ {
+ isCancelled = true;
+ return;
+ }
+ ApplyFallbacks(testResult, node);
+ });
+
+ if (!isCancelled)
+ {
+ this.Lifecycle.StopTestCase();
+ this.Lifecycle.WriteTestCase();
+ }
+ }
}
diff --git a/src/Allure.Xunit.v3.Generator/Allure.Xunit.v3.Generator.csproj b/src/Allure.Xunit.v3.Generator/Allure.Xunit.v3.Generator.csproj
new file mode 100644
index 00000000..064b710c
--- /dev/null
+++ b/src/Allure.Xunit.v3.Generator/Allure.Xunit.v3.Generator.csproj
@@ -0,0 +1,28 @@
+
+
+
+ netstandard2.0
+ Allure.Xunit.Generator
+ enable
+ Qameta Software Inc
+
+ A source generator that automatically enables Allure.Xunit.v3 for a
+ Microsoft Testing Platform C# project and adds support for Allure test
+ plan pre-filtering.
+
+
+ true
+
+
+
+
+ false
+ false
+
+
+
+
+
+
+
diff --git a/src/Allure.Xunit.v3.Generator/AllureXunitGenerator.cs b/src/Allure.Xunit.v3.Generator/AllureXunitGenerator.cs
new file mode 100644
index 00000000..73aeb458
--- /dev/null
+++ b/src/Allure.Xunit.v3.Generator/AllureXunitGenerator.cs
@@ -0,0 +1,542 @@
+using System;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using Allure.Xunit.Generator.Constants;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Allure.Xunit.Generator;
+
+
+///
+/// Generates Allure.Xunit.v3 assembly setup, entry point helpers, and test plan
+/// pre-filtering support for xUnit.net v3 projects that run with Microsoft Testing Platform.
+///
+[Generator]
+public sealed class AllureXunitGenerator : IIncrementalGenerator
+{
+ ///
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var options = SetupGeneratorOptionsStream(context);
+ var hasTestHook = SetupHasAllureXunitAttributeStream(context);
+ var selfRegistration = SetupSelfRegistrationStream(context);
+ var allureIdMethods = SetupAllureIdMethodStream(context);
+
+ context.RegisterPostInitializationOutput(GenerateAllureXunitRunner);
+ context.RegisterSourceOutput(options.Combine(hasTestHook), GenerateAllureXunitAssemblyAttribute);
+ context.RegisterSourceOutput(options.Combine(selfRegistration), GenerateAllureXunitEntryPoint);
+ context.RegisterSourceOutput(allureIdMethods, GenerateAllureXunitTestPlan);
+ }
+
+ static IncrementalValueProvider SetupSelfRegistrationStream(
+ IncrementalGeneratorInitializationContext context
+ ) =>
+ context.AnalyzerConfigOptionsProvider
+ .Select(ResolveSelfRegisteredExtensionsTypeName)
+ .Combine(context.CompilationProvider)
+ .Select(VerifySelfRegistration);
+
+ static string ResolveSelfRegisteredExtensionsTypeName(
+ AnalyzerConfigOptionsProvider optionsProvider,
+ CancellationToken token
+ ) =>
+ optionsProvider.GlobalOptions
+ .TryGetValue(Options.RootNamespace, out var rootNamespace)
+ && !string.IsNullOrEmpty(rootNamespace)
+ ? $"{rootNamespace}.{TypeNames.SelfRegisteredExtensions}"
+ : TypeNames.SelfRegisteredExtensions;
+
+ static IncrementalValueProvider> SetupAllureIdMethodStream(
+ IncrementalGeneratorInitializationContext context
+ ) =>
+ context.SyntaxProvider
+ .CreateSyntaxProvider(
+ predicate: IsMethodWithAttributes,
+ transform: ToAllureIdMethodKeyValuePair
+ )
+ .Where(static (kv) => kv is not null)
+ .Select(static (kv, _) => kv!.Value)
+ .Collect();
+
+ static bool IsMethodWithAttributes(SyntaxNode node, CancellationToken _) =>
+ node is MethodDeclarationSyntax methodDeclaration
+ && methodDeclaration.AttributeLists.Count > 0;
+
+ static (int, string)? ToAllureIdMethodKeyValuePair(GeneratorSyntaxContext ctx, CancellationToken _)
+ {
+ var semanticModel = ctx.SemanticModel;
+ var methodDeclaration = (MethodDeclarationSyntax)ctx.Node;
+
+ var allureIdAttributeType = semanticModel.Compilation.GetTypeByMetadataName(Types.AllureIdAttribute);
+ if (allureIdAttributeType is null)
+ {
+ return null;
+ }
+
+ foreach (var attributeList in methodDeclaration.AttributeLists)
+ {
+ foreach (var attributeApplicationSyntax in attributeList.Attributes)
+ {
+ if (semanticModel.GetSymbolInfo(attributeApplicationSyntax).Symbol is not IMethodSymbol attributeApplication)
+ {
+ continue;
+ }
+
+ var attributeType = attributeApplication.ContainingType;
+
+ if (SymbolEqualityComparer.Default.Equals(attributeType, allureIdAttributeType))
+ {
+ var method = semanticModel.GetDeclaredSymbol(methodDeclaration);
+ if (method is not null)
+ {
+ foreach (var attributeData in method.GetAttributes())
+ {
+ if (!SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, allureIdAttributeType))
+ {
+ continue;
+ }
+
+ var allureIdObj = attributeData.ConstructorArguments[0].Value;
+ if (allureIdObj is int allureId)
+ {
+ var typeFullName = method.ContainingType.ToDisplayString(FullyQualifiedNoTypeParameters);
+ var methodName = method.Name;
+ return (allureId, $"{typeFullName}.{methodName}");
+ }
+ }
+ }
+
+ return null;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ static string VerifySelfRegistration(
+ (string selfRegistrationTypeName, Compilation compilation) input,
+ CancellationToken token
+ )
+ {
+ var (selfRegistrationTypeName, compilation) = input;
+
+ var builderType = compilation.GetTypeByMetadataName(
+ "Microsoft.Testing.Platform.Builder.ITestApplicationBuilder"
+ );
+ if (builderType is null)
+ {
+ return "";
+ }
+
+ var selfRegisteredExtensionsType =
+ compilation.GetTypeByMetadataName(selfRegistrationTypeName);
+
+ if (selfRegisteredExtensionsType is null)
+ {
+ return "";
+ }
+
+ foreach (var member in selfRegisteredExtensionsType.GetMembers(MethodNames.AddSelfRegisteredExtensions))
+ {
+ if (ToRegistrationMethod(builderType, member) is { } method)
+ {
+ return GetMethodGroupExpression(method);
+ }
+ }
+
+ return "";
+ }
+
+ static IMethodSymbol? ToRegistrationMethod(INamedTypeSymbol builderType, ISymbol? member) =>
+ member is IMethodSymbol
+ {
+ IsStatic: true,
+ IsExtensionMethod: true,
+ ReturnsVoid: true,
+ DeclaredAccessibility: Accessibility.Public,
+ Parameters: { Length: 2 } parameters,
+ } method
+ && SymbolEqualityComparer.Default.Equals(parameters[0].Type, builderType)
+ && parameters[1].Type is IArrayTypeSymbol
+ {
+ ElementType.SpecialType: SpecialType.System_String,
+ }
+ ? method
+ : null;
+
+
+ static IncrementalValueProvider SetupGeneratorOptionsStream(
+ IncrementalGeneratorInitializationContext context
+ ) =>
+ context
+ .AnalyzerConfigOptionsProvider
+ .Select(ReadGeneratorProperties);
+
+ static IncrementalValueProvider SetupHasAllureXunitAttributeStream(
+ IncrementalGeneratorInitializationContext context
+ ) =>
+ context
+ .CompilationProvider
+ .Select(HasAllureXunitAttribute);
+
+ static AllureXunitGeneratorOptions ReadGeneratorProperties(
+ AnalyzerConfigOptionsProvider provider,
+ CancellationToken _
+ )
+ {
+ var opts = provider.GlobalOptions;
+ return new AllureXunitGeneratorOptions(
+ GenerateEntryPoint: ReadBooleanProperty(opts, Options.GenerateEntryPoint),
+ ApplyAttribute: ReadBooleanProperty(opts, Options.ApplyAttribute)
+ );
+
+ }
+
+ static bool ReadBooleanProperty(AnalyzerConfigOptions opts, string key) =>
+ !opts.TryGetValue(key, out var generateEntryPoint)
+ || generateEntryPoint.Equals("true", StringComparison.InvariantCultureIgnoreCase);
+
+ static string GetMethodGroupExpression(IMethodSymbol method) =>
+ $"{method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}.{method.Name}";
+
+ static bool HasAllureXunitAttribute(Compilation compilation, CancellationToken _)
+ {
+ var attributeType =
+ compilation.GetTypeByMetadataName(Types.AllureXunitAttribute);
+
+ if (attributeType is null)
+ {
+ return false;
+ }
+
+ foreach (var attr in compilation.Assembly.GetAttributes())
+ {
+ if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, attributeType))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ static void GenerateAllureXunitRunner(
+ IncrementalGeneratorPostInitializationContext ctx
+ )
+ {
+ ctx.AddSource("AllureXunitRunner.g.cs", AllureXunitRunnerSource);
+ }
+
+ static void GenerateAllureXunitAssemblyAttribute(
+ SourceProductionContext ctx,
+ (AllureXunitGeneratorOptions, bool) input
+ )
+ {
+ var (options, hasAllureXunit) = input;
+ if (!options.ApplyAttribute || hasAllureXunit)
+ {
+ return;
+ }
+
+ ctx.AddSource(
+ "AllureXunitAssemblyAttributes.g.cs",
+ $$"""
+ {{Preamble}}
+
+ [assembly:{{FqTypes.AllureXunitAttribute}}]
+ """
+ );
+ }
+
+ static void GenerateAllureXunitEntryPoint(
+ SourceProductionContext ctx,
+ (AllureXunitGeneratorOptions, string) input
+ )
+ {
+ var (options, selfRegistrationMethod) = input;
+
+ if (!options.GenerateEntryPoint)
+ {
+ return;
+ }
+
+ ctx.AddSource("AllureXunitEntryPoint.g.cs", GetEntryPointSource(selfRegistrationMethod));
+ }
+
+ static void GenerateAllureXunitTestPlan(
+ SourceProductionContext ctx,
+ ImmutableArray<(int, string)> allureIdMethods
+ )
+ {
+ ctx.AddSource("AllureXunitTestPlan.g.cs", GetAllureXunitTestPlanSource(allureIdMethods));
+ }
+
+ static string GetAllureXunitTestPlanSource(ImmutableArray<(int, string)> allureIdMethods)
+ {
+ var sb = new StringBuilder(
+ $$"""
+ {{Preamble}}
+ namespace Allure.Xunit
+ {
+ [{{FqTypes.ExcludeFromCodeCoverage}}]
+ internal static class AllureXunitTestPlan
+ {
+ ///
+ /// Returns a new array that includes the original arguments plus
+ /// --filter-method xUnit arguments for each test method
+ /// selected by the current test plan.
+ ///
+ ///
+ /// The original command-line arguments passed to the test application.
+ ///
+ ///
+ /// A new array that contains followed by
+ /// the xUnit.net pre-execution filter arguments for the current Allure test plan.
+ ///
+ public static string[] {{MethodNames.AddXunitPreExecutionFilterArguments}}(string[] originalArguments)
+ {
+ return {{FqTypes.TestPlanFunctions}}.{{MethodNames.AddXunitPreExecutionFilterArguments}}(originalArguments, AllureIdRegistry);
+ }
+
+ """
+ );
+
+ AddAllureIdRegistry(sb, allureIdMethods);
+
+ sb.AppendLine(
+ """
+ }
+ }
+ """
+ );
+
+ return sb.ToString();
+ }
+
+ static void AddAllureIdRegistry(StringBuilder sb, ImmutableArray<(int, string)> entries)
+ {
+ sb.AppendLine(
+ $$"""
+
+ ///
+ /// Gets a mapping from Allure ID values to the fully qualified names of methods annotated
+ /// with using those IDs.
+ ///
+ ///
+ /// Method names include the namespace, containing type name, and method name.
+ ///
+ internal static {{FqTypes.ImmutableDictionary_Int_ImmutableArray_String}} AllureIdRegistry { get; }
+
+ static AllureXunitTestPlan()
+ {
+ {{FqTypes.ImmutableDictionaryBuilder_Int_ImmutableArray_String}} builder = {{FqMethods.ImmutableDictionary_CreateBuilder_Int_String}}();
+ """
+ );
+
+ foreach (var (allureId, methodName) in entries)
+ {
+ sb.AppendLine(
+ $$"""
+ AddAllureIdMethodEntry(builder, {{allureId}}, {{SymbolDisplay.FormatLiteral(methodName, quote: true)}});
+ """
+ );
+ }
+
+ sb.AppendLine(
+ $$"""
+ AllureIdRegistry = builder.ToImmutable();
+ }
+
+ static void AddAllureIdMethodEntry({{FqTypes.ImmutableDictionaryBuilder_Int_ImmutableArray_String}} builder, int allureId, string methodName)
+ {
+ {{FqTypes.ImmutableArray_String}} value;
+ builder[allureId] = builder.TryGetValue(allureId, out value)
+ ? value.Add(methodName)
+ : {{FqMethods.ImmutableArray_Create}}(methodName);
+ }
+ """
+ );
+ }
+
+ static string AllureXunitRunnerSource =>
+ $$"""
+ {{Preamble}}
+ namespace Allure.Xunit
+ {
+ ///
+ /// Defines a helper for running xUnit.net v3 with Allure from a custom entry point.
+ ///
+ [{{FqTypes.ExcludeFromCodeCoverage}}]
+ internal static class AllureXunitRunner
+ {
+ ///
+ /// Applies the test plan pre-execution filter and calls xUnit's
+ /// with an
+ /// explicit extension registration function.
+ ///
+ ///
+ /// A function that registers Microsoft Testing Platform extensions. The function must call
+ /// in order to enable Allure.
+ ///
+ /// The command-line arguments passed to the test application.
+ /// The xUnit.net process exit code.
+ public static async {{FqTypes.Task_Int}} RunAsync(
+ {{FqTypes.Action(FqTypes.ITestApplicationBuilder, "string[]")}} extensionRegistration,
+ string[] args
+ )
+ {
+ return await {{FqTypes.TestPlatformTestFramework}}.RunAsync(
+ {{FqMethods.AddXunitPreExecutionFilterArguments}}(args),
+ extensionRegistration
+ );
+ }
+ }
+ }
+ """;
+
+ static string GetEntryPointSource(string addSelfRegisteredExtensions)
+ {
+ var selfRegistrationExists = addSelfRegisteredExtensions is { Length: >0 };
+
+ var sb = new StringBuilder(
+ $$"""
+ {{Preamble}}
+
+ namespace Allure.Xunit
+ {
+ ///
+ /// Defines the functions for running xUnit.net v3 with Allure and other self-registered
+ /// Microsoft Testing Platform extensions enabled.
+ ///
+ [{{FqTypes.ExcludeFromCodeCoverage}}]
+ internal class AllureXunitEntryPoint
+ {
+ ///
+ /// Applies the test plan pre-execution filter and calls xUnit's
+ ///
+ /// with the self-registered Microsoft Testing Platform extensions.
+ ///
+ ///
+ /// If the arguments include -automated or @@, delegates to
+ /// ,
+ /// which does not register Microsoft Testing Platform extensions.
+ /// Allure does not run in that case.
+ ///
+ /// Otherwise, if Allure self-registration is enabled, it will be registered
+ /// with the default settings.
+ ///
+ /// The command-line arguments passed to the test application.
+ /// The xUnit.net process exit code.
+ public static async {{FqTypes.Task_Int}} Main(string[] args)
+ {
+ if ({{FqMethods.Enumerable_Any}}(args, arg => arg == "-automated" || arg == "@@"))
+ return await {{FqMethods.ConsoleRunner_Run}}(args);
+ else
+
+ """
+ );
+
+ if (selfRegistrationExists)
+ {
+ sb.AppendLine(
+ $$"""
+ return await {{FqMethods.AllureXunitRunner_RunAsync}}({{addSelfRegisteredExtensions}}, args);
+ """
+ );
+ }
+ else
+ {
+ sb.AppendLine(
+ $$"""
+ {
+ {{FqMethods.Error_WriteLine}}("{{Messages.SelfRegistrationNotFound}}");
+ return 1;
+ }
+ """
+ );
+ }
+
+ sb.AppendLine(
+ $$"""
+ }
+
+ ///
+ /// Applies the test plan pre-execution filter and calls xUnit's
+ /// with the
+ /// self-registered Microsoft Testing Platform extensions, then enables Allure
+ /// with a custom registration function.
+ ///
+ ///
+ /// If you call this function, make sure Allure.Xunit.v3 self-registration is disabled
+ /// (the 'Allure_XunitEnableSelfRegistration' MSBuild property is set to 'false').
+ ///
+ ///
+ /// A function that configures Allure.Xunit.v3.
+ ///
+ /// The command-line arguments passed to the test application.
+ /// The xUnit.net process exit code.
+ public static async {{FqTypes.Task_Int}} RunAsync(
+ {{FqTypes.Action(FqTypes.IStandaloneAllureRegistrationContext)}} allureRegistration,
+ string[] args
+ )
+ {
+ """
+ );
+
+ if (selfRegistrationExists)
+ {
+ sb.AppendLine(
+ $$"""
+ return await {{FqMethods.AllureXunitRunner_RunAsync}}((builder, args) =>
+ {
+ {{addSelfRegisteredExtensions}}(builder, args);
+ {{FqMethods.AddAllureXunit}}(builder, allureRegistration);
+ }, args);
+ """
+ );
+ }
+ else
+ {
+ sb.AppendLine(
+ $$"""
+ {{FqMethods.Error_WriteLine}}("{{Messages.SelfRegistrationNotFound}}");
+ return 1;
+ """
+ );
+ }
+
+ sb.AppendLine(
+ $$"""
+ }
+ }
+ }
+ """
+ );
+
+ return sb.ToString();
+ }
+
+ static string Preamble =>
+ $$"""
+ //
+ // This file was generated by {{typeof(AllureXunitGenerator).AssemblyQualifiedName}}.
+ // Do not edit this file manually; changes may be overwritten.
+ //
+ """;
+
+ static readonly SymbolDisplayFormat FullyQualifiedNoTypeParameters = new(
+ globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted,
+ typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces
+ );
+
+ readonly record struct AllureXunitGeneratorOptions(
+ bool GenerateEntryPoint,
+ bool ApplyAttribute
+ );
+}
diff --git a/src/Allure.Xunit.v3.Generator/CompilerHints/IsExternalInit.cs b/src/Allure.Xunit.v3.Generator/CompilerHints/IsExternalInit.cs
new file mode 100644
index 00000000..fc1fbfd0
--- /dev/null
+++ b/src/Allure.Xunit.v3.Generator/CompilerHints/IsExternalInit.cs
@@ -0,0 +1,8 @@
+using System.ComponentModel;
+
+#pragma warning disable IDE0130
+
+namespace System.Runtime.CompilerServices;
+
+[EditorBrowsable(EditorBrowsableState.Never)]
+internal static class IsExternalInit { }
diff --git a/src/Allure.Xunit.v3.Generator/CompilerHints/NotNullWhen.cs b/src/Allure.Xunit.v3.Generator/CompilerHints/NotNullWhen.cs
new file mode 100644
index 00000000..463e4708
--- /dev/null
+++ b/src/Allure.Xunit.v3.Generator/CompilerHints/NotNullWhen.cs
@@ -0,0 +1,9 @@
+#pragma warning disable IDE0130
+
+namespace System.Diagnostics.CodeAnalysis;
+
+[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
+internal sealed class NotNullWhenAttribute(bool returnValue) : Attribute
+{
+ public bool ReturnValue { get; } = returnValue;
+}
diff --git a/src/Allure.Xunit.v3.Generator/Constants/FqMethods.cs b/src/Allure.Xunit.v3.Generator/Constants/FqMethods.cs
new file mode 100644
index 00000000..a0f9631e
--- /dev/null
+++ b/src/Allure.Xunit.v3.Generator/Constants/FqMethods.cs
@@ -0,0 +1,20 @@
+namespace Allure.Xunit.Generator.Constants;
+
+static class FqMethods
+{
+ public const string ConsoleRunner_Run = "global::Xunit.Runner.InProc.SystemConsole.ConsoleRunner.Run";
+
+ public const string Error_WriteLine = "global::System.Console.Error.WriteLine";
+
+ public const string Enumerable_Any = "global::System.Linq.Enumerable.Any";
+
+ public const string AddAllureXunit = "global::Allure.Xunit.AllureXunitExtensions.AddAllureXunit";
+
+ public const string ImmutableArray_Create = $"{FqTypes.ImmutableArray}.Create";
+
+ public const string ImmutableDictionary_CreateBuilder_Int_String = $"global::System.Collections.Immutable.ImmutableDictionary.CreateBuilder";
+
+ public const string AddXunitPreExecutionFilterArguments = $"global::Allure.Xunit.AllureXunitTestPlan.{MethodNames.AddXunitPreExecutionFilterArguments}";
+
+ public const string AllureXunitRunner_RunAsync = $"global::Allure.Xunit.AllureXunitRunner.RunAsync";
+}
diff --git a/src/Allure.Xunit.v3.Generator/Constants/FqTypes.cs b/src/Allure.Xunit.v3.Generator/Constants/FqTypes.cs
new file mode 100644
index 00000000..9a7055a9
--- /dev/null
+++ b/src/Allure.Xunit.v3.Generator/Constants/FqTypes.cs
@@ -0,0 +1,37 @@
+namespace Allure.Xunit.Generator.Constants;
+
+static class FqTypes
+{
+ public const string ITestApplicationBuilder = $"global::{Types.ITestApplicationBuilder}";
+
+ public const string AllureXunitAttribute = $"global::{Types.AllureXunitAttribute}";
+
+ public const string ExcludeFromCodeCoverage = "global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage";
+
+ public const string TestPlatformTestFramework = "global::Xunit.MicrosoftTestingPlatform.TestPlatformTestFramework";
+
+ public const string IStandaloneAllureRegistrationContext = "global::Allure.TestingPlatform.Registration.IStandaloneAllureRegistrationContext";
+
+ public const string Task_Int = "global::System.Threading.Tasks.Task";
+
+ public const string TestPlanFunctions = "global::Allure.Xunit.Functions.TestPlanFunctions";
+
+ public const string ImmutableArray = $"global::System.Collections.Immutable.ImmutableArray";
+
+ public const string ImmutableArray_String = $"{ImmutableArray}";
+
+ public const string ImmutableDictionary = $"global::System.Collections.Immutable.ImmutableDictionary";
+
+ public const string ImmutableDictionary_Int_ImmutableArray_String = $"{ImmutableDictionary}";
+
+ public const string ImmutableDictionaryBuilder_Int_ImmutableArray_String = $"{ImmutableDictionary_Int_ImmutableArray_String}.Builder";
+
+ public static string Action(string argumentType) =>
+ $"global::System.Action<{argumentType}>";
+
+ public static string Action(string argument1Type, string argument2Type) =>
+ $"global::System.Action<{argument1Type}, {argument2Type}>";
+
+ public static string AllureIdAttribute = "global::Allure.Net.Commons.Attributes.AllureIdAttribute";
+
+}
diff --git a/src/Allure.Xunit.v3.Generator/Constants/Messages.cs b/src/Allure.Xunit.v3.Generator/Constants/Messages.cs
new file mode 100644
index 00000000..9f2abb57
--- /dev/null
+++ b/src/Allure.Xunit.v3.Generator/Constants/Messages.cs
@@ -0,0 +1,10 @@
+namespace Allure.Xunit.Generator.Constants;
+
+static class Messages
+{
+ public const string SelfRegistrationNotFound =
+ $"[Allure.Xunit.v3]: Could not find the {TypeNames.SelfRegisteredExtensions} "
+ + $"class with the {MethodNames.AddSelfRegisteredExtensions} method in "
+ + "the root namespace of the project. Make sure the Microsoft.Testing.Platform "
+ + "extension self-registration is enabled and try again.";
+}
diff --git a/src/Allure.Xunit.v3.Generator/Constants/MethodNames.cs b/src/Allure.Xunit.v3.Generator/Constants/MethodNames.cs
new file mode 100644
index 00000000..e0dafc79
--- /dev/null
+++ b/src/Allure.Xunit.v3.Generator/Constants/MethodNames.cs
@@ -0,0 +1,8 @@
+namespace Allure.Xunit.Generator.Constants;
+
+static class MethodNames
+{
+ public const string AddXunitPreExecutionFilterArguments = "AddXunitPreExecutionFilterArguments";
+
+ public const string AddSelfRegisteredExtensions = "AddSelfRegisteredExtensions";
+}
diff --git a/src/Allure.Xunit.v3.Generator/Constants/Options.cs b/src/Allure.Xunit.v3.Generator/Constants/Options.cs
new file mode 100644
index 00000000..8554ec51
--- /dev/null
+++ b/src/Allure.Xunit.v3.Generator/Constants/Options.cs
@@ -0,0 +1,10 @@
+namespace Allure.Xunit.Generator.Constants;
+
+static class Options
+{
+ public const string RootNamespace = "build_property.RootNamespace";
+
+ public const string GenerateEntryPoint = "build_property.Allure_GenerateXunitEntryPoint";
+
+ public const string ApplyAttribute = "build_property.Allure_ApplyXunitAttribute";
+}
diff --git a/src/Allure.Xunit.v3.Generator/Constants/SeeCrefs.cs b/src/Allure.Xunit.v3.Generator/Constants/SeeCrefs.cs
new file mode 100644
index 00000000..8fd45eb4
--- /dev/null
+++ b/src/Allure.Xunit.v3.Generator/Constants/SeeCrefs.cs
@@ -0,0 +1,10 @@
+namespace Allure.Xunit.Generator.Constants;
+
+static class SeeCrefs
+{
+ public const string ConsoleRunner_Run = $"{FqMethods.ConsoleRunner_Run}(string[])";
+
+ public const string AddAllureXunit =
+ $"{FqMethods.AddAllureXunit}({FqTypes.ITestApplicationBuilder}, "
+ + $"global::System.Action{{{FqTypes.IStandaloneAllureRegistrationContext}}})";
+}
diff --git a/src/Allure.Xunit.v3.Generator/Constants/TypeNames.cs b/src/Allure.Xunit.v3.Generator/Constants/TypeNames.cs
new file mode 100644
index 00000000..6765801a
--- /dev/null
+++ b/src/Allure.Xunit.v3.Generator/Constants/TypeNames.cs
@@ -0,0 +1,6 @@
+namespace Allure.Xunit.Generator.Constants;
+
+static class TypeNames
+{
+ public const string SelfRegisteredExtensions = "SelfRegisteredExtensions";
+}
diff --git a/src/Allure.Xunit.v3.Generator/Constants/Types.cs b/src/Allure.Xunit.v3.Generator/Constants/Types.cs
new file mode 100644
index 00000000..0ee341f4
--- /dev/null
+++ b/src/Allure.Xunit.v3.Generator/Constants/Types.cs
@@ -0,0 +1,10 @@
+namespace Allure.Xunit.Generator.Constants;
+
+static class Types
+{
+ public const string ITestApplicationBuilder = "Microsoft.Testing.Platform.Builder.ITestApplicationBuilder";
+
+ public const string AllureXunitAttribute = "Allure.Xunit.AllureXunitAttribute";
+
+ public const string AllureIdAttribute = "Allure.Net.Commons.Attributes.AllureIdAttribute";
+}
diff --git a/src/Allure.Xunit.v3/Allure.Xunit.v3.csproj b/src/Allure.Xunit.v3/Allure.Xunit.v3.csproj
new file mode 100644
index 00000000..b284a7a6
--- /dev/null
+++ b/src/Allure.Xunit.v3/Allure.Xunit.v3.csproj
@@ -0,0 +1,54 @@
+
+
+
+ netstandard2.0
+ Allure.Xunit
+ enable
+ Qameta Software Inc
+ Create beautiful reports from your xUnit.net v3 tests.
+ Allure-X-Color.png
+ $(PackageTags) xunit xunit3 xunit.v3
+
+
+
+ <_GeneratorTargetFramework>netstandard2.0
+ <_GeneratorProjectName>Allure.Xunit.v3.Generator
+ <_GeneratorOutputPath>$(
+ [MSBuild]::NormalizePath(
+ '$(ArtifactsPath)',
+ 'bin',
+ '$(_GeneratorProjectName)',
+ '$(Configuration.ToLower())_$(_GeneratorTargetFramework)',
+ '$(_GeneratorProjectName).dll'
+ )
+ )
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Allure.Xunit.v3/AllureRunnerReporter.cs b/src/Allure.Xunit.v3/AllureRunnerReporter.cs
new file mode 100644
index 00000000..69ca825b
--- /dev/null
+++ b/src/Allure.Xunit.v3/AllureRunnerReporter.cs
@@ -0,0 +1,53 @@
+using System.Threading.Tasks;
+using Xunit.Runner.Common;
+using Xunit.Sdk;
+using Allure.Xunit.Internal;
+
+namespace Allure.Xunit;
+
+///
+/// Provides the xUnit.net v3 runner reporter that forwards test lifecycle messages
+/// to Allure when the Allure Microsoft Testing Platform runtime is active.
+///
+public class AllureRunnerReporter : IRunnerReporter
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public AllureRunnerReporter() { }
+
+ ///
+ public bool CanBeEnvironmentallyEnabled => AllureXunitMtpServices.IsAllureAlive;
+
+ ///
+ public string Description => "Allure runner reporter for xUnit.net v3";
+
+ ///
+ public bool ForceNoLogo => false;
+
+ ///
+ public bool IsEnvironmentallyEnabled => this.CanBeEnvironmentallyEnabled;
+
+ ///
+ public string RunnerSwitch => "allure";
+
+ ///
+ public async ValueTask CreateMessageHandler(
+ IRunnerLogger logger,
+ IMessageSink? diagnosticMessageSink
+ )
+ {
+ if (!AllureXunitMtpServices.IsAllureAlive)
+ {
+ return new DefaultRunnerReporterMessageHandler(logger);
+ }
+
+ var allureHandler = new AllureMessageHandler(logger, AllureXunitMtpServices.MessageBus);
+
+ CurrentMessageHandler = allureHandler;
+
+ return allureHandler;
+ }
+
+ internal static AllureMessageHandler? CurrentMessageHandler { get; private set; }
+}
diff --git a/src/Allure.Xunit.v3/AllureXunitAttribute.cs b/src/Allure.Xunit.v3/AllureXunitAttribute.cs
new file mode 100644
index 00000000..a30e37df
--- /dev/null
+++ b/src/Allure.Xunit.v3/AllureXunitAttribute.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using Xunit.Runner.Common;
+using Xunit.v3;
+using System.Reflection;
+using Allure.TestingPlatform.Sdk.Correlation;
+using Allure.Xunit.Internal;
+
+namespace Allure.Xunit;
+
+///
+/// Enables Allure.Xunit.v3 for an xUnit.net v3 test assembly.
+///
+[AttributeUsage(AttributeTargets.Assembly)]
+public class AllureXunitAttribute() :
+ Attribute,
+ IBeforeAfterTestAttribute,
+ IRegisterRunnerReporterAttribute,
+ ITraitAttribute
+{
+ ///
+ public Type RunnerReporterType => typeof(AllureRunnerReporter);
+
+ ///
+ public void After(MethodInfo methodUnderTest, IXunitTest test) { }
+
+ ///
+ public void Before(MethodInfo methodUnderTest, IXunitTest test) =>
+ AllureRunnerReporter.CurrentMessageHandler?.HandleBeforeTest(
+ methodUnderTest,
+ test,
+ test.TestMethodArguments
+ );
+
+ ///
+ public IReadOnlyCollection> GetTraits() =>
+ AllureXunitMtpServices.IsAllureAlive
+ ? [new(TestNodeMetadataCorrelationStrategy.MetadataKey, TestNodeMetadataCorrelationStrategy.CreateCorrelationUid())]
+ : [];
+}
diff --git a/src/Allure.Xunit.v3/AllureXunitBuilderHook.cs b/src/Allure.Xunit.v3/AllureXunitBuilderHook.cs
new file mode 100644
index 00000000..657b96d0
--- /dev/null
+++ b/src/Allure.Xunit.v3/AllureXunitBuilderHook.cs
@@ -0,0 +1,21 @@
+using Microsoft.Testing.Platform.Builder;
+
+#pragma warning disable IDE0060
+
+namespace Allure.Xunit;
+
+///
+/// Provides the Microsoft Testing Platform builder hook used by Allure.Xunit.v3 self-registration.
+///
+public static class AllureXunitBuilderHook
+{
+ ///
+ /// Registers Allure.Xunit.v3 with the Microsoft Testing Platform test application builder.
+ ///
+ /// The test application builder to configure.
+ /// The command-line arguments passed to the test application.
+ public static void AddExtensions(ITestApplicationBuilder builder, string[] args)
+ {
+ builder.AddAllureXunit();
+ }
+}
diff --git a/src/Allure.Xunit.v3/AllureXunitExtensions.cs b/src/Allure.Xunit.v3/AllureXunitExtensions.cs
new file mode 100644
index 00000000..21f72725
--- /dev/null
+++ b/src/Allure.Xunit.v3/AllureXunitExtensions.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Reflection;
+using Allure.TestingPlatform.Registration;
+using Allure.TestingPlatform.Sdk;
+using Allure.Xunit.Internal;
+using Microsoft.Testing.Platform.Builder;
+
+namespace Allure.Xunit;
+
+///
+/// Provides extension methods that register Allure.Xunit.v3 with Microsoft Testing Platform.
+///
+public static class AllureXunitExtensions
+{
+ extension (ITestApplicationBuilder builder)
+ {
+ ///
+ /// Registers Allure.Xunit.v3 with Microsoft Testing Platform and applies
+ /// additional Allure.TestingPlatform configuration.
+ ///
+ ///
+ /// A callback that configures the Allure.TestingPlatform registration.
+ ///
+ public void AddAllureXunit(Action allureRegistration)
+ {
+ var allureRuntimeReferences = builder.AddEmbeddedAllure((ctx) =>
+ {
+ allureRegistration(ctx);
+ if (IsAllureXunitAttributeApplied)
+ {
+ ctx.UseTestNodeMetadataCorrelation();
+ }
+ });
+
+ builder.TestHost.AddTestHostApplicationLifetime((serviceProvider) =>
+ new AllureXunitMtpServices(
+ serviceProvider,
+ allureRuntimeReferences.GetRuntimeReference(serviceProvider)
+ )
+ );
+ }
+
+ ///
+ /// Registers Allure.Xunit.v3 with Microsoft Testing Platform using the default configuration.
+ ///
+ public void AddAllureXunit() => AddAllureXunit(builder, static (_) => { });
+ }
+
+ static bool IsAllureXunitAttributeApplied =>
+ Assembly.GetEntryAssembly().GetCustomAttribute() is not null;
+}
diff --git a/src/Allure.Xunit.v3/CompilerHints/IsExternalInit.cs b/src/Allure.Xunit.v3/CompilerHints/IsExternalInit.cs
new file mode 100644
index 00000000..fc1fbfd0
--- /dev/null
+++ b/src/Allure.Xunit.v3/CompilerHints/IsExternalInit.cs
@@ -0,0 +1,8 @@
+using System.ComponentModel;
+
+#pragma warning disable IDE0130
+
+namespace System.Runtime.CompilerServices;
+
+[EditorBrowsable(EditorBrowsableState.Never)]
+internal static class IsExternalInit { }
diff --git a/src/Allure.Xunit.v3/CompilerHints/NotNullWhen.cs b/src/Allure.Xunit.v3/CompilerHints/NotNullWhen.cs
new file mode 100644
index 00000000..463e4708
--- /dev/null
+++ b/src/Allure.Xunit.v3/CompilerHints/NotNullWhen.cs
@@ -0,0 +1,9 @@
+#pragma warning disable IDE0130
+
+namespace System.Diagnostics.CodeAnalysis;
+
+[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
+internal sealed class NotNullWhenAttribute(bool returnValue) : Attribute
+{
+ public bool ReturnValue { get; } = returnValue;
+}
diff --git a/src/Allure.Xunit.v3/Functions/ExceptionFunctions.cs b/src/Allure.Xunit.v3/Functions/ExceptionFunctions.cs
new file mode 100644
index 00000000..d61e742d
--- /dev/null
+++ b/src/Allure.Xunit.v3/Functions/ExceptionFunctions.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Linq;
+using Allure.Xunit.Internal;
+using Xunit.Sdk;
+
+namespace Allure.Xunit.Functions;
+
+static class ExceptionFunctions
+{
+ public static bool IsConfiguredAssertionFailure(ITestFailed testFailed) =>
+ AllureXunitMtpServices.AllureRuntime is
+ {
+ Configuration.FailExceptions: { Count: >0 } failExceptions
+ }
+ && testFailed.ExceptionTypes.Any(
+ (e) => failExceptions.Contains(e, StringComparer.OrdinalIgnoreCase)
+ );
+}
diff --git a/src/Allure.Xunit.v3/Functions/TestPlanFunctions.cs b/src/Allure.Xunit.v3/Functions/TestPlanFunctions.cs
new file mode 100644
index 00000000..0b9b0ce1
--- /dev/null
+++ b/src/Allure.Xunit.v3/Functions/TestPlanFunctions.cs
@@ -0,0 +1,183 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using Allure.Net.Commons.Attributes;
+using Allure.Net.Commons.Functions;
+using Allure.Net.Commons.TestPlan;
+
+namespace Allure.Xunit.Functions;
+
+///
+/// Provides helpers used to apply Allure test plans to xUnit.net v3 test runs.
+///
+public static class TestPlanFunctions
+{
+ static readonly Lazy testPlanLazy = new(AllureTestPlan.FromEnvironment);
+
+ ///
+ /// Gets the global Allure test plan loaded from the current process environment.
+ ///
+ public static AllureTestPlan TestPlan => testPlanLazy.Value;
+
+ ///
+ /// Returns an array consisting of the original CLI arguments plus filter arguments that,
+ /// when passed to xUnit.net v3, enforces the global test plan for the entry point assembly.
+ ///
+ /// An array of command-line arguments passed to the test application.
+ ///
+ /// A mapping from Allure ID to test method names.
+ ///
+ ///
+ /// A new array that contains followed by
+ /// the xUnit.net pre-execution filter arguments for the current Allure test plan.
+ ///
+ public static string[] AddXunitPreExecutionFilterArguments(
+ string[] originalArguments,
+ ImmutableDictionary> allureIdRegistry
+ ) =>
+ [
+ ..originalArguments,
+ ..GetXunitPreExecutionFilter(allureIdRegistry, TestPlan, Assembly.GetEntryAssembly()),
+ ];
+
+ ///
+ /// Returns a sequence of filter arguments that, when passed to xUnit.net v3,
+ /// enforces the provided test plan.
+ ///
+ ///
+ /// A mapping from Allure ID to test method names.
+ ///
+ ///
+ /// The test plan to enforce. Use to read
+ /// the global test plan, or use to get the cached instance.
+ ///
+ ///
+ /// The test assembly. In the Microsoft Testing Platform flow, this is the entry assembly.
+ ///
+ ///
+ /// A sequence of xunit.v3.mtp-v2 arguments in the form
+ /// --filter-method method1 --filter-method method2 ....
+ ///
+ public static IEnumerable GetXunitPreExecutionFilter(
+ ImmutableDictionary> allureIdRegistry,
+ AllureTestPlan testPlan,
+ Assembly testAssembly
+ )
+ {
+ if (ReferenceEquals(testPlan, AllureTestPlan.DEFAULT_TESTPLAN))
+ {
+ // No test plan provided.
+ yield break;
+ }
+
+ bool emitted = false;
+
+ var testAssemblyName = testAssembly.GetName().Name;
+ foreach (var entry in testPlan.Tests)
+ {
+ if (entry.Selector is not null
+ && TryMatchByFullName(testAssemblyName, entry.Selector, out var fullNameFilter))
+ {
+ emitted = true;
+ yield return "--filter-method";
+ yield return fullNameFilter;
+ }
+
+ if (entry.Id is not null
+ && int.TryParse(entry.Id, out var expectedAllureId)
+ && allureIdRegistry.TryGetValue(expectedAllureId, out var allureIdFilters))
+ {
+ foreach (var allureIdFilter in allureIdFilters)
+ {
+ emitted = true;
+ yield return "--filter-method";
+ yield return allureIdFilter;
+ }
+ }
+ }
+
+ if (!emitted)
+ {
+ // The test plan exists but is empty or no entry matched.
+ // No test should be run.
+ yield return "--filter-method";
+ yield return NON_EXISTING_METHOD_NAME;
+ }
+ }
+
+ ///
+ /// Checks if the test defined by the provided method is selected
+ /// by the global test plan.
+ ///
+ /// A test method to check.
+ ///
+ /// if the method is selected by the current test plan;
+ /// otherwise, .
+ ///
+ public static bool IsSelected(MethodInfo testMethod) =>
+ TestPlan.IsSelected(
+ fullName: IdFunctions.CreateFullName(testMethod),
+ allureId: testMethod.GetCustomAttribute()?.Value
+ );
+
+ static bool TryMatchByFullName(
+ string testAssemblyName,
+ string fullName,
+ [NotNullWhen(true)] out string? filterMethodValue
+ )
+ {
+ if (TrySplitAssemblyName(fullName, out var assemblyName, out var rest)
+ && StringComparer.OrdinalIgnoreCase.Equals(assemblyName, testAssemblyName)
+ && TryCropOutParameters(rest, out var methodFullName))
+ {
+ filterMethodValue = methodFullName;
+ return true;
+ }
+
+ filterMethodValue = default;
+ return false;
+ }
+
+ static bool TrySplitAssemblyName(
+ string fullName,
+ [NotNullWhen(true)] out string assemblyName,
+ [NotNullWhen(true)] out string? rest
+ )
+ {
+ if (fullName.IndexOf(':') is int assemblyNameEnd && assemblyNameEnd != -1)
+ {
+ assemblyName = fullName.Substring(0, assemblyNameEnd);
+ rest = fullName.Substring(assemblyNameEnd + 1);
+ return true;
+ }
+
+ rest = assemblyName = "";
+ return false;
+ }
+
+ static bool TryCropOutParameters(
+ string methodSignature,
+ [NotNullWhen(true)] out string methodFullName
+ )
+ {
+ int cropAt = methodSignature.IndexOf('(');
+ int typeParametersStart = methodSignature.IndexOf('[');
+ if (cropAt == -1 || typeParametersStart != -1 && typeParametersStart < cropAt)
+ {
+ cropAt = typeParametersStart;
+ }
+
+ if (cropAt == -1)
+ {
+ methodFullName = "";
+ return false;
+ }
+
+ methodFullName = methodSignature.Substring(0, cropAt);
+ return true;
+ }
+
+ const string NON_EXISTING_METHOD_NAME = "f78eccb2-459e-482b-87ea-d8d7106959d1";
+}
diff --git a/src/Allure.Xunit.v3/Functions/XunitMessageMapping.cs b/src/Allure.Xunit.v3/Functions/XunitMessageMapping.cs
new file mode 100644
index 00000000..6a6bcd54
--- /dev/null
+++ b/src/Allure.Xunit.v3/Functions/XunitMessageMapping.cs
@@ -0,0 +1,134 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Reflection;
+using Allure.Net.Commons;
+using Allure.TestingPlatform.Sdk.Correlation;
+using Allure.TestingPlatform.Sdk.Messages;
+using Allure.TestingPlatform.Sdk.Properties;
+using Xunit.Runner.Common;
+using Xunit.Sdk;
+
+using AllureTestResult = Allure.Net.Commons.TestResult;
+
+namespace Allure.Xunit.Functions;
+
+static class XunitMessageMapping
+{
+ public static bool TryGetCorrelationUid(
+ IReadOnlyDictionary> xunitTraits,
+ [NotNullWhen(true)] out CorrelationUid? correlationUid
+ )
+ {
+ if (xunitTraits.TryGetValue(TestNodeMetadataCorrelationStrategy.MetadataKey, out var metadataValue)
+ && metadataValue.Count == 1
+ )
+ {
+ correlationUid = new(metadataValue.First());
+ return true;
+ }
+ correlationUid = null;
+ return false;
+ }
+
+ public static bool TryConvertToAllureScopeStartMessage(
+ ITestCaseStarting testCaseStarting,
+ [NotNullWhen(true)] out AllureScopeStartMessage? allureScopeStart
+ )
+ {
+ if (testCaseStarting is { Traits: { } traits, TestCaseUniqueID: { } scopeUid }
+ && TryGetCorrelationUid(traits, out var correlationUid)
+ )
+ {
+ allureScopeStart = new(correlationUid.Value, new(scopeUid));
+ return true;
+ }
+ allureScopeStart = null;
+ return false;
+ }
+
+ public static bool TryConvertToAllureScopeStopMessage(
+ ITestCaseFinished testCaseFinished,
+ MessageMetadataCache metadataCache,
+ [NotNullWhen(true)] out AllureScopeStopMessage? allureScopeStop
+ )
+ {
+ if (testCaseFinished is { TestCaseUniqueID: { } scopeUid }
+ && metadataCache.TryGetTestCaseMetadata(scopeUid) is { Traits: { } traits }
+ && TryGetCorrelationUid(traits, out var correlationUid)
+ )
+ {
+ allureScopeStop = new(correlationUid.Value, new(scopeUid));
+ return true;
+ }
+ allureScopeStop = null;
+ return false;
+ }
+
+ public static bool TryConvertToTestUpdateWithMethod(
+ MethodInfo testMethod,
+ ITest test,
+ object?[]? arguments,
+ [NotNullWhen(true)] out AllureTestUpdateMessage? allureTestUpdate
+ )
+ {
+ if (TryGetCorrelationUid(test.Traits, out var correlationUid))
+ {
+ allureTestUpdate = new AllureTestUpdateMessage(correlationUid.Value, new(test.TestCase.UniqueID))
+ {
+ Properties = [
+ new AllureTestMethodProperty(testMethod) { Arguments = [.. arguments ?? []] },
+ new AllureDefaultSuitesProperty(testMethod.DeclaringType),
+ new AllureLabelsProperty([Label.Thread()]),
+ ]
+ };
+ return true;
+ }
+
+ allureTestUpdate = null;
+ return false;
+ }
+
+ public static bool TryConvertToCancellation(
+ ITest test,
+ [NotNullWhen(true)] out AllureTestUpdateMessage? cancellation
+ )
+ {
+ if (TryGetCorrelationUid(test.Traits, out var correlationUid))
+ {
+ cancellation = new AllureTestUpdateMessage(correlationUid.Value, new(test.TestCase.UniqueID))
+ {
+ Properties = [new AllureCancelProperty()],
+ };
+ return true;
+ }
+
+ cancellation = null;
+ return false;
+ }
+
+ public static bool TryConvertToTestUpdateWithFailedStatus(
+ ITestFailed testFailed,
+ MessageMetadataCache metadataCache,
+ [NotNullWhen(true)] out AllureTestUpdateMessage? allureTestUpdate
+ )
+ {
+ if (ExceptionFunctions.IsConfiguredAssertionFailure(testFailed)
+ && testFailed is { TestCaseUniqueID: { } testCaseUniqueId }
+ && metadataCache.TryGetTestCaseMetadata(testFailed.TestCaseUniqueID) is { Traits: { } traits }
+ && TryGetCorrelationUid(traits, out var correlationUid)
+ )
+ {
+ allureTestUpdate = new AllureTestUpdateMessage(correlationUid.Value, new(testCaseUniqueId))
+ {
+ Properties = [
+ new AllureStatusProperty(Status.failed),
+ ]
+ };
+ return true;
+ }
+
+ allureTestUpdate = null;
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/src/Allure.Xunit.v3/Internal/AllureMessageHandler.cs b/src/Allure.Xunit.v3/Internal/AllureMessageHandler.cs
new file mode 100644
index 00000000..adac6ec8
--- /dev/null
+++ b/src/Allure.Xunit.v3/Internal/AllureMessageHandler.cs
@@ -0,0 +1,115 @@
+using System;
+using System.Reflection;
+using System.Threading.Tasks;
+using Allure.Net.Commons.TestPlan;
+using Allure.TestingPlatform.Functions;
+using Allure.TestingPlatform.Sdk.Messages;
+using Allure.Xunit.Functions;
+using Microsoft.Testing.Platform.Extensions.Messages;
+using Xunit;
+using Xunit.Runner.Common;
+using Xunit.Sdk;
+using IMtpMessageBus = Microsoft.Testing.Platform.Messages.IMessageBus;
+
+namespace Allure.Xunit.Internal;
+
+sealed class AllureMessageHandler(
+ IRunnerLogger logger,
+ IMtpMessageBus mtpMessageBus
+) :
+ DefaultRunnerReporterMessageHandler(logger),
+ IRunnerReporterMessageHandler,
+ IDataProducer
+{
+ public Type[] DataTypesProduced => [
+ typeof(AllureScopeStartMessage),
+ typeof(AllureScopeStopMessage),
+ typeof(AllureTestUpdateMessage),
+ ];
+
+ public string Uid { get; } = "30b8fdc2-68a8-49be-b0f8-b6bed05d07bd";
+
+ public string DisplayName { get; } =
+ "Allure.Xunit.v3 runner message handler";
+
+ public string Description { get; } =
+ "A message handler that translates xUnit runner reporter messages for Allure.";
+
+ public string Version { get; } = TestingPlatformFunctions.GetPackageVersion(typeof(AllureMessageHandler));
+
+ public Task IsEnabledAsync() => Task.FromResult(true);
+
+ protected override void HandleTestCaseStarting(MessageHandlerArgs args)
+ {
+ base.HandleTestCaseStarting(args);
+
+ if (XunitMessageMapping.TryConvertToAllureScopeStartMessage(args.Message, out var allureScopeStart))
+ {
+ this.PublishSync(allureScopeStart);
+ }
+ }
+
+ internal void HandleBeforeTest(MethodInfo testMethod, ITest test, object?[]? arguments)
+ {
+ this.ApplyRuntimeGuard(testMethod, test);
+
+ if (XunitMessageMapping.TryConvertToTestUpdateWithMethod(
+ testMethod,
+ test,
+ arguments,
+ out var allureTestUpdate
+ ))
+ {
+ this.PublishSync(allureTestUpdate);
+ }
+ }
+
+ protected override void HandleTestFailed(MessageHandlerArgs args)
+ {
+ base.HandleTestFailed(args);
+
+ if (XunitMessageMapping.TryConvertToTestUpdateWithFailedStatus(
+ args.Message,
+ this.MetadataCache,
+ out var allureTestUpdate
+ ))
+ {
+ this.PublishSync(allureTestUpdate);
+ }
+ }
+
+ protected override void HandleTestCaseFinished(MessageHandlerArgs args)
+ {
+ base.HandleTestCaseFinished(args);
+
+ if (XunitMessageMapping.TryConvertToAllureScopeStopMessage(
+ args.Message,
+ this.MetadataCache,
+ out var allureScopeStop
+ ))
+ {
+ this.PublishSync(allureScopeStop);
+ }
+ }
+
+ void ApplyRuntimeGuard(MethodInfo testMethod, ITest test)
+ {
+ var isSelected = TestPlanFunctions.IsSelected(testMethod);
+ if (isSelected)
+ {
+ return;
+ }
+
+ if (XunitMessageMapping.TryConvertToCancellation(test, out var cancellation))
+ {
+ this.PublishSync(cancellation);
+ }
+
+ Assert.Skip(AllureTestPlan.SkipReason);
+ }
+
+ void PublishSync(AllureCorrelatedMessage message) =>
+ mtpMessageBus
+ .PublishAsync(this, message)
+ .SpinWait();
+}
diff --git a/src/Allure.Xunit.v3/Internal/AllureXunitMtpServices.cs b/src/Allure.Xunit.v3/Internal/AllureXunitMtpServices.cs
new file mode 100644
index 00000000..20257a5d
--- /dev/null
+++ b/src/Allure.Xunit.v3/Internal/AllureXunitMtpServices.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Allure.TestingPlatform.Sdk;
+using Allure.TestingPlatform.Sdk.Runtime;
+using Microsoft.Testing.Platform.Extensions.TestHost;
+using Microsoft.Testing.Platform.Messages;
+using Microsoft.Testing.Platform.Services;
+
+namespace Allure.Xunit.Internal;
+
+class AllureXunitMtpServices(
+ IServiceProvider serviceProvider,
+ IAllureTestingPlatformRuntimeReference runtimeReference
+) :
+ AllureTestingPlatformExtension(
+ "725e7e6e-74de-4e77-b0bf-eaebfe19914e",
+ "MTP services exposed to Allure.Xunit.v3",
+ "The extension represents a static access point for "
+ + "Allure.TestingPlatform and Microsoft Testing Platform "
+ + "services required by Allure.Xunit.v3",
+ runtimeReference
+ ),
+ ITestHostApplicationLifetime
+{
+ record class ExposedAllureMtpServices(
+ LiveAllureTestingPlatformRuntime AllureRuntime,
+ Lazy MessageBus
+ );
+
+ public Task BeforeRunAsync(CancellationToken cancellationToken)
+ {
+ allureTestingPlatform = new(
+ this.LiveRuntime,
+ new(serviceProvider.GetMessageBus)
+ );
+ return Task.CompletedTask;
+ }
+
+ public Task AfterRunAsync(int exitCode, CancellationToken cancellationToken)
+ {
+ allureTestingPlatform = null;
+ return Task.CompletedTask;
+ }
+
+ static ExposedAllureMtpServices? allureTestingPlatform;
+
+ static ExposedAllureMtpServices Services => allureTestingPlatform
+ ?? throw new InvalidOperationException(
+ "Internal error: Allure.Xunit.v3 is not initialized."
+ );
+
+ public static bool IsAllureAlive => allureTestingPlatform is not null;
+
+ public static IMessageBus MessageBus => Services.MessageBus.Value;
+
+ public static LiveAllureTestingPlatformRuntime AllureRuntime => Services.AllureRuntime;
+
+}
diff --git a/src/Allure.Xunit.v3/Internal/Extensions.cs b/src/Allure.Xunit.v3/Internal/Extensions.cs
new file mode 100644
index 00000000..61058adb
--- /dev/null
+++ b/src/Allure.Xunit.v3/Internal/Extensions.cs
@@ -0,0 +1,37 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Allure.Xunit.Internal;
+
+static class Extensions
+{
+ extension (Task task)
+ {
+ public void SpinWait()
+ {
+ var spin = new SpinWait();
+
+ while (!task.IsCompleted)
+ {
+ spin.SpinOnce();
+ }
+
+ task.GetAwaiter().GetResult();
+ }
+ }
+
+ extension (Task task)
+ {
+ public T SpinWait()
+ {
+ var spin = new SpinWait();
+
+ while (!task.IsCompleted)
+ {
+ spin.SpinOnce();
+ }
+
+ return task.GetAwaiter().GetResult();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Allure.Xunit.v3/README.md b/src/Allure.Xunit.v3/README.md
new file mode 100644
index 00000000..d4ea8a3a
--- /dev/null
+++ b/src/Allure.Xunit.v3/README.md
@@ -0,0 +1,328 @@
+# Allure.Xunit.v3
+
+[](https://www.nuget.org/packages/Allure.Xunit.v3)
+[](https://www.nuget.org/packages/Allure.Xunit.v3)
+
+> An Allure adapter for [xUnit.net v3](https://xunit.net/).
+
+[
](https://allurereport.org "Allure Report")
+
+- Learn more about Allure Report at [https://allurereport.org](https://allurereport.org)
+- 📚 [Documentation](https://allurereport.org/docs/) – discover official documentation for Allure Report
+- ❓ [Questions and Support](https://github.com/orgs/allure-framework/discussions/categories/questions-support) – get help from the team and community
+- 📢 [Official announcements](https://github.com/orgs/allure-framework/discussions/categories/announcements) – stay updated with our latest news and updates
+- 💬 [General Discussion](https://github.com/orgs/allure-framework/discussions/categories/general-discussion) – engage in casual conversations, share insights and ideas with the community
+- 🖥️ [Live Demo](https://demo.allurereport.org/) — explore a live example of Allure Report in action
+
+---
+
+## Overview
+
+`Allure.Xunit.v3` integrates [Allure Report](https://allurereport.org/) with
+[xUnit.net v3](https://xunit.net/) projects that run on
+[Microsoft Testing Platform](https://learn.microsoft.com/en-us/dotnet/core/testing/microsoft-testing-platform-intro).
+
+The package builds on top of `Allure.TestingPlatform` and adds the xUnit.net v3
+adapter layer. It reports xUnit test results, Allure metadata attributes,
+runtime API calls, parameters, steps, fixtures, and attachments to Allure result
+files.
+
+`Allure.Xunit.v3` supports only xUnit.net v3 projects that run through
+Microsoft Testing Platform.
+
+## Requirements
+
+Install these packages in the test project:
+
+- `xunit.v3.mtp-v2`
+- `Allure.Xunit.v3`
+
+Enable the Microsoft Testing Platform runner:
+
+```xml
+
+ true
+
+
+
+
+
+
+```
+
+If `UseMicrosoftTestingPlatformRunner` is not set to `true`, the adapter is not
+registered and the project will not produce Allure results through this package.
+
+## Quick Start
+
+After the packages and MSBuild property are added, run the tests with `dotnet run`:
+
+```bash
+dotnet run --project ./MyTests.csproj
+```
+
+If `dotnet test` is [configured to use Microsoft Testing Platform](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-with-dotnet-test#mtp-mode-of-dotnet-test) for your project, you can run:
+
+```bash
+dotnet test
+```
+
+By default, Allure.Xunit.v3 writes result files to an `allure-results`
+directory inside the Microsoft Testing Platform results directory. Microsoft
+Testing Platform creates that directory as `TestResults` in the build output
+directory unless you pass a different location with `--results-directory`.
+
+A typical default path is:
+
+```
+./bin/Debug/net10.0/TestResults/allure-results
+```
+
+Use attributes from `Allure.Net.Commons.Attributes` to add metadata to tests:
+
+```csharp
+using Allure.Net.Commons.Attributes;
+using Xunit;
+
+[AllureFeature("Checkout")]
+public class CheckoutTests
+{
+ [Fact]
+ [AllureStory("Apply promo code")]
+ [AllureDescription("Checks that a valid promo code changes the order total.")]
+ public void AppliesPromoCode()
+ {
+ // ...
+ }
+}
+```
+
+You can also use `Allure.Net.Commons.AllureApi` to add labels, links,
+parameters, steps, and attachments at runtime.
+
+## Configuration
+
+The default registration uses the same configuration discovery as
+`Allure.TestingPlatform`:
+
+1. explicit configuration passed during custom registration;
+2. the file path from the `ALLURE_CONFIG` environment variable;
+3. `allureConfig.json` from the application base directory;
+4. default configuration values.
+
+An `allureConfig.json` file can be copied to the output directory:
+
+```xml
+
+
+ PreserveNewest
+
+
+```
+
+Example configuration:
+
+```json
+{
+ "directory": "allure-results",
+ "title": "My xUnit.net v3 tests"
+}
+```
+
+The result directory can also be set for a single run:
+
+```bash
+dotnet test -- --allure-results-directory ./artifacts/allure-results
+```
+
+## Test Plans
+
+To support test plans, Allure.Xunit.v3 generates an Allure-aware entry point.
+The entry point adds xUnit.net pre-execution filter arguments for tests
+selected by the current Allure test plan and calls xUnit.net.
+
+If `Allure_GenerateXunitEntryPoint` is set to `false`, the xUnit.net entry point
+will be used. Pre-execution filtering is not generated. In that mode,
+Allure still applies the test plan at runtime, but xUnit.net may construct
+test class instances and run fixtures for tests that are not selected by the plan.
+
+## Advanced Usage
+
+### Custom Allure registration
+
+Disable the default Allure registration when you need to customize
+`Allure.TestingPlatform`, for example to use a custom configuration object,
+results writer, or enablement rule:
+
+```xml
+
+ false
+
+```
+
+Then define your own Microsoft Testing Platform builder hook:
+
+```csharp
+using Allure.Net.Commons.Configuration;
+using Allure.TestingPlatform;
+using Allure.Xunit;
+using Microsoft.Testing.Platform.Builder;
+
+namespace MyTests;
+
+public static class MyAllureBuilderHook
+{
+ public static void AddExtensions(ITestApplicationBuilder builder, string[] args)
+ {
+ builder.AddAllureXunit(allure =>
+ {
+ allure.UseConfiguration(new AllureConfiguration
+ {
+ Directory = "artifacts/allure-results",
+ Title = "My xUnit.net v3 tests",
+ });
+ });
+ }
+}
+```
+
+And register the hook for self-registration:
+
+```xml
+
+
+ My Allure registration
+ MyTests.MyAllureBuilderHook
+
+
+```
+
+Use a stable UUID value for `{UUID}`.
+
+### Custom entry point
+
+Use a custom entry point when the test application needs startup logic or when
+Allure registration must be configured from code that runs before xUnit.net.
+
+If all you need is to provide custom configuration to Allure, delegate to
+the generated `AllureXunitEntryPoint.RunAsync` overload:
+
+```csharp
+using System.Threading.Tasks;
+using Allure.Net.Commons.Configuration;
+using Allure.TestingPlatform;
+using Allure.Xunit;
+
+namespace MyTests;
+
+public static class Program
+{
+ public static async Task Main(string[] args) =>
+ await AllureXunitEntryPoint.RunAsync(allure =>
+ {
+ allure.UseConfiguration(new AllureConfiguration
+ {
+ Directory = "artifacts/allure-results",
+ });
+ }, args);
+}
+```
+
+Then set `StartupObject` to your entry point and disable the default Allure
+registration:
+
+```xml
+
+ false
+ MyTests.Program
+
+```
+
+It's important to set `Allure_XunitEnableSelfRegistration` to `false`. Otherwise,
+Allure will be registered twice, which results in an exception.
+
+For better control over extension registration, call `AllureXunitRunner.RunAsync`
+instead and pass a registration function to it:
+
+```csharp
+using System.Threading.Tasks;
+using Allure.Net.Commons.Configuration;
+using Allure.TestingPlatform;
+using Allure.Xunit;
+
+namespace MyTests;
+
+public static class Program
+{
+ public static async Task Main(string[] args) =>
+ await AllureXunitRunner.RunAsync(
+ (builder, argsWithTestPlanFilters) =>
+ {
+ builder.AddAllureXunit(allure =>
+ {
+ allure.UseConfiguration(new AllureConfiguration
+ {
+ Directory = "artifacts/allure-results",
+ });
+ });
+ // other registrations
+ },
+ args
+ );
+}
+```
+
+Point `StartupObject` to the entry point class. You may also disable Allure entry point
+generation:
+
+```xml
+
+ false
+ MyTests.Program
+
+```
+
+`StartupObject` is required in this setup because xUnit.net still generates its
+own entry point. Omitting it will result in the `Program has more than one entry
+point defined` error or your entry point not being selected.
+
+## MSBuild Properties
+
+| Property | Default | Description |
+| --- | --- | --- |
+| `UseMicrosoftTestingPlatformRunner` | user-defined | Must be set to `true` to run xUnit.net v3 through Microsoft Testing Platform. |
+| `Allure_XunitEnableSelfRegistration` | `true` when MTP is enabled | Registers Allure.Xunit.v3 automatically with Microsoft Testing Platform. |
+| `Allure_GenerateXunitEntryPoint` | `$(GenerateSelfRegisteredExtensions)` when MTP is enabled | Generates the Allure-aware xUnit.net entry point. |
+| `Allure_ApplyXunitAttribute` | `true` when MTP is enabled | Applies `AllureXunitAttribute` to the test assembly automatically. |
+| `Allure_WarnIfMtpDisabled` | `true` | Emits warning `ALLURE001` when the package is referenced without the MTP runner enabled. |
+| `StartupObject` | `Allure.Xunit.AllureXunitEntryPoint` when an entry point is generated and `StartupObject` is empty | Selects the application entry point. |
+
+## Troubleshooting
+
+### No Allure results are generated
+
+Check that:
+
+- the project references both `xunit.v3.mtp-v2` and `Allure.Xunit.v3`;
+- `UseMicrosoftTestingPlatformRunner` is set to `true`;
+- `Allure_XunitEnableSelfRegistration` is not set to `false` unless you provide
+ a custom registration;
+- the `--allure off` command-line option is not used.
+
+### Warning ALLURE001 is shown
+
+`Allure.Xunit.v3` was referenced by a project that does not enable the
+Microsoft Testing Platform runner. Set `UseMicrosoftTestingPlatformRunner` to
+`true`, or set `Allure_WarnIfMtpDisabled` to `false` if the warning is expected.
+
+### A custom entry point does not produce Allure results
+
+Make sure the entry point delegates to `AllureXunitEntryPoint.RunAsync` or
+`AllureXunitRunner.RunAsync`, and make sure Allure is registered either through
+self-registration or by calling `AddAllureXunit`.
+
+### Test plan correctly deselects the tests but class constructors and fixtures are still executed
+
+Make sure `Allure_GenerateXunitEntryPoint` is not set to `false` and that
+`StartupObject`, when set, points to an entry point that delegates to the
+Allure-generated runner helpers.
diff --git a/src/Allure.Xunit.v3/package/buildTransitive/Allure.Xunit.v3.props b/src/Allure.Xunit.v3/package/buildTransitive/Allure.Xunit.v3.props
new file mode 100644
index 00000000..3b8488ba
--- /dev/null
+++ b/src/Allure.Xunit.v3/package/buildTransitive/Allure.Xunit.v3.props
@@ -0,0 +1,13 @@
+
+
+
+ false
+ true
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Allure.Xunit.v3/package/buildTransitive/Allure.Xunit.v3.targets b/src/Allure.Xunit.v3/package/buildTransitive/Allure.Xunit.v3.targets
new file mode 100644
index 00000000..32bc1d53
--- /dev/null
+++ b/src/Allure.Xunit.v3/package/buildTransitive/Allure.Xunit.v3.targets
@@ -0,0 +1,56 @@
+
+
+
+ $(GenerateSelfRegisteredExtensions)
+ true
+ true
+
+
+
+
+ Allure.Xunit.v3
+ Allure.Xunit.AllureXunitBuilderHook
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Allure.Xunit.AllureXunitEntryPoint
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/Allure.Testing/AllureAssertionExtensions.cs b/tests/Allure.Testing/AllureAssertionExtensions.cs
index 7509877f..fc1926e9 100644
--- a/tests/Allure.Testing/AllureAssertionExtensions.cs
+++ b/tests/Allure.Testing/AllureAssertionExtensions.cs
@@ -17,8 +17,8 @@ public static partial class AllureAssertionExtensions
extension (IAssertionSource source)
{
///
- /// Checks if the exact number of test results were written to the output and each result satisfies the corresponding
- /// constraints.
+ /// Checks if the exact number of test results were written to the output and
+ /// there is a perfect match between the test results and the provided constraints.
///
///
/// Pass null or a function returning null for a no-op constraint.
@@ -35,7 +35,7 @@ public MemberAssertionResult HasTestResults(
var assertion = source.Member(
s => s.TestResults,
- tr => new CollectionItemConstraintsAssertion, AllureTestResult>(
+ tr => new CollectionItemConstraintsPerfectMatchAssertion, AllureTestResult>(
tr.Context,
constraints,
"test result"));
@@ -353,8 +353,8 @@ public NarrowCollectionByIndexAssertion, Allure
}
///
- /// Checks if the exact number of containers were written to the output and each container satisfies
- /// the corresponding constraints.
+ /// Checks if the exact number of containers were written to the output and
+ /// there is a perfect match between the containers and the provided constraints.
///
///
/// Pass null or a function returning null for a no-op constraint.
@@ -371,7 +371,7 @@ public MemberAssertionResult HasContainers(
var assertion = source.Member(
s => s.Containers,
- ca => new CollectionItemConstraintsAssertion, AllureContainer>(
+ ca => new CollectionItemConstraintsPerfectMatchAssertion, AllureContainer>(
ca.Context,
constraints,
"container"));
@@ -687,6 +687,147 @@ public NarrowCollectionByIndexAssertion, AllureC
return new(source.Context.Map(ctx => ctx!.Containers), index, "container");
}
+
+ ///
+ /// Checks if the exact number of globals were written to the output and
+ /// there is a perfect match between the globals and the provided constraints.
+ ///
+ ///
+ /// Pass null or a function returning null for a no-op constraint.
+ ///
+ public MemberAssertionResult HasGlobals(
+ Func, IAssertion?>?[] constraints,
+ [CallerArgumentExpression(nameof(constraints))] string? expression = null
+ )
+ {
+ var ctx = source.Context;
+ var expressionBuilder = ctx.ExpressionBuilder;
+ expressionBuilder.Append($".{nameof(HasGlobals)}({expression ?? "..."})");
+ var length = expressionBuilder.Length;
+
+ var assertion = source.Member(
+ s => s.Globals,
+ globs => new CollectionItemConstraintsPerfectMatchAssertion, AllureGlobals>(
+ globs.Context,
+ constraints,
+ "globals"));
+
+ expressionBuilder.Length = length;
+
+ return assertion;
+ }
+
+ ///
+ /// Checks if the output contains exactly one globals that matches the provided criteria.
+ ///
+ public MemberAssertionResult HasOnlyOneGlobals(
+ Func, IAssertion> criteria,
+ [CallerArgumentExpression(nameof(criteria))] string? expression = null
+ )
+ {
+ var ctx = source.Context;
+ var expressionBuilder = ctx.ExpressionBuilder;
+ expressionBuilder.Append($".{nameof(HasOnlyOneGlobals)}({expression ?? "..."})");
+ var length = expressionBuilder.Length;
+
+ var assertion = source.Member(
+ s => s.Globals,
+ globs => new HasOneItemByCriteriaAssertion, AllureGlobals>(
+ globs.Context,
+ criteria,
+ "globals"));
+
+ expressionBuilder.Length = length;
+
+ return assertion;
+ }
+
+ ///
+ /// Checks if the output contains at least one globals that matches the provided criteria.
+ ///
+ public MemberAssertionResult HasGlobals(
+ Func, IAssertion> criteria,
+ [CallerArgumentExpression(nameof(criteria))] string? expression = null
+ )
+ {
+ var ctx = source.Context;
+ var expressionBuilder = ctx.ExpressionBuilder;
+ expressionBuilder.Append($".{nameof(HasGlobals)}({expression ?? "..."})");
+ var length = expressionBuilder.Length;
+
+ var assertion = source.Member(
+ s => s.Globals,
+ globs => new HasItemByCriteriaAssertion, AllureGlobals>(
+ globs.Context,
+ criteria,
+ "globals"));
+
+ expressionBuilder.Length = length;
+
+ return assertion;
+ }
+
+ ///
+ /// Passes if no globals matches the provided criteria.
+ ///
+ public MemberAssertionResult HasNoGlobals(
+ Func, IAssertion> criteria,
+ [CallerArgumentExpression(nameof(criteria))] string? expression = null
+ )
+ {
+ var ctx = source.Context;
+ var expressionBuilder = ctx.ExpressionBuilder;
+ expressionBuilder.Append($".{nameof(HasNoGlobals)}({expression ?? "..."})");
+ var length = expressionBuilder.Length;
+
+ var assertion = source.Member(
+ s => s.Globals,
+ globs => new HasNoItemByCriteriaAssertion, AllureGlobals>(
+ globs.Context,
+ criteria,
+ "globals"));
+
+ expressionBuilder.Length = length;
+
+ return assertion;
+ }
+
+ ///
+ /// Checks if exactly one globals was written to the output and narrows the assertion chain to that result.
+ ///
+ public NarrowCollectionAssertion, AllureGlobals> HasSingleGlobals()
+ {
+ source.Context.ExpressionBuilder.Append($".{nameof(HasSingleGlobals)}()");
+
+ return new(source.Context.Map(ctx => ctx!.Globals), "globals");
+ }
+
+ ///
+ /// Checks if exactly one globals matches the provided criteria and narrows the assertion chain to that result.
+ ///
+ public NarrowCollectionByCriteriaAssertion, AllureGlobals> HasSingleGlobals(
+ Func, IAssertion> criteria,
+ [CallerArgumentExpression(nameof(criteria))] string? expression = null
+ )
+ {
+ source.Context.ExpressionBuilder.Append($".{nameof(HasSingleGlobals)}({expression ?? "..."})");
+
+ return new(source.Context.Map(ctx => ctx!.Globals), criteria, "globals");
+ }
+
+ ///
+ /// Checks if enough globals were written to the output and narrows the assertion chain to the result
+ /// at the specified index.
+ ///
+ public NarrowCollectionByIndexAssertion, AllureGlobals> HasGlobalsAt(
+ int index,
+ [CallerArgumentExpression(nameof(index))] string? expression = null
+ )
+ {
+ source.Context.ExpressionBuilder.Append($".{nameof(HasGlobalsAt)}({expression ?? "..."})");
+
+ return new(source.Context.Map(ctx => ctx!.Globals), index, "globals");
+ }
}
extension (IAssertionSource> source)
diff --git a/tests/Allure.Testing/AllureSampleRunner.cs b/tests/Allure.Testing/AllureSampleRunner.cs
index d86f740f..dcce1a20 100644
--- a/tests/Allure.Testing/AllureSampleRunner.cs
+++ b/tests/Allure.Testing/AllureSampleRunner.cs
@@ -94,32 +94,90 @@ CancellationToken ct
return allureResults;
}
- static async Task> EnsureSampleResults(
- AllureSampleRegistryEntry sample,
- AllureSampleRunInput input,
+ ///
+ /// Reads the Allure result files in the provided directory.
+ ///
+ /// A directory to read the files from.
+ public static async Task ReadAllureResults(string resultsDirectory) =>
+ await ReadAllureResults(new DirectoryInfo(resultsDirectory), CancellationToken.None);
+
+ ///
+ /// Reads the Allure result files in the provided directory.
+ ///
+ /// A directory to read the files from.
+ /// A cancellation token.
+ public static async Task ReadAllureResults(
+ string resultsDirectory,
CancellationToken ct
) =>
- sample.IsPreRunFlow
- ? EnsureExistingAllureResultsDirectory(sample)
- : await ProduceSampleResults(sample, input, ct);
+ await ReadAllureResults(new DirectoryInfo(resultsDirectory), ct);
- static DirectoryInfo EnsureExistingAllureResultsDirectory(AllureSampleRegistryEntry sample)
+ ///
+ /// Reads the Allure result files in the provided directory.
+ ///
+ /// A directory to read the files from.
+ public static async Task ReadAllureResults(DirectoryInfo resultsDirectory) =>
+ await ReadAllureResults(resultsDirectory, CancellationToken.None);
+
+ ///
+ /// Reads the Allure result files in the provided directory.
+ ///
+ /// A directory to read the files from.
+ /// A cancellation token.
+ public static async Task ReadAllureResults(
+ DirectoryInfo resultsDirectory,
+ CancellationToken ct
+ )
{
- var path = sample.DefaultResultsPath;
- var dInfo = new DirectoryInfo(path);
- if (!dInfo.Exists || !dInfo.EnumerateFiles().Any())
+ var resultFiles = resultsDirectory.Exists
+ ? resultsDirectory.GetFiles()
+ : [];
+
+ var testResults = await ReadJsonObjectResults2(resultFiles, "-result.json", ct);
+ var containers = await ReadJsonObjectResults2(resultFiles, "-container.json", ct);
+ var globals = await ReadJsonObjectResults2(resultFiles, "-globals.json", ct);
+
+ return (testResults, containers, globals) switch
{
- throw new FileNotFoundException(
- $"Can't read Allure results of '{sample.RegistryId}.{sample.SampleId}'. "
- + $"Please, make sure the sample's been run and the results are "
- + $"available at '{path}'. Run "
- + "'dotnet msbuild -t:Allure_RunTestSamples' to execute all the samples of "
- + "the solution/project."
- );
- }
- return dInfo;
+ ({IsPassed: true, Value: var trs}, {IsPassed: true, Value: var conts}, { IsPassed: true, Value: var globs }) => new(
+ TestResults: trs,
+ Containers: conts,
+ Attachments: await ReadAttachments(resultFiles, ct),
+ Globals: globs
+ ),
+
+ ({IsPassed: false, Message: var err1}, {IsPassed: false, Message: var err2}, { IsPassed: false, Message: var err3 }) =>
+ throw new AssertionException($"{err1}{Environment.NewLine}{err2}{Environment.NewLine}{err3}"),
+
+ ({IsPassed: false, Message: var err1}, {IsPassed: false, Message: var err2}, _) =>
+ throw new AssertionException($"{err1}{Environment.NewLine}{err2}"),
+
+ ({IsPassed: false, Message: var err1}, _, {IsPassed: false, Message: var err2}) =>
+ throw new AssertionException($"{err1}{Environment.NewLine}{err2}"),
+
+ (_, {IsPassed: false, Message: var err1}, {IsPassed: false, Message: var err2}) =>
+ throw new AssertionException($"{err1}{Environment.NewLine}{err2}"),
+
+ ({IsPassed: false, Message: var error}, _, _) =>
+ throw new AssertionException(error),
+
+ (_, {IsPassed: false, Message: var error}, _) =>
+ throw new AssertionException(error),
+
+ (_, _, {IsPassed: false, Message: var error}) =>
+ throw new AssertionException(error),
+ };
}
+ static async Task> EnsureSampleResults(
+ AllureSampleRegistryEntry sample,
+ AllureSampleRunInput input,
+ CancellationToken ct
+ ) =>
+ sample.IsPreRunFlow && input.IsPreRunCompatible
+ ? new DirectoryInfo(sample.DefaultResultsPath)
+ : await ProduceSampleResults(sample, input, ct);
+
static async Task> ProduceSampleResults(
AllureSampleRegistryEntry sample,
AllureSampleRunInput input,
@@ -158,7 +216,9 @@ CancellationToken ct
LogStdStreams(stdout, stderr);
- return resultsDirGuard.Transfer();
+ return resultsDirGuard.Own
+ ? resultsDirGuard.Transfer()
+ : resultsDirGuard.Value;
}
static void LogProcessStart(ProcessStartInfo psi, AllureSampleRunInput input)
@@ -166,7 +226,7 @@ static void LogProcessStart(ProcessStartInfo psi, AllureSampleRunInput input)
Console.WriteLine(
"Running {0} {1}",
psi.FileName,
- string.Join(" ", psi.Arguments.Select(a => $"'{a}'"))
+ string.Join(" ", psi.ArgumentList.Select(a => $"'{a}'"))
);
Console.WriteLine(" Working directory: {0}", psi.WorkingDirectory);
@@ -370,35 +430,6 @@ static Task CollectProcessStream(StreamReader reader, CancellationToken
TaskCreationOptions.LongRunning
);
- static async Task ReadAllureResults(
- DirectoryInfo resultsDirectory,
- CancellationToken ct
- )
- {
- var resultFiles = resultsDirectory.GetFiles();
-
- var testResults = await ReadJsonObjectResults2(resultFiles, "-result.json", ct);
- var containers = await ReadJsonObjectResults2(resultFiles, "-container.json", ct);
-
- return (testResults, containers) switch
- {
- ({IsPassed: true, Value: var trs}, {IsPassed: true, Value: var conts}) => new(
- TestResults: trs,
- Containers: conts,
- Attachments: await ReadAttachments(resultFiles, ct)
- ),
-
- ({IsPassed: false, Message: var err1}, {IsPassed: false, Message: var err2}) =>
- throw new AssertionException($"{err1}{Environment.NewLine}{err2}"),
-
- ({IsPassed: false, Message: var error}, _) =>
- throw new AssertionException(error),
-
- (_, {IsPassed: false, Message: var error}) =>
- throw new AssertionException(error),
- };
- }
-
static async Task>> ReadJsonObjectResults2(
IEnumerable allOutputFiles,
string suffix,
diff --git a/tests/Allure.Testing/Assertions/CollectionItemConstraintsAssertion.cs b/tests/Allure.Testing/Assertions/CollectionItemConstraintsAssertion.cs
index b61fc2f2..790f8592 100644
--- a/tests/Allure.Testing/Assertions/CollectionItemConstraintsAssertion.cs
+++ b/tests/Allure.Testing/Assertions/CollectionItemConstraintsAssertion.cs
@@ -44,8 +44,8 @@ await this.ApplyItemConstraints(items),
protected override string GetExpectation() =>
this.lastExpectation is var (index, expectation)
? expectation is not null
- ? $"to have {this.itemConstraints.Length} {this.itemDescriptionPlural} and {this.itemDescription} at {index} {expectation}"
- : $"to have {this.itemConstraints.Length} {this.itemDescriptionPlural} and {this.itemDescription} at {index} satisfying the corresponding constraints"
+ ? $"to have {this.itemConstraints.Length} {this.itemDescriptionPlural} and {this.itemDescription} #{index + 1} {expectation}"
+ : $"to have {this.itemConstraints.Length} {this.itemDescriptionPlural} and {this.itemDescription} #{index + 1} satisfying the corresponding constraints"
: $"to have {this.itemConstraints.Length} {this.itemDescriptionPlural} each satisfying the corresponding constraints";
async Task ApplyItemConstraints(IEnumerable items)
@@ -80,7 +80,7 @@ async Task ApplyItemConstraints(IEnumerable items)
this.lastExpectation = (i, expected);
return AssertionResult.Failed(
- $"{this.itemDescription} at {i} {actual}"
+ $"{this.itemDescription} #{i + 1} {actual}"
);
}
}
diff --git a/tests/Allure.Testing/Assertions/CollectionItemConstraintsPerfectMatchAssertion.cs b/tests/Allure.Testing/Assertions/CollectionItemConstraintsPerfectMatchAssertion.cs
new file mode 100644
index 00000000..366646bc
--- /dev/null
+++ b/tests/Allure.Testing/Assertions/CollectionItemConstraintsPerfectMatchAssertion.cs
@@ -0,0 +1,151 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading.Tasks;
+using Allure.Testing.Internal;
+using TUnit.Assertions.Core;
+
+namespace Allure.Testing.Assertions;
+
+public class CollectionItemConstraintsPerfectMatchAssertion(
+ AssertionContext context,
+ Func, IAssertion?>?[] itemConstraints,
+ string itemDescription
+) :
+ Assertion(context)
+
+ where TCollection : IEnumerable
+{
+ readonly Func, IAssertion?>?[] itemConstraints = itemConstraints;
+ readonly string itemDescription = itemDescription;
+ readonly string itemDescriptionPlural =
+ itemConstraints.Length == 1
+ ? itemDescription
+ : $"{itemDescription}s";
+
+ ImmutableArray? expectations = null;
+
+ int ExpectedCount => this.itemConstraints.Length;
+
+ protected override async Task CheckAsync(
+ EvaluationMetadata metadata
+ ) =>
+ metadata switch
+ {
+ { Exception.Message: var message } =>
+ await Task.FromResult(AssertionResult.Failed(message)),
+
+ { Value: { } items } =>
+ await this.ApplyItemConstraints(items),
+
+ _ => await Task.FromResult(AssertionResult.Failed("the collection was null")),
+ };
+
+ protected override string GetExpectation() =>
+ this.expectations is { } initiazedExpectations
+ ? $"to have {this.itemConstraints.Length} {this.itemDescriptionPlural} "
+ + $"perfectly matching the following constraints:"
+ + string.Join(
+ "",
+ initiazedExpectations.Select((e, ei) =>
+ $"{Environment.NewLine} {ei + 1}. {e ?? $"a constraint #{ei + 1}"}"
+ )
+ )
+ : $"to have {this.itemConstraints.Length} {this.itemDescriptionPlural} "
+ + "perfectly matching the provided constraints";
+
+ async Task ApplyItemConstraints(IEnumerable items)
+ {
+ var list = items.ToImmutableArray();
+ var actualCount = list.Length;
+
+ if (actualCount != this.ExpectedCount)
+ {
+ return AssertionResult.Failed(
+ $"the collection had {actualCount} {this.itemDescriptionPlural}"
+ );
+ }
+
+ AssertionResult[,] matchMatrix = new AssertionResult[this.ExpectedCount, actualCount];
+
+ for (int i = 0; i < actualCount; i++)
+ {
+ var item = list[i];
+ for (int j = 0; j < this.ExpectedCount; j++)
+ {
+ var constraint = this.itemConstraints[j];
+
+ matchMatrix[i, j] = constraint is not null
+ ? await AssertionFunctions.ExecuteInlineAssertionAsync(
+ item,
+ $"{this.itemDescription}s[{i}]",
+ constraint
+ )
+ : AssertionResult.Passed;
+ }
+ }
+
+ if (TryFindUnmatchedItems(matchMatrix) is { Length: >0 } mismatches)
+ {
+ this.expectations = mismatches[0].failures
+ .Select(e => e.expectation)
+ .ToImmutableArray();
+
+ return AssertionResult.Failed(
+ $"Couldn't find a perfect match:"
+ + string.Join(
+ "",
+ mismatches.Select((m) =>
+ $"{Environment.NewLine} - {this.itemDescription} #{m.index + 1} didn't match"
+ + string.Join(
+ "",
+ m.failures.Select((f, ci) =>
+ $"{Environment.NewLine} - constraint {ci + 1} because {f.actual}"
+ )
+ )
+ )
+ )
+ );
+ }
+
+ return AssertionResult.Passed;
+ }
+
+ static ImmutableArray<(int index, ImmutableArray<(string? expectation, string? actual)> failures)> TryFindUnmatchedItems(AssertionResult[,] matches)
+ {
+ var itemCount = matches.GetLength(0);
+ var constraintCount = matches.GetLength(1);
+
+ var itemByConstraint = Enumerable.Repeat(-1, constraintCount).ToArray();
+
+ return [.. Enumerable.Range(0, itemCount)
+ .Where((i) => !TryMatchItem(i, new bool[constraintCount]))
+ .Select((i) => (i, Enumerable.Range(0, constraintCount)
+ .Select((j) => NarrowingFunctions.ExtractExpectedAndActual(matches[i, j].Message, 2))
+ .ToImmutableArray()))];
+
+ bool TryMatchItem(int item, bool[] seenConstraints)
+ {
+ for (var constraint = 0; constraint < constraintCount; constraint++)
+ {
+ if (!matches[item, constraint].IsPassed || seenConstraints[constraint])
+ {
+ continue;
+ }
+
+ seenConstraints[constraint] = true;
+
+ var previousItem = itemByConstraint[constraint];
+
+ if (previousItem == -1 || TryMatchItem(previousItem, seenConstraints))
+ {
+ itemByConstraint[constraint] = item;
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/tests/Allure.Testing/Assertions/JsonPropertyComparerAssertion.cs b/tests/Allure.Testing/Assertions/JsonPropertyComparerAssertion.cs
index 0bf90549..9f739b13 100644
--- a/tests/Allure.Testing/Assertions/JsonPropertyComparerAssertion.cs
+++ b/tests/Allure.Testing/Assertions/JsonPropertyComparerAssertion.cs
@@ -33,7 +33,7 @@ await Task.FromResult(AssertionResult.Failed(message)),
comparer.Equals(value, expectedValue)
? await Task.FromResult(AssertionResult.Passed)
: await Task.FromResult(
- AssertionResult.Failed($"received {FormatFunctions.FormatAsStringLiteral(value)}")),
+ AssertionResult.Failed($"received {propertyName} {FormatFunctions.FormatAsStringLiteral(value)}")),
{ Message: var message } =>
await Task.FromResult(AssertionResult.Failed(message)),
diff --git a/tests/Allure.Testing/Assertions/Model/AllureAttachment.cs b/tests/Allure.Testing/Assertions/Model/AllureAttachment.cs
index f49e4f01..9a4b3d31 100644
--- a/tests/Allure.Testing/Assertions/Model/AllureAttachment.cs
+++ b/tests/Allure.Testing/Assertions/Model/AllureAttachment.cs
@@ -1,13 +1,9 @@
using System.Text.Json;
-using Allure.Testing.Assertions.Model.Properties;
namespace Allure.Testing.Assertions.Model;
public readonly record struct AllureAttachment(JsonElement Json) :
- IAllureModelObject,
- IAllureNameProperty,
- IAllureAttachmentSourceProperty,
- IAllureLinkTypeProperty
+ IAllureAttachment
{
public static string? Validate(JsonElement json) => default;
diff --git a/tests/Allure.Testing/Assertions/Model/AllureGlobalAttachment.cs b/tests/Allure.Testing/Assertions/Model/AllureGlobalAttachment.cs
new file mode 100644
index 00000000..ca2679e0
--- /dev/null
+++ b/tests/Allure.Testing/Assertions/Model/AllureGlobalAttachment.cs
@@ -0,0 +1,13 @@
+using System.Text.Json;
+using Allure.Testing.Assertions.Model.Properties;
+
+namespace Allure.Testing.Assertions.Model;
+
+public readonly record struct AllureGlobalAttachment(JsonElement Json) :
+ IAllureAttachment,
+ IAllureTimestampProperty
+{
+ public static string? Validate(JsonElement json) => default;
+
+ public static AllureGlobalAttachment Constructor(JsonElement json) => new(json);
+}
diff --git a/tests/Allure.Testing/Assertions/Model/AllureGlobalError.cs b/tests/Allure.Testing/Assertions/Model/AllureGlobalError.cs
new file mode 100644
index 00000000..90f5fe52
--- /dev/null
+++ b/tests/Allure.Testing/Assertions/Model/AllureGlobalError.cs
@@ -0,0 +1,13 @@
+using System.Text.Json;
+using Allure.Testing.Assertions.Model.Properties;
+
+namespace Allure.Testing.Assertions.Model;
+
+public readonly record struct AllureGlobalError(JsonElement Json) :
+ IAllureStatusDetails,
+ IAllureTimestampProperty
+{
+ public static string? Validate(JsonElement json) => default;
+
+ public static AllureGlobalError Constructor(JsonElement json) => new(json);
+}
diff --git a/tests/Allure.Testing/Assertions/Model/AllureGlobals.cs b/tests/Allure.Testing/Assertions/Model/AllureGlobals.cs
new file mode 100644
index 00000000..cda644d4
--- /dev/null
+++ b/tests/Allure.Testing/Assertions/Model/AllureGlobals.cs
@@ -0,0 +1,23 @@
+using System.Text.Json;
+using Allure.Testing.Assertions.Model.Properties;
+
+namespace Allure.Testing.Assertions.Model;
+
+public readonly record struct AllureGlobals(JsonElement Json) :
+ IAllureModelObject,
+ IAllureGlobalAttachmentsProperty,
+ IAllureGlobalErrorsProperty
+{
+ public static string? Validate(JsonElement json) => json switch
+ {
+ { ValueKind: JsonValueKind.Null } =>
+ "the globals element was null",
+
+ { ValueKind: not JsonValueKind.Object } =>
+ "the globals element was not a JSON object",
+
+ _ => null,
+ };
+
+ public static AllureGlobals Constructor(JsonElement json) => new(json);
+}
diff --git a/tests/Allure.Testing/Assertions/Model/AllureResults.cs b/tests/Allure.Testing/Assertions/Model/AllureResults.cs
index ad57277e..c6a7b239 100644
--- a/tests/Allure.Testing/Assertions/Model/AllureResults.cs
+++ b/tests/Allure.Testing/Assertions/Model/AllureResults.cs
@@ -6,5 +6,6 @@ namespace Allure.Testing.Assertions.Model;
public record class AllureResults(
ImmutableArray TestResults,
ImmutableArray Containers,
- ImmutableDictionary> Attachments
+ ImmutableDictionary> Attachments,
+ ImmutableArray Globals
);
diff --git a/tests/Allure.Testing/Assertions/Model/AllureStatusDetails.cs b/tests/Allure.Testing/Assertions/Model/AllureStatusDetails.cs
index 9b57316b..f51cc482 100644
--- a/tests/Allure.Testing/Assertions/Model/AllureStatusDetails.cs
+++ b/tests/Allure.Testing/Assertions/Model/AllureStatusDetails.cs
@@ -1,15 +1,9 @@
using System.Text.Json;
-using Allure.Testing.Assertions.Model.Properties;
namespace Allure.Testing.Assertions.Model;
public readonly record struct AllureStatusDetails(JsonElement Json) :
- IAllureModelObject,
- IAllureFlakyProperty,
- IAllureKnownProperty,
- IAllureMessageProperty,
- IAllureMutedProperty,
- IAllureTraceProperty
+ IAllureStatusDetails
{
public static string? Validate(JsonElement json) => default;
diff --git a/tests/Allure.Testing/Assertions/Model/AllureTestResult.cs b/tests/Allure.Testing/Assertions/Model/AllureTestResult.cs
index 19f16ff0..2555d630 100644
--- a/tests/Allure.Testing/Assertions/Model/AllureTestResult.cs
+++ b/tests/Allure.Testing/Assertions/Model/AllureTestResult.cs
@@ -1,4 +1,3 @@
-using System;
using System.Text.Json;
using Allure.Testing.Assertions.Model.Properties;
diff --git a/tests/Allure.Testing/Assertions/Model/IAllureAttachment.cs b/tests/Allure.Testing/Assertions/Model/IAllureAttachment.cs
new file mode 100644
index 00000000..9bafb235
--- /dev/null
+++ b/tests/Allure.Testing/Assertions/Model/IAllureAttachment.cs
@@ -0,0 +1,11 @@
+using Allure.Testing.Assertions.Model.Properties;
+
+namespace Allure.Testing.Assertions.Model;
+
+public interface IAllureAttachment :
+ IAllureModelObject,
+ IAllureNameProperty,
+ IAllureAttachmentSourceProperty,
+ IAllureLinkTypeProperty
+
+ where TSelf : IAllureAttachment, IAllureModelObject;
diff --git a/tests/Allure.Testing/Assertions/Model/IAllureStatusDetails.cs b/tests/Allure.Testing/Assertions/Model/IAllureStatusDetails.cs
new file mode 100644
index 00000000..4fc21310
--- /dev/null
+++ b/tests/Allure.Testing/Assertions/Model/IAllureStatusDetails.cs
@@ -0,0 +1,13 @@
+using Allure.Testing.Assertions.Model.Properties;
+
+namespace Allure.Testing.Assertions.Model;
+
+public interface IAllureStatusDetails :
+ IAllureModelObject,
+ IAllureFlakyProperty,
+ IAllureKnownProperty,
+ IAllureMessageProperty,
+ IAllureMutedProperty,
+ IAllureTraceProperty
+
+ where TSelf : IAllureStatusDetails, IAllureModelObject;
diff --git a/tests/Allure.Testing/Assertions/Model/Properties/IAllureAftersProperty.cs b/tests/Allure.Testing/Assertions/Model/Properties/IAllureAftersProperty.cs
index 303ff5ea..d25a7340 100644
--- a/tests/Allure.Testing/Assertions/Model/Properties/IAllureAftersProperty.cs
+++ b/tests/Allure.Testing/Assertions/Model/Properties/IAllureAftersProperty.cs
@@ -2,6 +2,4 @@ namespace Allure.Testing.Assertions.Model.Properties;
[GenerateAllureAssertions(ItemMethodName = "AfterFixture")]
public interface IAllureAftersProperty : IAllureObjectArrayProperty
- where TSelf : IAllureModelObject, IAllureObjectArrayProperty
-{
-}
+ where TSelf : IAllureModelObject, IAllureObjectArrayProperty;
diff --git a/tests/Allure.Testing/Assertions/Model/Properties/IAllureAttachmentSourceProperty.cs b/tests/Allure.Testing/Assertions/Model/Properties/IAllureAttachmentSourceProperty.cs
index acdd59b5..30a54d1c 100644
--- a/tests/Allure.Testing/Assertions/Model/Properties/IAllureAttachmentSourceProperty.cs
+++ b/tests/Allure.Testing/Assertions/Model/Properties/IAllureAttachmentSourceProperty.cs
@@ -2,6 +2,4 @@ namespace Allure.Testing.Assertions.Model.Properties;
[GenerateAllureAssertions(PropertyName = "Source")]
public interface IAllureAttachmentSourceProperty : IAllureStringProperty
- where TSelf : IAllureModelObject, IAllureAttachmentSourceProperty
-{
-}
+ where TSelf : IAllureModelObject, IAllureAttachmentSourceProperty;
diff --git a/tests/Allure.Testing/Assertions/Model/Properties/IAllureAttachmentsProperty.cs b/tests/Allure.Testing/Assertions/Model/Properties/IAllureAttachmentsProperty.cs
index b015677c..fca6165a 100644
--- a/tests/Allure.Testing/Assertions/Model/Properties/IAllureAttachmentsProperty.cs
+++ b/tests/Allure.Testing/Assertions/Model/Properties/IAllureAttachmentsProperty.cs
@@ -2,6 +2,4 @@ namespace Allure.Testing.Assertions.Model.Properties;
[GenerateAllureAssertions]
public interface IAllureAttachmentsProperty : IAllureObjectArrayProperty
- where TSelf : IAllureModelObject, IAllureAttachmentsProperty
-{
-}
+ where TSelf : IAllureModelObject, IAllureAttachmentsProperty;
diff --git a/tests/Allure.Testing/Assertions/Model/Properties/IAllureBeforesProperty.cs b/tests/Allure.Testing/Assertions/Model/Properties/IAllureBeforesProperty.cs
index 6cc37fb4..603756a1 100644
--- a/tests/Allure.Testing/Assertions/Model/Properties/IAllureBeforesProperty.cs
+++ b/tests/Allure.Testing/Assertions/Model/Properties/IAllureBeforesProperty.cs
@@ -2,7 +2,4 @@ namespace Allure.Testing.Assertions.Model.Properties;
[GenerateAllureAssertions(ItemMethodName = "BeforeFixture")]
public interface IAllureBeforesProperty : IAllureObjectArrayProperty
- where TSelf : IAllureModelObject, IAllureObjectArrayProperty
-{
-
-}
+ where TSelf : IAllureModelObject, IAllureObjectArrayProperty;
diff --git a/tests/Allure.Testing/Assertions/Model/Properties/IAllureChildrenProperty.cs b/tests/Allure.Testing/Assertions/Model/Properties/IAllureChildrenProperty.cs
index 1b1c1f16..8d67fa1b 100644
--- a/tests/Allure.Testing/Assertions/Model/Properties/IAllureChildrenProperty.cs
+++ b/tests/Allure.Testing/Assertions/Model/Properties/IAllureChildrenProperty.cs
@@ -4,7 +4,4 @@ namespace Allure.Testing.Assertions.Model.Properties;
[GenerateAllureAssertions(ItemMethodName = "Child")]
public interface IAllureChildrenProperty : IAllureArrayProperty
- where TSelf : IAllureModelObject, IAllureChildrenProperty
-{
-
-}
+ where TSelf : IAllureModelObject, IAllureChildrenProperty;
diff --git a/tests/Allure.Testing/Assertions/Model/Properties/IAllureDescriptionHtmlProperty.cs b/tests/Allure.Testing/Assertions/Model/Properties/IAllureDescriptionHtmlProperty.cs
index b1e8a450..f0228def 100644
--- a/tests/Allure.Testing/Assertions/Model/Properties/IAllureDescriptionHtmlProperty.cs
+++ b/tests/Allure.Testing/Assertions/Model/Properties/IAllureDescriptionHtmlProperty.cs
@@ -2,6 +2,4 @@ namespace Allure.Testing.Assertions.Model.Properties;
[GenerateAllureAssertions]
public interface IAllureDescriptionHtmlProperty : IAllureStringProperty
- where TSelf : IAllureModelObject, IAllureDescriptionHtmlProperty
-{
-}
+ where TSelf : IAllureModelObject, IAllureDescriptionHtmlProperty;
diff --git a/tests/Allure.Testing/Assertions/Model/Properties/IAllureDescriptionProperty.cs b/tests/Allure.Testing/Assertions/Model/Properties/IAllureDescriptionProperty.cs
index dfa79199..a1ea0430 100644
--- a/tests/Allure.Testing/Assertions/Model/Properties/IAllureDescriptionProperty.cs
+++ b/tests/Allure.Testing/Assertions/Model/Properties/IAllureDescriptionProperty.cs
@@ -2,6 +2,4 @@ namespace Allure.Testing.Assertions.Model.Properties;
[GenerateAllureAssertions]
public interface IAllureDescriptionProperty : IAllureStringProperty
- where TSelf : IAllureModelObject, IAllureDescriptionProperty
-{
-}
+ where TSelf : IAllureModelObject, IAllureDescriptionProperty;
diff --git a/tests/Allure.Testing/Assertions/Model/Properties/IAllureFlakyProperty.cs b/tests/Allure.Testing/Assertions/Model/Properties/IAllureFlakyProperty.cs
index bdabd49c..6e47fb50 100644
--- a/tests/Allure.Testing/Assertions/Model/Properties/IAllureFlakyProperty.cs
+++ b/tests/Allure.Testing/Assertions/Model/Properties/IAllureFlakyProperty.cs
@@ -2,6 +2,4 @@ namespace Allure.Testing.Assertions.Model.Properties;
[GenerateAllureAssertions]
public interface IAllureFlakyProperty : IAllureBoolProperty
- where TSelf : IAllureModelObject, IAllureFlakyProperty
-{
-}
+ where TSelf : IAllureModelObject, IAllureFlakyProperty;
diff --git a/tests/Allure.Testing/Assertions/Model/Properties/IAllureFullNameProperty.cs b/tests/Allure.Testing/Assertions/Model/Properties/IAllureFullNameProperty.cs
index d53915a1..da6a0733 100644
--- a/tests/Allure.Testing/Assertions/Model/Properties/IAllureFullNameProperty.cs
+++ b/tests/Allure.Testing/Assertions/Model/Properties/IAllureFullNameProperty.cs
@@ -2,6 +2,4 @@ namespace Allure.Testing.Assertions.Model.Properties;
[GenerateAllureAssertions]
public interface IAllureFullNameProperty : IAllureStringProperty
- where TSelf : IAllureModelObject, IAllureFullNameProperty
-{
-}
+ where TSelf : IAllureModelObject, IAllureFullNameProperty;
diff --git a/tests/Allure.Testing/Assertions/Model/Properties/IAllureGlobalAttachmentsProperty.cs b/tests/Allure.Testing/Assertions/Model/Properties/IAllureGlobalAttachmentsProperty.cs
new file mode 100644
index 00000000..41394254
--- /dev/null
+++ b/tests/Allure.Testing/Assertions/Model/Properties/IAllureGlobalAttachmentsProperty.cs
@@ -0,0 +1,8 @@
+namespace Allure.Testing.Assertions.Model.Properties;
+
+[GenerateAllureAssertions(
+ PropertyName = "Attachments",
+ MethodName = "GlobalAttachments",
+ ItemMethodName = "GlobalAttachment")]
+public interface IAllureGlobalAttachmentsProperty : IAllureObjectArrayProperty
+ where TSelf : IAllureModelObject, IAllureGlobalAttachmentsProperty;
diff --git a/tests/Allure.Testing/Assertions/Model/Properties/IAllureGlobalErrorsProperty.cs b/tests/Allure.Testing/Assertions/Model/Properties/IAllureGlobalErrorsProperty.cs
new file mode 100644
index 00000000..81a7f220
--- /dev/null
+++ b/tests/Allure.Testing/Assertions/Model/Properties/IAllureGlobalErrorsProperty.cs
@@ -0,0 +1,5 @@
+namespace Allure.Testing.Assertions.Model.Properties;
+
+[GenerateAllureAssertions(PropertyName = "Errors")]
+public interface IAllureGlobalErrorsProperty : IAllureObjectArrayProperty
+ where TSelf : IAllureModelObject, IAllureGlobalErrorsProperty;
diff --git a/tests/Allure.Testing/Assertions/Model/Properties/IAllureHistoryIdProperty.cs b/tests/Allure.Testing/Assertions/Model/Properties/IAllureHistoryIdProperty.cs
index 168abe4f..fa86bade 100644
--- a/tests/Allure.Testing/Assertions/Model/Properties/IAllureHistoryIdProperty.cs
+++ b/tests/Allure.Testing/Assertions/Model/Properties/IAllureHistoryIdProperty.cs
@@ -2,6 +2,4 @@ namespace Allure.Testing.Assertions.Model.Properties;
[GenerateAllureAssertions]
public interface IAllureHistoryIdProperty : IAllureStringProperty
- where TSelf : IAllureModelObject, IAllureHistoryIdProperty
-{
-}
+ where TSelf : IAllureModelObject, IAllureHistoryIdProperty;
diff --git a/tests/Allure.Testing/Assertions/Model/Properties/IAllureKnownProperty.cs b/tests/Allure.Testing/Assertions/Model/Properties/IAllureKnownProperty.cs
index 650a910a..0d7d5bd8 100644
--- a/tests/Allure.Testing/Assertions/Model/Properties/IAllureKnownProperty.cs
+++ b/tests/Allure.Testing/Assertions/Model/Properties/IAllureKnownProperty.cs
@@ -2,6 +2,4 @@ namespace Allure.Testing.Assertions.Model.Properties;
[GenerateAllureAssertions]
public interface IAllureKnownProperty : IAllureBoolProperty
- where TSelf : IAllureModelObject, IAllureKnownProperty
-{
-}
+ where TSelf : IAllureModelObject, IAllureKnownProperty;
diff --git a/tests/Allure.Testing/Assertions/Model/Properties/IAllureLabelsProperty.cs b/tests/Allure.Testing/Assertions/Model/Properties/IAllureLabelsProperty.cs
index 78463867..024ce01c 100644
--- a/tests/Allure.Testing/Assertions/Model/Properties/IAllureLabelsProperty.cs
+++ b/tests/Allure.Testing/Assertions/Model/Properties/IAllureLabelsProperty.cs
@@ -1,9 +1,5 @@
-using System.Collections.Immutable;
-
namespace Allure.Testing.Assertions.Model.Properties;
[GenerateAllureAssertions]
public interface IAllureLabelsProperty : IAllureObjectArrayProperty
- where TSelf : IAllureModelObject, IAllureLabelsProperty
-{
-}
+ where TSelf : IAllureModelObject, IAllureLabelsProperty;
diff --git a/tests/Allure.Testing/Assertions/Model/Properties/IAllureLinkTypeProperty.cs b/tests/Allure.Testing/Assertions/Model/Properties/IAllureLinkTypeProperty.cs
index 27849455..62cf77fc 100644
--- a/tests/Allure.Testing/Assertions/Model/Properties/IAllureLinkTypeProperty.cs
+++ b/tests/Allure.Testing/Assertions/Model/Properties/IAllureLinkTypeProperty.cs
@@ -2,6 +2,4 @@ namespace Allure.Testing.Assertions.Model.Properties;
[GenerateAllureAssertions(PropertyName = "Type")]
public interface IAllureLinkTypeProperty : IAllureStringProperty
- where TSelf : IAllureModelObject, IAllureLinkTypeProperty
-{
-}
+ where TSelf : IAllureModelObject, IAllureLinkTypeProperty;
diff --git a/tests/Allure.Testing/Assertions/Model/Properties/IAllureLinkUrlProperty.cs b/tests/Allure.Testing/Assertions/Model/Properties/IAllureLinkUrlProperty.cs
index 6263990f..fae06225 100644
--- a/tests/Allure.Testing/Assertions/Model/Properties/IAllureLinkUrlProperty.cs
+++ b/tests/Allure.Testing/Assertions/Model/Properties/IAllureLinkUrlProperty.cs
@@ -2,6 +2,4 @@ namespace Allure.Testing.Assertions.Model.Properties;
[GenerateAllureAssertions(PropertyName = "Url")]
public interface IAllureLinkUrlProperty : IAllureStringProperty
- where TSelf : IAllureModelObject, IAllureLinkUrlProperty
-{
-}
+ where TSelf : IAllureModelObject, IAllureLinkUrlProperty;
diff --git a/tests/Allure.Testing/Assertions/Model/Properties/IAllureLinksProperty.cs b/tests/Allure.Testing/Assertions/Model/Properties/IAllureLinksProperty.cs
index 277a5116..ce7cd1a4 100644
--- a/tests/Allure.Testing/Assertions/Model/Properties/IAllureLinksProperty.cs
+++ b/tests/Allure.Testing/Assertions/Model/Properties/IAllureLinksProperty.cs
@@ -1,9 +1,5 @@
-using System.Collections.Immutable;
-
namespace Allure.Testing.Assertions.Model.Properties;
[GenerateAllureAssertions]
public interface IAllureLinksProperty : IAllureObjectArrayProperty
- where TSelf : IAllureModelObject, IAllureLinksProperty
-{
-}
+ where TSelf : IAllureModelObject, IAllureLinksProperty;
diff --git a/tests/Allure.Testing/Assertions/Model/Properties/IAllureMessageProperty.cs b/tests/Allure.Testing/Assertions/Model/Properties/IAllureMessageProperty.cs
index d1e62f2a..fa00d804 100644
--- a/tests/Allure.Testing/Assertions/Model/Properties/IAllureMessageProperty.cs
+++ b/tests/Allure.Testing/Assertions/Model/Properties/IAllureMessageProperty.cs
@@ -2,6 +2,4 @@ namespace Allure.Testing.Assertions.Model.Properties;
[GenerateAllureAssertions]
public interface IAllureMessageProperty : IAllureStringProperty
- where TSelf : IAllureModelObject, IAllureMessageProperty
-{
-}
+ where TSelf : IAllureModelObject, IAllureMessageProperty;
diff --git a/tests/Allure.Testing/Assertions/Model/Properties/IAllureMutedProperty.cs b/tests/Allure.Testing/Assertions/Model/Properties/IAllureMutedProperty.cs
index ac8f2f58..01037245 100644
--- a/tests/Allure.Testing/Assertions/Model/Properties/IAllureMutedProperty.cs
+++ b/tests/Allure.Testing/Assertions/Model/Properties/IAllureMutedProperty.cs
@@ -2,6 +2,4 @@ namespace Allure.Testing.Assertions.Model.Properties;
[GenerateAllureAssertions]
public interface IAllureMutedProperty : IAllureBoolProperty
- where TSelf : IAllureModelObject, IAllureMutedProperty