From ad7dfdead0d89f9465714190a677b0d49b3c8103 Mon Sep 17 00:00:00 2001
From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com>
Date: Thu, 23 Jul 2026 17:07:47 +0700
Subject: [PATCH 01/89] feat(sdk): add sdk project
---
allure-csharp.slnx | 3 +++
src/Allure.Net.Sdk/Allure.Net.Sdk.csproj | 20 ++++++++++++++++++++
2 files changed, 23 insertions(+)
create mode 100644 src/Allure.Net.Sdk/Allure.Net.Sdk.csproj
diff --git a/allure-csharp.slnx b/allure-csharp.slnx
index c37dcc2e..8a240974 100644
--- a/allure-csharp.slnx
+++ b/allure-csharp.slnx
@@ -55,6 +55,9 @@
+
+
+
diff --git a/src/Allure.Net.Sdk/Allure.Net.Sdk.csproj b/src/Allure.Net.Sdk/Allure.Net.Sdk.csproj
new file mode 100644
index 00000000..0fba1233
--- /dev/null
+++ b/src/Allure.Net.Sdk/Allure.Net.Sdk.csproj
@@ -0,0 +1,20 @@
+
+
+
+ netstandard2.0
+ Allure.Sdk
+ Provides the API and utilities for building Allure integrations.
+ Allure-Color.png
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
From d6718bed45b8cc1228651da7add168a936551d1c Mon Sep 17 00:00:00 2001
From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com>
Date: Thu, 23 Jul 2026 18:28:55 +0700
Subject: [PATCH 02/89] feat(sdk): add destinations
---
src/Allure.Net.Sdk/Allure.Net.Sdk.csproj | 4 +
.../Results/FileSystemResultsDestination.cs | 153 ++++++++++++++++++
.../Results/IAllureResultsDestination.cs | 80 +++++++++
.../Results/InMemoryResultsDestination.cs | 131 +++++++++++++++
.../Results/NullResultsDestination.cs | 70 ++++++++
5 files changed, 438 insertions(+)
create mode 100644 src/Allure.Net.Sdk/Results/FileSystemResultsDestination.cs
create mode 100644 src/Allure.Net.Sdk/Results/IAllureResultsDestination.cs
create mode 100644 src/Allure.Net.Sdk/Results/InMemoryResultsDestination.cs
create mode 100644 src/Allure.Net.Sdk/Results/NullResultsDestination.cs
diff --git a/src/Allure.Net.Sdk/Allure.Net.Sdk.csproj b/src/Allure.Net.Sdk/Allure.Net.Sdk.csproj
index 0fba1233..80658733 100644
--- a/src/Allure.Net.Sdk/Allure.Net.Sdk.csproj
+++ b/src/Allure.Net.Sdk/Allure.Net.Sdk.csproj
@@ -12,6 +12,10 @@
+
+
+
+
diff --git a/src/Allure.Net.Sdk/Results/FileSystemResultsDestination.cs b/src/Allure.Net.Sdk/Results/FileSystemResultsDestination.cs
new file mode 100644
index 00000000..2f466c62
--- /dev/null
+++ b/src/Allure.Net.Sdk/Results/FileSystemResultsDestination.cs
@@ -0,0 +1,153 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Allure.Model;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Allure.Sdk.Results;
+
+///
+/// A destination that represents a directory in the local file system.
+///
+public class FileSystemResultsDestination : IAllureResultsDestination
+{
+ const int DefaultCopyBufferSize = 81920;
+ private readonly string outputDirectory;
+ private readonly JsonSerializerOptions serializerOptions;
+
+ public FileSystemResultsDestination(string directoryPath, bool indentJson)
+ {
+ this.outputDirectory = directoryPath;
+
+ this.serializerOptions = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = indentJson,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ Converters =
+ {
+ new JsonStringEnumConverter(
+ namingPolicy: JsonNamingPolicy.CamelCase,
+ allowIntegerValues: false
+ ),
+ new JsonStringEnumConverter(
+ namingPolicy: JsonNamingPolicy.CamelCase,
+ allowIntegerValues: false
+ ),
+ new JsonStringEnumConverter(
+ namingPolicy: JsonNamingPolicy.CamelCase,
+ allowIntegerValues: false
+ ),
+ new JsonStringEnumConverter(
+ namingPolicy: JsonNamingPolicy.CamelCase,
+ allowIntegerValues: false
+ ),
+ },
+ };
+ }
+
+ public void WriteTestResult(TestResult testResult)
+ {
+ this.WriteAllureObject(testResult, "-result.json");
+ }
+
+ public async Task WriteTestResultAsync(TestResult testResult, CancellationToken cancellationToken)
+ {
+ await this.WriteAllureObjectAsync(testResult, "-result.json");
+ }
+
+ public void WriteContainer(TestResultScope container)
+ {
+ this.WriteAllureObject(container, "-container.json");
+ }
+
+ public async Task WriteContainerAsync(TestResultScope container, CancellationToken cancellationToken)
+ {
+ await this.WriteAllureObjectAsync(container, "-container.json");
+ }
+
+ public void WriteGlobals(Globals globals)
+ {
+ this.WriteAllureObject(globals, "-globals.json");
+ }
+
+ public async Task WriteGlobalsAsync(Globals globals, CancellationToken cancellationToken)
+ {
+ await this.WriteAllureObjectAsync(globals, "-globals.json");
+ }
+
+ public void WriteAttachment(string outputFileName, Stream content)
+ {
+ using FileStream output = this.CreateOutput(outputFileName);
+ content.CopyTo(output);
+ }
+
+ public async Task WriteAttachmentAsync(
+ string outputFileName,
+ Stream content,
+ CancellationToken cancellationToken
+ )
+ {
+ using FileStream output = this.CreateOutput(outputFileName);
+ await content.CopyToAsync(output, DefaultCopyBufferSize, cancellationToken);
+ }
+
+ public void CopyAttachment(string destinationFileName, string sourceFilePath)
+ {
+ var outputFilePath = Path.Combine(outputDirectory, destinationFileName);
+ File.Copy(sourceFilePath, outputFilePath);
+ }
+
+ public async Task CopyAttachmentAsync(
+ string destinationFileName,
+ string sourceFilePath,
+ CancellationToken cancellationToken
+ )
+ {
+ using var source = File.OpenRead(sourceFilePath);
+ using var output = this.CreateOutput(destinationFileName);
+ await source.CopyToAsync(output, DefaultCopyBufferSize, cancellationToken);
+ }
+
+ FileStream CreateOutput(string name)
+ {
+ var outputFilePath = Path.Combine(outputDirectory, name);
+
+ this.EnsureDirectory();
+
+ return File.OpenWrite(outputFilePath);
+ }
+
+ void WriteAllureObject(object allureObject, string suffix)
+ {
+ using var fileStream = this.CreateAllureObjectOutputStream(suffix);
+ JsonSerializer.Serialize(fileStream, allureObject, serializerOptions);
+ }
+
+ async Task WriteAllureObjectAsync(object allureObject, string suffix)
+ {
+ using var fileStream = this.CreateAllureObjectOutputStream(suffix);
+ await JsonSerializer.SerializeAsync(fileStream, allureObject, serializerOptions);
+ }
+
+ FileStream CreateAllureObjectOutputStream(string suffix)
+ {
+ var uuid = Guid.NewGuid();
+ var outputFileName = $"{uuid:N}{suffix}";
+ var outputFilePath = Path.Combine(outputDirectory, outputFileName);
+
+ this.EnsureDirectory();
+
+ return File.OpenWrite(outputFilePath);
+ }
+
+ void EnsureDirectory()
+ {
+ if (!Directory.Exists(this.outputDirectory))
+ {
+ Directory.CreateDirectory(this.outputDirectory);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Allure.Net.Sdk/Results/IAllureResultsDestination.cs b/src/Allure.Net.Sdk/Results/IAllureResultsDestination.cs
new file mode 100644
index 00000000..31631877
--- /dev/null
+++ b/src/Allure.Net.Sdk/Results/IAllureResultsDestination.cs
@@ -0,0 +1,80 @@
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Allure.Model;
+
+namespace Allure.Sdk.Results;
+
+///
+/// Represents a destination of Allure results.
+///
+public interface IAllureResultsDestination
+{
+ ///
+ /// Writes a test result to the destination.
+ ///
+ void WriteTestResult(TestResult testResult);
+
+ ///
+ /// Writes a test result to the destination.
+ ///
+ Task WriteTestResultAsync(TestResult testResult, CancellationToken cancellationToken);
+
+ ///
+ /// Writes a container to the destination.
+ ///
+ void WriteContainer(TestResultScope scope);
+
+ ///
+ /// Writes a container to the destination.
+ ///
+ Task WriteContainerAsync(TestResultScope scope, CancellationToken cancellationToken);
+
+ ///
+ /// Writes a globals object to the destination.
+ ///
+ void WriteGlobals(Globals globals);
+
+ ///
+ /// Writes a globals object to the destination.
+ ///
+ Task WriteGlobalsAsync(Globals globals, CancellationToken cancellationToken);
+
+ ///
+ /// Writes an attachment file to the destination.
+ ///
+ /// A name of the attachment file in the output location.
+ /// A stream thad defines the content of the attachment.
+ void WriteAttachment(string outputFileName, Stream content);
+
+ ///
+ /// Writes an attachment file to the destination.
+ ///
+ /// A name of the attachment file in the output location.
+ /// A stream thad defines the content of the attachment.
+ /// A cancellation token.
+ Task WriteAttachmentAsync(
+ string outputFileName,
+ Stream content,
+ CancellationToken cancellationToken
+ );
+
+ ///
+ /// Copies an attachment file from an existing file to the destination.
+ ///
+ /// A name of the file in the output location.
+ /// The path of the file.
+ void CopyAttachment(string destinationFileName, string sourceFilePath);
+
+ ///
+ /// Copies an attachment file from an existing file to the destination.
+ ///
+ /// A name of the file in the output location.
+ /// The path of the file.
+ /// A cancellation token.
+ Task CopyAttachmentAsync(
+ string destinationFileName,
+ string sourceFilePath,
+ CancellationToken cancellationToken
+ );
+}
\ No newline at end of file
diff --git a/src/Allure.Net.Sdk/Results/InMemoryResultsDestination.cs b/src/Allure.Net.Sdk/Results/InMemoryResultsDestination.cs
new file mode 100644
index 00000000..6e78dad0
--- /dev/null
+++ b/src/Allure.Net.Sdk/Results/InMemoryResultsDestination.cs
@@ -0,0 +1,131 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Allure.Model;
+
+namespace Allure.Sdk.Results;
+
+///
+/// An destination that stores all results in memory. Useful for testing.
+///
+public class InMemoryResultsDestination : IAllureResultsDestination
+{
+ readonly object monitor = new();
+
+ ///
+ /// Gets a list of all written test results.
+ ///
+ public List TestResults { get; } = [];
+
+ ///
+ /// Gets a list of all written containers.
+ ///
+ public List TestContainers { get; } = [];
+
+ ///
+ /// Gets a list of all written globals.
+ ///
+ public List Globals { get; } = [];
+
+ ///
+ /// Gets a list of all written attachment content.
+ ///
+ public Dictionary ByteAttachments { get; } = [];
+
+ ///
+ /// Gets a list of all copied attachment files.
+ ///
+ public Dictionary FileAttachments { get; } = [];
+
+ ///
+ public void CopyAttachment(string destinationFileName, string sourceFilePath)
+ {
+ lock (this.monitor)
+ {
+ this.FileAttachments.Add(destinationFileName, sourceFilePath);
+ }
+ }
+
+ ///
+ public Task CopyAttachmentAsync(string destinationFileName, string sourceFilePath, CancellationToken cancellationToken)
+ {
+ this.CopyAttachment(destinationFileName, sourceFilePath);
+ return Task.CompletedTask;
+ }
+
+ ///
+ public void WriteAttachment(string outputFileName, Stream content)
+ {
+ using MemoryStream memoryStream = new();
+ content.CopyTo(memoryStream);
+ this.ByteAttachments.Add(outputFileName, memoryStream.ToArray());
+ }
+
+ ///
+ public Task WriteAttachmentAsync(string outputFileName, Stream content, CancellationToken cancellationToken)
+ {
+ this.WriteAttachment(outputFileName, content);
+ return Task.CompletedTask;
+ }
+
+ ///
+ public void WriteGlobals(Globals globals)
+ {
+ lock(this.monitor)
+ {
+ this.Globals.Add(globals);
+ }
+ }
+
+ ///
+ public Task WriteGlobalsAsync(Globals globals, CancellationToken cancellationToken)
+ {
+ this.WriteGlobals(globals);
+ return Task.CompletedTask;
+ }
+
+ ///
+ public void WriteContainer(TestResultScope container)
+ {
+ lock(this.monitor)
+ {
+ this.TestContainers.Add(container);
+ }
+ }
+
+ ///
+ public Task WriteContainerAsync(TestResultScope container, CancellationToken cancellationToken)
+ {
+ this.WriteContainer(container);
+ return Task.CompletedTask;
+ }
+
+ ///
+ public void WriteTestResult(TestResult testResult)
+ {
+ lock(this.monitor)
+ {
+ this.TestResults.Add(testResult);
+ }
+ }
+
+ ///
+ public Task WriteTestResultAsync(TestResult testResult, CancellationToken cancellationToken)
+ {
+ this.WriteTestResult(testResult);
+ return Task.CompletedTask;
+ }
+
+ public void Clear()
+ {
+ lock(this.monitor)
+ {
+ this.TestContainers.Clear();
+ this.TestResults.Clear();
+ this.Globals.Clear();
+ this.ByteAttachments.Clear();
+ this.FileAttachments.Clear();
+ }
+ }
+}
diff --git a/src/Allure.Net.Sdk/Results/NullResultsDestination.cs b/src/Allure.Net.Sdk/Results/NullResultsDestination.cs
new file mode 100644
index 00000000..0e89e4a3
--- /dev/null
+++ b/src/Allure.Net.Sdk/Results/NullResultsDestination.cs
@@ -0,0 +1,70 @@
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Allure.Model;
+
+namespace Allure.Sdk.Results;
+
+///
+/// An absent destination that ignores everything written to it.
+///
+public class NullResultsDestination : IAllureResultsDestination
+{
+ ///
+ public void CopyAttachment(string destinationFileName, string sourceFilePath)
+ {
+ }
+
+ ///
+ public Task CopyAttachmentAsync(
+ string destinationFileName,
+ string sourceFilePath,
+ CancellationToken cancellationToken
+ ) =>
+ Task.CompletedTask;
+
+ ///
+ public void WriteAttachment(string outputFileName, Stream content)
+ {
+ }
+
+ ///
+ public Task WriteAttachmentAsync(
+ string outputFileName,
+ Stream content,
+ CancellationToken cancellationToken
+ ) =>
+ Task.CompletedTask;
+
+ ///
+ public void WriteGlobals(Globals globals)
+ {
+ }
+
+ ///
+ public Task WriteGlobalsAsync(Globals globals, CancellationToken cancellationToken) =>
+ Task.CompletedTask;
+
+ ///
+ public void WriteContainer(TestResultScope container)
+ {
+ }
+
+ ///
+ public Task WriteContainerAsync(TestResultScope container, CancellationToken cancellationToken) =>
+ Task.CompletedTask;
+
+ ///
+ public void WriteTestResult(TestResult testResult)
+ {
+ }
+
+ ///
+ public Task WriteTestResultAsync(TestResult testResult, CancellationToken cancellationToken) =>
+ Task.CompletedTask;
+
+ ///
+ /// A cached instance of the null destination.
+ ///
+ public static NullResultsDestination Instance { get; } = new();
+}
\ No newline at end of file
From fbb7c040c99e04011608f341d9039053668d4f78 Mon Sep 17 00:00:00 2001
From: Maksim Stepanov <17935127+delatrie@users.noreply.github.com>
Date: Fri, 24 Jul 2026 16:19:06 +0700
Subject: [PATCH 03/89] feat(sdk): add remaining features
---
.../CompilerHints/IsExternalInit.cs | 21 +
.../CompilerHints/MaybeNullWhenAttribute.cs | 15 +
.../CompilerHints/NotNullWhenAttribute.cs | 14 +
.../Configuration/AllureConfiguration.cs | 32 +
.../Configuration/AllureLinkTemplate.cs | 6 +
.../IAllureConfigurationSource.cs | 11 +
.../JsonFileConfigurationSource.cs | 160 +++++
.../LambdaConfigurationSource.cs | 29 +
src/Allure.Net.Sdk/Functions/IdFunctions.cs | 266 ++++++++
.../Functions/ModelFunctions.cs | 267 ++++++++
src/Allure.Net.Sdk/Internal/ReadLockScope.cs | 21 +
.../ReaderWriterLockSlimExtensions.cs | 24 +
.../AllureInProcessEndpointBuilder.cs | 79 +++
.../AllureRegistrationDependencies.cs | 27 +
.../DefaultParameterSerializerBuilder.cs | 63 ++
.../Internal/RuleBasedParameterSerializer.cs | 73 ++
.../Runtime/AllureInProcessRuntimeEndpoint.cs | 27 +
.../Internal/Runtime/AllureRuntime.cs | 33 +
.../Internal/Runtime/AllureRuntimeRoute.cs | 24 +
.../Runtime/AsyncLocalRuntimeContext.cs | 101 +++
.../RuntimeBoundAsyncFixtureContext.cs | 56 ++
.../Runtime/RuntimeBoundAsyncOperations.cs | 554 +++++++++++++++
.../Runtime/RuntimeBoundAsyncStepContext.cs | 67 ++
.../Runtime/RuntimeBoundLifecycleApi.cs | 118 ++++
.../Internal/Runtime/RuntimeBoundModelApi.cs | 136 ++++
.../Runtime/RuntimeBoundSyncFixtureContext.cs | 53 ++
.../Runtime/RuntimeBoundSyncOperations.cs | 630 ++++++++++++++++++
.../Runtime/RuntimeBoundSyncStepContext.cs | 55 ++
src/Allure.Net.Sdk/Internal/TypeFunctions.cs | 20 +
src/Allure.Net.Sdk/Internal/WriteLockScope.cs | 21 +
.../AllureRegistrationContextExtensions.cs | 57 ++
.../AllureRegistrationDefaults.cs | 50 ++
.../AllureRuntimeRegistrationContext.cs | 140 ++++
.../IAllureEndpointRegistrationContext.cs | 18 +
...ureInProcessEndpointRegistrationContext.cs | 25 +
.../IAllureIntegrationRegistrationContext.cs | 27 +
.../IAllureRegistrationContext.cs | 9 +
.../IAllureRegistrationDependencies.cs | 15 +
.../Registration/IAllureRegistrationHook.cs | 8 +
.../IAllureRuntimeRegistrationContext.cs | 28 +
.../IAllureRuntimeRegistrationHook.cs | 14 +
.../Registration/ILateBoundReferenceView.cs | 8 +
...IParameterSerializerRegistrationContext.cs | 20 +
.../Registration/LateBoundReference.cs | 26 +
...SerializerRegistrationContextExtensions.cs | 33 +
.../Runtime/AllureExecutionState.cs | 298 +++++++++
.../Runtime/IAllureLifecycleApi.cs | 28 +
src/Allure.Net.Sdk/Runtime/IAllureModelApi.cs | 44 ++
src/Allure.Net.Sdk/Runtime/IAllureRuntime.cs | 26 +
.../Runtime/IAllureRuntimeContext.cs | 136 ++++
.../IParameterSerializationRule.cs | 8 +
.../JsonParameterSerializationRule.cs | 27 +
.../LambdaParameterSerializationRule.cs | 15 +
.../ToStringParameterSerializationRule.cs | 14 +
.../TypedParameterSerializationRule.cs | 20 +
55 files changed, 4097 insertions(+)
create mode 100644 src/Allure.Net.Sdk/CompilerHints/IsExternalInit.cs
create mode 100644 src/Allure.Net.Sdk/CompilerHints/MaybeNullWhenAttribute.cs
create mode 100644 src/Allure.Net.Sdk/CompilerHints/NotNullWhenAttribute.cs
create mode 100644 src/Allure.Net.Sdk/Configuration/AllureConfiguration.cs
create mode 100644 src/Allure.Net.Sdk/Configuration/AllureLinkTemplate.cs
create mode 100644 src/Allure.Net.Sdk/Configuration/IAllureConfigurationSource.cs
create mode 100644 src/Allure.Net.Sdk/Configuration/JsonFileConfigurationSource.cs
create mode 100644 src/Allure.Net.Sdk/Configuration/LambdaConfigurationSource.cs
create mode 100644 src/Allure.Net.Sdk/Functions/IdFunctions.cs
create mode 100644 src/Allure.Net.Sdk/Functions/ModelFunctions.cs
create mode 100644 src/Allure.Net.Sdk/Internal/ReadLockScope.cs
create mode 100644 src/Allure.Net.Sdk/Internal/ReaderWriterLockSlimExtensions.cs
create mode 100644 src/Allure.Net.Sdk/Internal/Registration/AllureInProcessEndpointBuilder.cs
create mode 100644 src/Allure.Net.Sdk/Internal/Registration/AllureRegistrationDependencies.cs
create mode 100644 src/Allure.Net.Sdk/Internal/Registration/DefaultParameterSerializerBuilder.cs
create mode 100644 src/Allure.Net.Sdk/Internal/RuleBasedParameterSerializer.cs
create mode 100644 src/Allure.Net.Sdk/Internal/Runtime/AllureInProcessRuntimeEndpoint.cs
create mode 100644 src/Allure.Net.Sdk/Internal/Runtime/AllureRuntime.cs
create mode 100644 src/Allure.Net.Sdk/Internal/Runtime/AllureRuntimeRoute.cs
create mode 100644 src/Allure.Net.Sdk/Internal/Runtime/AsyncLocalRuntimeContext.cs
create mode 100644 src/Allure.Net.Sdk/Internal/Runtime/RuntimeBoundAsyncFixtureContext.cs
create mode 100644 src/Allure.Net.Sdk/Internal/Runtime/RuntimeBoundAsyncOperations.cs
create mode 100644 src/Allure.Net.Sdk/Internal/Runtime/RuntimeBoundAsyncStepContext.cs
create mode 100644 src/Allure.Net.Sdk/Internal/Runtime/RuntimeBoundLifecycleApi.cs
create mode 100644 src/Allure.Net.Sdk/Internal/Runtime/RuntimeBoundModelApi.cs
create mode 100644 src/Allure.Net.Sdk/Internal/Runtime/RuntimeBoundSyncFixtureContext.cs
create mode 100644 src/Allure.Net.Sdk/Internal/Runtime/RuntimeBoundSyncOperations.cs
create mode 100644 src/Allure.Net.Sdk/Internal/Runtime/RuntimeBoundSyncStepContext.cs
create mode 100644 src/Allure.Net.Sdk/Internal/TypeFunctions.cs
create mode 100644 src/Allure.Net.Sdk/Internal/WriteLockScope.cs
create mode 100644 src/Allure.Net.Sdk/Registration/AllureRegistrationContextExtensions.cs
create mode 100644 src/Allure.Net.Sdk/Registration/AllureRegistrationDefaults.cs
create mode 100644 src/Allure.Net.Sdk/Registration/AllureRuntimeRegistrationContext.cs
create mode 100644 src/Allure.Net.Sdk/Registration/IAllureEndpointRegistrationContext.cs
create mode 100644 src/Allure.Net.Sdk/Registration/IAllureInProcessEndpointRegistrationContext.cs
create mode 100644 src/Allure.Net.Sdk/Registration/IAllureIntegrationRegistrationContext.cs
create mode 100644 src/Allure.Net.Sdk/Registration/IAllureRegistrationContext.cs
create mode 100644 src/Allure.Net.Sdk/Registration/IAllureRegistrationDependencies.cs
create mode 100644 src/Allure.Net.Sdk/Registration/IAllureRegistrationHook.cs
create mode 100644 src/Allure.Net.Sdk/Registration/IAllureRuntimeRegistrationContext.cs
create mode 100644 src/Allure.Net.Sdk/Registration/IAllureRuntimeRegistrationHook.cs
create mode 100644 src/Allure.Net.Sdk/Registration/ILateBoundReferenceView.cs
create mode 100644 src/Allure.Net.Sdk/Registration/IParameterSerializerRegistrationContext.cs
create mode 100644 src/Allure.Net.Sdk/Registration/LateBoundReference.cs
create mode 100644 src/Allure.Net.Sdk/Registration/ParameterSerializerRegistrationContextExtensions.cs
create mode 100644 src/Allure.Net.Sdk/Runtime/AllureExecutionState.cs
create mode 100644 src/Allure.Net.Sdk/Runtime/IAllureLifecycleApi.cs
create mode 100644 src/Allure.Net.Sdk/Runtime/IAllureModelApi.cs
create mode 100644 src/Allure.Net.Sdk/Runtime/IAllureRuntime.cs
create mode 100644 src/Allure.Net.Sdk/Runtime/IAllureRuntimeContext.cs
create mode 100644 src/Allure.Net.Sdk/Serialization/IParameterSerializationRule.cs
create mode 100644 src/Allure.Net.Sdk/Serialization/JsonParameterSerializationRule.cs
create mode 100644 src/Allure.Net.Sdk/Serialization/LambdaParameterSerializationRule.cs
create mode 100644 src/Allure.Net.Sdk/Serialization/ToStringParameterSerializationRule.cs
create mode 100644 src/Allure.Net.Sdk/Serialization/TypedParameterSerializationRule.cs
diff --git a/src/Allure.Net.Sdk/CompilerHints/IsExternalInit.cs b/src/Allure.Net.Sdk/CompilerHints/IsExternalInit.cs
new file mode 100644
index 00000000..4c9e70a3
--- /dev/null
+++ b/src/Allure.Net.Sdk/CompilerHints/IsExternalInit.cs
@@ -0,0 +1,21 @@
+using System.ComponentModel;
+
+#pragma warning disable IDE0130
+
+namespace System.Runtime.CompilerServices;
+
+///
+/// This class serves as an init-only setter modreq to make a library that
+/// uses init only setters compile against pre-net5.0 TFMs (including .NET
+/// Standard). See
+///
+/// this article
+///
+/// and
+///
+/// this answer
+///
+/// for more details.
+///
+[EditorBrowsable(EditorBrowsableState.Never)]
+internal static class IsExternalInit { }
diff --git a/src/Allure.Net.Sdk/CompilerHints/MaybeNullWhenAttribute.cs b/src/Allure.Net.Sdk/CompilerHints/MaybeNullWhenAttribute.cs
new file mode 100644
index 00000000..67e159d8
--- /dev/null
+++ b/src/Allure.Net.Sdk/CompilerHints/MaybeNullWhenAttribute.cs
@@ -0,0 +1,15 @@
+namespace System.Diagnostics.CodeAnalysis;
+
+///
+/// This attribute hints the compiler that the parameter it's applied to
+/// has the nullability described by it type, but it may additionally be null
+/// when the method returns a specified value.
+/// It's not included in netstandard2.0 but can be define it the project's code.
+///
+///
+/// A return value that indicated the argument is not null
+///
+[AttributeUsage(AttributeTargets.Parameter)]
+internal sealed class MaybeNullWhenAttribute(bool returnValue) : Attribute {
+ public bool ReturnValue { get; } = returnValue;
+}
\ No newline at end of file
diff --git a/src/Allure.Net.Sdk/CompilerHints/NotNullWhenAttribute.cs b/src/Allure.Net.Sdk/CompilerHints/NotNullWhenAttribute.cs
new file mode 100644
index 00000000..a52915cb
--- /dev/null
+++ b/src/Allure.Net.Sdk/CompilerHints/NotNullWhenAttribute.cs
@@ -0,0 +1,14 @@
+namespace System.Diagnostics.CodeAnalysis;
+
+///
+/// This attribute hints the compiler that the parameter it's applied to
+/// is not null if the method returns a specified value.
+/// It's not included in netstandard2.0 but can be define it the project's code.
+///
+///
+/// A return value that indicated the argument is not null
+///
+[AttributeUsage(AttributeTargets.Parameter)]
+internal sealed class NotNullWhenAttribute(bool returnValue) : Attribute {
+ public bool ReturnValue { get; } = returnValue;
+}
\ No newline at end of file
diff --git a/src/Allure.Net.Sdk/Configuration/AllureConfiguration.cs b/src/Allure.Net.Sdk/Configuration/AllureConfiguration.cs
new file mode 100644
index 00000000..f43e2e5e
--- /dev/null
+++ b/src/Allure.Net.Sdk/Configuration/AllureConfiguration.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Immutable;
+using System.IO;
+
+namespace Allure.Sdk.Configuration;
+
+public class AllureConfiguration
+{
+ readonly string resultsDirecory =
+ Path.Combine(Environment.CurrentDirectory, "allure-results");
+
+ public string Hostname { get; init; } = Environment.MachineName;
+
+ public string ResultsDirectory
+ {
+ get => this.resultsDirecory;
+ init
+ {
+ this.resultsDirecory = Path.GetFullPath(value);
+ }
+ }
+
+ public ImmutableDictionary LinkTemplates { get; init; } = [];
+
+ public ImmutableList FailExceptions { get; init; } = [];
+
+ public bool IndentOutput { get; init; } = false;
+
+ public ImmutableDictionary GlobalLabels { get; init; } = [];
+
+ public string? RegistrationHook { get; init; }
+}
diff --git a/src/Allure.Net.Sdk/Configuration/AllureLinkTemplate.cs b/src/Allure.Net.Sdk/Configuration/AllureLinkTemplate.cs
new file mode 100644
index 00000000..5a7d0339
--- /dev/null
+++ b/src/Allure.Net.Sdk/Configuration/AllureLinkTemplate.cs
@@ -0,0 +1,6 @@
+namespace Allure.Sdk.Configuration;
+
+public record class AllureLinkTemplate(
+ string UrlTemplate,
+ string? NameTemplate
+);
diff --git a/src/Allure.Net.Sdk/Configuration/IAllureConfigurationSource.cs b/src/Allure.Net.Sdk/Configuration/IAllureConfigurationSource.cs
new file mode 100644
index 00000000..894997d3
--- /dev/null
+++ b/src/Allure.Net.Sdk/Configuration/IAllureConfigurationSource.cs
@@ -0,0 +1,11 @@
+namespace Allure.Sdk.Configuration;
+
+public interface IAllureConfigurationSource
+ where TConfig : AllureConfiguration
+{
+ string Name { get; }
+
+ bool CanLoad { get; }
+
+ TConfig LoadConfiguration();
+}
diff --git a/src/Allure.Net.Sdk/Configuration/JsonFileConfigurationSource.cs b/src/Allure.Net.Sdk/Configuration/JsonFileConfigurationSource.cs
new file mode 100644
index 00000000..5e12fb1c
--- /dev/null
+++ b/src/Allure.Net.Sdk/Configuration/JsonFileConfigurationSource.cs
@@ -0,0 +1,160 @@
+using System;
+using System.IO;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+
+namespace Allure.Sdk.Configuration;
+
+public class JsonFileConfigurationSource(string path) :
+ IAllureConfigurationSource
+
+ where TConfiguration : AllureConfiguration, new()
+{
+ static readonly JsonSerializerOptions serializerOptions = new()
+ {
+ PropertyNameCaseInsensitive = true,
+ };
+
+ public string Name => $"json from {path}";
+
+ public bool CanLoad => path is { Length: >0 };
+
+ public TConfiguration LoadConfiguration()
+ {
+ var configFullPath = Path.GetFullPath(path);
+ if (!File.Exists(configFullPath))
+ {
+ throw new FileNotFoundException(
+ $"The configuration file '{configFullPath}' does not exist."
+ );
+ }
+
+ using var stream = File.OpenRead(configFullPath);
+ var root = JsonNode.Parse(stream) as JsonObject
+ ?? throw new JsonException(
+ "The Allure configuration file must contain a JSON object."
+ );
+
+ var configurationObject = GetConfigurationObject(root);
+ NormalizeLegacyProperties(configurationObject);
+
+ return configurationObject.Deserialize(serializerOptions)
+ ?? new();
+ }
+
+ static JsonObject GetConfigurationObject(JsonObject root) =>
+ root.TryGetPropertyValue("allure", out var allureNode)
+ ? (allureNode as JsonObject
+ ?? throw new InvalidOperationException(
+ "The 'allure' property must contain a JSON object"))
+ : root;
+
+ void NormalizeLegacyProperties(JsonObject configuration)
+ {
+ if (!configuration.ContainsKey("hostname")
+ && configuration.TryGetPropertyValue("title", out var titleNode)
+ && titleNode?.GetValueKind() is JsonValueKind.String)
+ {
+ configuration["hostname"] = titleNode.DeepClone();
+ }
+
+ if (!configuration.ContainsKey("resultsDirectory"))
+ {
+ if (configuration.TryGetPropertyValue("directory", out var directoryNode)
+ && directoryNode?.GetValueKind() is JsonValueKind.String)
+ {
+ configuration["resultsDirectory"] = directoryNode.DeepClone();
+ }
+ }
+
+ if (!configuration.ContainsKey("linkTemplates")
+ && configuration.TryGetPropertyValue("links", out var linksNode)
+ && linksNode?.GetValueKind() is JsonValueKind.Array)
+ {
+ configuration["linkTemplates"] = ConvertLegacyLinks(linksNode.AsArray());
+ }
+
+ configuration.Remove("title");
+ configuration.Remove("directory");
+ configuration.Remove("links");
+ }
+
+ JsonNode? ConvertLegacyLinks(JsonArray jsonArray)
+ {
+ var result = new JsonObject();
+
+ foreach (var item in jsonArray)
+ {
+ if (item is not JsonValue value
+ || !value.TryGetValue(out var link))
+ {
+ throw new JsonException(
+ "Every item in the legacy 'links' array must be a string."
+ );
+ }
+
+ if (TryParseLegacyLink(link, out var type, out var urlTemplate))
+ {
+ result[type] = new JsonObject
+ {
+ ["urlTemplate"] = urlTemplate,
+ };
+ }
+ }
+
+ return result;
+ }
+
+ bool TryParseLegacyLink(string link, out string type, out string urlTemplate)
+ {
+ for (var open = link.IndexOf('{'); open >= 0; open = link.IndexOf('{', open + 1))
+ {
+ if (open + 1 >= link.Length)
+ {
+ break;
+ }
+
+ if (link[open + 1] == '{')
+ {
+ open++;
+ continue;
+ }
+
+ var close = link.IndexOf('}', open + 1);
+ if (close < 0 || close == open + 1)
+ {
+ continue;
+ }
+
+ type = link.Substring(open + 1, close - open - 1);
+ urlTemplate = $"{link.Substring(0, open)}{{0}}{link.Substring(close + 1)}";
+ return true;
+ }
+
+ type = "";
+ urlTemplate = "";
+ return false;
+ }
+}
+
+public static class JsonFileConfigurationSource
+{
+ public static JsonFileConfigurationSource FromEnvironmentVariable(
+ string environmentVariableName
+ )
+ where TConfiguration : AllureConfiguration, new()
+ => new(
+ Environment.GetEnvironmentVariable(environmentVariableName)
+ );
+
+ public static JsonFileConfigurationSource FromEnvironmentVariable()
+ where TConfiguration : AllureConfiguration, new()
+ =>
+ FromEnvironmentVariable("ALLURE_CONFIG");
+
+ public static JsonFileConfigurationSource FromBaseDirectory()
+ where TConfiguration : AllureConfiguration, new()
+ =>
+ new(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "allureConfig.json"));
+}
diff --git a/src/Allure.Net.Sdk/Configuration/LambdaConfigurationSource.cs b/src/Allure.Net.Sdk/Configuration/LambdaConfigurationSource.cs
new file mode 100644
index 00000000..32822169
--- /dev/null
+++ b/src/Allure.Net.Sdk/Configuration/LambdaConfigurationSource.cs
@@ -0,0 +1,29 @@
+using System;
+
+namespace Allure.Sdk.Configuration;
+
+public sealed class LambdaConfigurationSource(
+ string name,
+ Func factory
+) :
+ IAllureConfigurationSource
+
+ where TConfiguration : AllureConfiguration
+{
+ public string Name => name;
+
+ public bool CanLoad => true;
+
+ public TConfiguration LoadConfiguration() => factory();
+}
+
+public static class LambdaConfigurationSource
+{
+ public static LambdaConfigurationSource Create(
+ string name,
+ Func configurationFactory
+ )
+ where TConfiguration : AllureConfiguration
+ =>
+ new(name, configurationFactory);
+}
diff --git a/src/Allure.Net.Sdk/Functions/IdFunctions.cs b/src/Allure.Net.Sdk/Functions/IdFunctions.cs
new file mode 100644
index 00000000..8fbe9b91
--- /dev/null
+++ b/src/Allure.Net.Sdk/Functions/IdFunctions.cs
@@ -0,0 +1,266 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Text.Json;
+using Allure.Model;
+
+namespace Allure.Sdk.Functions;
+
+///
+/// Functions to create UUIDs, full names, etc.
+///
+public static class IdFunctions
+{
+ ///
+ /// Generates a UUID for a test or container.
+ ///
+ public static string CreateUUID() => Guid.NewGuid().ToString();
+
+ ///
+ /// Creates a titlePath: a path to a test class in a tree of test results.
+ ///
+ /// A type representing a test class
+ ///
+ /// A titlePath consists of:
+ ///
+ /// - assembly name
+ /// - elements of namespace
+ /// - name of type (including its declaring types, if any)
+ /// - type parameters (for generic type definitions)
+ /// - type arguments (for constructed generic types)
+ ///
+ /// The type node can be renamed by applying to the class.
+ ///
+ public static List CreateTitlePath(Type type)
+ {
+ static IEnumerable ExpandNestness(Type type)
+ {
+ for (; type.IsNested; type = type.DeclaringType)
+ yield return type.Name;
+ yield return type.Name;
+ }
+
+ var assemblyName = type.Assembly.GetName().Name;
+ var namespaceParts = (type.Namespace ?? "").Split('.').Where(s => s.Length > 0);
+ var typeNode = type.GetCustomAttribute()?.Name
+ ?? (string.Join("+", ExpandNestness(type).Reverse()) +
+ SerializeTypeParameterTypeList(
+ type.GetGenericArguments()));
+
+ return [
+ assemblyName,
+ .. namespaceParts,
+ typeNode,
+ ];
+ }
+
+ ///
+ /// Creates a titlePath: a path to a test class in a tree of test results.
+ ///
+ /// A test method
+ ///
+ /// A titlePath consists of:
+ ///
+ /// - assembly name
+ /// - elements of namespace
+ /// - name of type (including its declaring types, if any)
+ /// - type parameters (for generic type definitions)
+ /// - type arguments (for constructed generic types)
+ /// - method name with type parameters and parameter types (for parameterized method)
+ ///
+ /// The type and method nodes can be renamed by applying .
+ ///
+ public static List CreateTitlePath(MethodInfo method)
+ {
+ var titlePath = CreateTitlePath(method.DeclaringType);
+ if (method.GetParameters().Length > 0)
+ {
+ titlePath.Add(
+ method.GetCustomAttribute()?.Name
+ ?? GetMethodId(method)
+ );
+ }
+ return titlePath;
+ }
+
+ ///
+ /// Creates a name that uniquely identifies a given type.
+ ///
+ ///
+ /// A fully-qualified name of a type includes:
+ ///
+ /// - assembly name
+ /// - namespace (if any)
+ /// - name of type (including its declaring types, if any)
+ /// - type parameters (for generic type definitions)
+ /// - type arguments (for constructed generic types)
+ ///
+ /// Unlike , it doesn't include assembly
+ /// versions and other metadata that can be subject to change.
+ /// Unlike , it includes assembly names to
+ /// the name of the type and its type arguments, which prevents collisions
+ /// in some scenarios.
+ ///
+ /// For a generic parameters (like T in class Foo<T> { }), returns just its name.
+ ///
+ public static string GetTypeId(Type type) =>
+ type.IsGenericParameter ? type.Name : SerializeNonParameterType(type);
+
+ ///
+ /// Creates a name that uniquely identifies a given method in its declaring type.
+ ///
+ ///
+ /// A method.
+ /// If it's a constructed generic method, its generic definition is used instead.
+ ///
+ ///
+ /// For a given test method, its id includes:
+ ///
+ /// - name
+ /// - type parameters (like T in void Foo<T>() { })
+ /// - parameter types (like System.String in void Foo(string bar) { })
+ ///
+ ///
+ public static string GetMethodId(MethodInfo method)
+ {
+ if (method.IsGenericMethod && !method.IsGenericMethodDefinition)
+ {
+ method = method.GetGenericMethodDefinition();
+ }
+
+ var methodName = method.Name;
+ var typeParameters = method.GetGenericArguments();
+ var typeParametersDecl = SerializeTypeParameterTypeList(typeParameters);
+ var parameterTypes = SerializeParameterTypes(method.GetParameters());
+ return $"{methodName}{typeParametersDecl}({parameterTypes})";
+ }
+
+ ///
+ /// Creates a string that unuquely identifies a given method.
+ ///
+ ///
+ /// A method.
+ /// If it's a constructed generic method, its generic definition is used instead.
+ ///
+ ///
+ /// For a given test method the full name includes:
+ ///
+ /// - assembly name
+ /// - namespace (if any)
+ /// - name of type (including its declaring types, if any)
+ /// - type parameters of the declaring type (for generic type definitions)
+ /// - type arguments of the declaring type (for constructed generic types)
+ /// - the method's name
+ /// - type parameters of the method (if any)
+ /// - parameter types
+ ///
+ ///
+ public static string CreateFullName(MethodInfo method) =>
+ $"{GetTypeId(method.DeclaringType)}.{GetMethodId(method)}";
+
+ ///
+ /// Creates a testCaseId value. testCaseId has a fixed length and depends
+ /// only on a given fullName. The fullName shouldn't depend on test parameters.
+ ///
+ public static string CreateTestCaseId(string fullName) =>
+ ToMD5(fullName);
+
+ ///
+ /// Creates a historyId value to be used by Allure Reporter. historyId has a
+ /// fixed length and depends on a fullName and parameters of a test.
+ /// Howewer, it doesn't depend on parametrs order as well as on parameter
+ /// names in general. Parameters are sorted alphabetically by their names.
+ /// Then, only the values are used to produce the final historyId value.
+ ///
+ /// The fullName of a test.
+ /// The parameters of a test.
+ public static string CreateHistoryId(
+ string fullName,
+ IEnumerable parameters
+ ) =>
+ ToMD5(
+ JsonSerializer.Serialize(
+ new
+ {
+ fullName,
+ parameters = parameters.Where(p => !p.Excluded)
+ .OrderBy(p => p.Name)
+ .Select(p => p.Value)
+ }
+ )
+ );
+
+ static string ToMD5(string input)
+ {
+ using var md5 = System.Security.Cryptography.MD5.Create();
+ var inputBytes = Encoding.UTF8.GetBytes(input);
+ var outputBytes = md5.ComputeHash(inputBytes);
+ return ToHexString(outputBytes);
+ }
+
+ static string ToHexString(byte[] inputBytes)
+ {
+ var sb = new StringBuilder();
+ foreach (byte b in inputBytes)
+ {
+ sb.Append(
+ b.ToString("x2")
+ );
+ }
+ return sb.ToString();
+ }
+
+ static string SerializeParameterTypes(
+ IEnumerable parameters
+ ) =>
+ SerializeTypeList(
+ parameters.Select(p => p.ParameterType)
+ );
+
+ static string SerializeTypeParameterTypeList(IEnumerable types) =>
+ types.Any() ? SerializeNonEmptyTypeParameterTypeList(types) : "";
+
+ static string SerializeNonEmptyTypeParameterTypeList(IEnumerable types) =>
+ "[" + SerializeTypeList(types) + "]";
+
+ static string SerializeTypeList(
+ IEnumerable types
+ ) =>
+ string.Join(
+ ",",
+ types.Select(GetTypeId)
+ );
+
+ static string SerializeNonParameterType(Type type) =>
+ GetUniqueTypeName(type) + SerializeTypeParameterTypeList(
+ type.GetGenericArguments()
+ );
+
+ static string GetUniqueTypeName(Type type) =>
+ IsSystemType(type)
+ ? ConstructFullName(type)
+ : GetTypeNameWithAssembly(type);
+
+ static string ConstructFullName(Type type) =>
+ type.IsNested
+ ? ConstructFullNameOfNestedType(type)
+ : ConstructFullNameOfOutmostType(type);
+
+ static string ConstructFullNameOfNestedType(Type type) =>
+ ConstructFullName(type.DeclaringType) + "+" + type.Name;
+
+ static string ConstructFullNameOfOutmostType(Type type) =>
+ string.IsNullOrEmpty(type.Namespace)
+ ? type.Name
+ : $"{type.Namespace}.{type.Name}";
+
+ static string GetTypeNameWithAssembly(Type type) =>
+ $"{type.Assembly.GetName().Name}:" + ConstructFullName(type);
+
+ static bool IsSystemType(Type type) =>
+ type.Assembly == systemTypesAssembly;
+
+ static readonly Assembly systemTypesAssembly = typeof(object).Assembly;
+}
\ No newline at end of file
diff --git a/src/Allure.Net.Sdk/Functions/ModelFunctions.cs b/src/Allure.Net.Sdk/Functions/ModelFunctions.cs
new file mode 100644
index 00000000..a09c4f30
--- /dev/null
+++ b/src/Allure.Net.Sdk/Functions/ModelFunctions.cs
@@ -0,0 +1,267 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+using Allure.Abstractions;
+using Allure.Model;
+using Allure.Sdk.Configuration;
+using Allure.Sdk.Internal;
+
+namespace Allure.Sdk.Functions;
+
+///
+/// Contains functions to help implementing Allure model-related conversions.
+///
+public static class ModelFunctions
+{
+ ///
+ /// Checks if an exception type, one of its base types, or one of the
+ /// interfaces it implements exists in the list of known execption types.
+ ///
+ /// The list of known exception types.
+ /// The exception to check.
+ public static bool IsKnownError(IEnumerable knownErrorBases, Exception e) =>
+ knownErrorBases
+ ?.Intersect(
+ GetExceptionClassChain(e)
+ )
+ ?.Any() == true;
+
+ ///
+ /// Returns a if a given exception represents
+ /// an assertion error. Otherwise, returns .
+ ///
+ ///
+ /// The list of exception types. Exceptions of those types (including
+ /// subclasses) are considered assertion errors. This list typically comes
+ /// from the configuration associated with the current lifecycle instance.
+ ///
+ /// The exception to convert.
+ ///
+ public static Status ResolveErrorStatus(
+ IEnumerable failExceptions,
+ Exception e
+ ) =>
+ IsKnownError(failExceptions, e)
+ ? Status.Failed
+ : Status.Broken;
+
+ ///
+ /// Converts an exception to the status details.
+ ///
+ /// The exception to convert.
+ public static StatusDetails? ToStatusDetails(Exception? e) =>
+ e is null
+ ? null
+ : new()
+ {
+ Message = string.IsNullOrEmpty(e.Message)
+ ? e.GetType().Name
+ : e.Message,
+ Trace = e.ToString()
+ };
+
+ ///
+ /// Checks if the test result contains a suite-hierarchy label, i.e., one
+ /// of the parentSuite, suite, or subSuite labels. If
+ /// not, adds the provided default values to the list of labels. Otherwise,
+ /// leaves the test result as is.
+ ///
+ /// A test result to modify
+ ///
+ /// A value for the parentSuite label. If null or empty, the label
+ /// won't be added
+ ///
+ ///
+ /// A value for the suite label. If null or empty, the label won't
+ /// be added
+ ///
+ ///
+ /// A value for the subSuite label. If null or empty, the label won't
+ /// be added
+ ///
+ public static void EnsureSuites(
+ TestResult testResult,
+ string? parentSuite,
+ string? suite,
+ string? subSuite
+ )
+ {
+ var labels = testResult.Labels;
+ if (labels.Any(IsSuiteLabel))
+ {
+ return;
+ }
+
+ if (!string.IsNullOrEmpty(parentSuite))
+ {
+ labels.Add(Label.ParentSuite(parentSuite!));
+ }
+
+ if (!string.IsNullOrEmpty(suite))
+ {
+ labels.Add(Label.Suite(suite!));
+ }
+
+ if (!string.IsNullOrEmpty(subSuite))
+ {
+ labels.Add(Label.SubSuite(subSuite!));
+ }
+ }
+
+ ///
+ /// Returns a sequence of labels defined by the environment variables in form
+ /// of ALLURE_LABEL_<name>=<value>
+ ///
+ public static IEnumerable