diff --git a/allure-csharp.slnx b/allure-csharp.slnx index 1aabca56..1b8ce46f 100644 --- a/allure-csharp.slnx +++ b/allure-csharp.slnx @@ -60,6 +60,9 @@ + + + @@ -80,6 +83,9 @@ + + + diff --git a/src/Allure.TestingPlatform/Allure.TestingPlatform.csproj b/src/Allure.TestingPlatform/Allure.TestingPlatform.csproj new file mode 100644 index 00000000..43a3ed99 --- /dev/null +++ b/src/Allure.TestingPlatform/Allure.TestingPlatform.csproj @@ -0,0 +1,27 @@ + + + + Allure.TestingPlatform + Allure.TestingPlatform + net8.0 + enable + Allure Framework Contributors + Framework-agnostic Allure adapter for Microsoft.Testing.Platform. Consumes MTP messages and drives Allure.Net.Commons; intended as the foundation for framework-specific adapters (xUnit.v3, etc.). + Allure-Color.png + $(PackageTags) microsoft-testing-platform mtp + + + + + + + + + + + + + + + + diff --git a/src/Allure.TestingPlatform/AllureDataConsumer.cs b/src/Allure.TestingPlatform/AllureDataConsumer.cs new file mode 100644 index 00000000..29e8fd19 --- /dev/null +++ b/src/Allure.TestingPlatform/AllureDataConsumer.cs @@ -0,0 +1,110 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Allure.Net.Commons; +using Allure.Net.Commons.Functions; +using Microsoft.Testing.Platform.Extensions.Messages; +using Microsoft.Testing.Platform.Extensions.TestHost; + +namespace Allure.TestingPlatform; + +/// +/// Consumes Microsoft.Testing.Platform s +/// and projects them onto Allure's . +/// +/// +/// Minimal viable scope: maps terminal test states (Passed / Failed / Skipped +/// / Error / Timeout / Cancelled) to a single Schedule → Start → Update → +/// Stop → Write sequence per test. InProgress updates are ignored — +/// timing therefore reflects when the consumer processed the terminal +/// message, not the test's actual wall-clock duration. This will be addressed +/// in a follow-up PR by reading . +/// +public sealed class AllureDataConsumer : IDataConsumer +{ + static readonly Type[] s_consumed = [typeof(TestNodeUpdateMessage)]; + + readonly AllureLifecycle _lifecycle; + readonly object _sequenceLock = new(); + + public AllureDataConsumer() : this(AllureLifecycle.Instance) { } + + internal AllureDataConsumer(AllureLifecycle lifecycle) + { + _lifecycle = lifecycle; + } + + public string Uid => "Allure.TestingPlatform"; + + public string Version => typeof(AllureDataConsumer).Assembly.GetName().Version?.ToString() ?? "0.0.0"; + + public string DisplayName => "Allure Report"; + + public string Description => "Writes Allure result files for tests reported through Microsoft.Testing.Platform."; + + public Type[] DataTypesConsumed => s_consumed; + + public Task IsEnabledAsync() => Task.FromResult(true); + + public Task ConsumeAsync( + IDataProducer dataProducer, + IData value, + CancellationToken cancellationToken) + { + if (value is TestNodeUpdateMessage update) + { + HandleUpdate(update); + } + return Task.CompletedTask; + } + + void HandleUpdate(TestNodeUpdateMessage update) + { + var node = update.TestNode; + var state = node.Properties.OfType().LastOrDefault(); + if (state is null || state is InProgressTestNodeStateProperty) + { + return; + } + + var (status, message, trace) = MapState(state); + + var testResult = new TestResult + { + uuid = IdFunctions.CreateUUID(), + name = node.DisplayName, + fullName = node.Uid.Value, + historyId = node.Uid.Value, + }; + + lock (_sequenceLock) + { + _lifecycle.StartTestCase(testResult); + _lifecycle.UpdateTestCase(tr => + { + tr.status = status; + if (message is not null || trace is not null) + { + tr.statusDetails ??= new StatusDetails(); + tr.statusDetails.message = message; + tr.statusDetails.trace = trace; + } + }); + _lifecycle.StopTestCase(); + _lifecycle.WriteTestCase(); + } + } + + static (Status status, string? message, string? trace) MapState(TestNodeStateProperty state) => + state switch + { + PassedTestNodeStateProperty => (Status.passed, null, null), + SkippedTestNodeStateProperty s => (Status.skipped, s.Explanation, null), + FailedTestNodeStateProperty f => (Status.failed, f.Exception?.Message, f.Exception?.StackTrace), + ErrorTestNodeStateProperty e => (Status.broken, e.Exception?.Message, e.Exception?.StackTrace), + TimeoutTestNodeStateProperty t => (Status.broken, t.Exception?.Message ?? "Test timed out", t.Exception?.StackTrace), + CancelledTestNodeStateProperty c => (Status.broken, c.Exception?.Message ?? "Test cancelled", c.Exception?.StackTrace), + _ => (Status.none, null, null), + }; +} diff --git a/src/Allure.TestingPlatform/README.md b/src/Allure.TestingPlatform/README.md new file mode 100644 index 00000000..b80d45e6 --- /dev/null +++ b/src/Allure.TestingPlatform/README.md @@ -0,0 +1,34 @@ +# Allure.TestingPlatform + +Framework-agnostic Allure adapter for [Microsoft.Testing.Platform][mtp]. It +consumes MTP messages (`TestNodeUpdateMessage`) and drives `AllureLifecycle` +from `Allure.Net.Commons`, producing standard Allure result files. + +This package is the foundation for framework-specific adapters (xUnit.v3, +NUnit-on-MTP, etc.); those layers add framework-specific enrichment (traits → +labels, parameter formatting, fixture mapping) on top of the lifecycle handled +here. + +## Status + +**Minimal viable scaffold.** Currently maps the test happy path +(`InProgress` / `Passed` / `Failed` / `Skipped`) to Allure test results. +Steps, fixtures, attachments, parameters and labels will follow in subsequent +PRs. + +## Usage + +In your MTP-based test project's `Program.cs`: + +```csharp +using Allure.TestingPlatform; +using Microsoft.Testing.Platform.Builder; + +var builder = await TestApplication.CreateBuilderAsync(args); +// register your test framework's MTP integration first, e.g. builder.AddXunit() +builder.AddAllure(); +using var app = await builder.BuildAsync(); +return await app.RunAsync(); +``` + +[mtp]: https://learn.microsoft.com/dotnet/core/testing/microsoft-testing-platform-intro diff --git a/src/Allure.TestingPlatform/TestApplicationBuilderExtensions.cs b/src/Allure.TestingPlatform/TestApplicationBuilderExtensions.cs new file mode 100644 index 00000000..a0140dab --- /dev/null +++ b/src/Allure.TestingPlatform/TestApplicationBuilderExtensions.cs @@ -0,0 +1,25 @@ +using System; +using Microsoft.Testing.Platform.Builder; + +namespace Allure.TestingPlatform; + +/// +/// Wires the Allure data consumer into a Microsoft.Testing.Platform host. +/// +public static class TestApplicationBuilderExtensions +{ + /// + /// Registers the Allure data consumer so the host writes Allure result + /// files for every reported test. + /// + public static ITestApplicationBuilder AddAllure(this ITestApplicationBuilder builder) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.TestHost.AddDataConsumer(_ => new AllureDataConsumer()); + return builder; + } +} diff --git a/tests/Allure.TestingPlatform.Tests/Allure.TestingPlatform.Tests.csproj b/tests/Allure.TestingPlatform.Tests/Allure.TestingPlatform.Tests.csproj new file mode 100644 index 00000000..8c2364d4 --- /dev/null +++ b/tests/Allure.TestingPlatform.Tests/Allure.TestingPlatform.Tests.csproj @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/tests/Allure.TestingPlatform.Tests/AllureDataConsumerTests.cs b/tests/Allure.TestingPlatform.Tests/AllureDataConsumerTests.cs new file mode 100644 index 00000000..f4edfb12 --- /dev/null +++ b/tests/Allure.TestingPlatform.Tests/AllureDataConsumerTests.cs @@ -0,0 +1,40 @@ +using System.Threading.Tasks; +using Allure.TestingPlatform; +using Microsoft.Testing.Platform.Extensions.Messages; +using NUnit.Framework; + +namespace Allure.TestingPlatform.Tests; + +[TestFixture] +public class AllureDataConsumerTests +{ + [Test] + public void DataTypesConsumed_includes_TestNodeUpdateMessage() + { + var consumer = new AllureDataConsumer(); + + Assert.That(consumer.DataTypesConsumed, Has.Member(typeof(TestNodeUpdateMessage))); + } + + [Test] + public async Task IsEnabledAsync_returns_true() + { + var consumer = new AllureDataConsumer(); + + Assert.That(await consumer.IsEnabledAsync(), Is.True); + } + + [Test] + public void Extension_metadata_is_populated() + { + var consumer = new AllureDataConsumer(); + + Assert.Multiple(() => + { + Assert.That(consumer.Uid, Is.EqualTo("Allure.TestingPlatform")); + Assert.That(consumer.DisplayName, Is.Not.Empty); + Assert.That(consumer.Description, Is.Not.Empty); + Assert.That(consumer.Version, Is.Not.Empty); + }); + } +}