diff --git a/src/PipeForge.Tests.NetCoreApp/PipeForge.Tests.NetCoreApp.csproj b/src/PipeForge.Tests.NetCoreApp/PipeForge.Tests.NetCoreApp.csproj
deleted file mode 100644
index 2f67084..0000000
--- a/src/PipeForge.Tests.NetCoreApp/PipeForge.Tests.NetCoreApp.csproj
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
- netcoreapp3.1
- latest
- enable
- enable
- false
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/PipeForge.Tests.NetCoreApp/PipelineRunner/DescribeTests.cs b/src/PipeForge.Tests.NetCoreApp/PipelineRunner/DescribeTests.cs
deleted file mode 100644
index f34f76e..0000000
--- a/src/PipeForge.Tests.NetCoreApp/PipelineRunner/DescribeTests.cs
+++ /dev/null
@@ -1,55 +0,0 @@
-using PipeForge.Tests.NetCoreApp.TestUtils;
-
-namespace PipeForge.Tests.NetCoreApp.PipelineRunner;
-
-public class DescribeTests
-{
- private class NamedStep : IPipelineStep
- {
- public string Name { get; }
- public string? Description { get; }
- public bool MayShortCircuit { get; }
- public string? ShortCircuitCondition { get; }
-
- public NamedStep(string name, string? description = null, bool shortCircuiting = false, string? condition = null)
- {
- Name = name;
- Description = description;
- MayShortCircuit = shortCircuiting;
- ShortCircuitCondition = condition;
- }
-
- public Task InvokeAsync(TestContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- {
- return next(context, cancellationToken);
- }
- }
-
- [Fact]
- public void Describe_ReturnsStepDescriptions_InOrderAndWithMetadata()
- {
- // Arrange
- var steps = new List>>
- {
- new(() => new NamedStep("StepA", "First step")),
- new(() => new NamedStep("StepB", "Second step", shortCircuiting: true, "stop if canceled")),
- new(() => new NamedStep("StepC", "Third step"))
- };
-
- var runner = new PipelineRunner(steps);
-
- // Act
- var json = runner.Describe();
-
- // Assert
- json.ShouldContain("\"Name\": \"StepA\"");
- json.ShouldContain("\"Name\": \"StepB\"");
- json.ShouldContain("\"Name\": \"StepC\"");
-
- json.ShouldContain("\"MayShortCircuit\": true");
- json.ShouldContain("\"ShortCircuitCondition\": \"stop if canceled\"");
-
- json.IndexOf("StepA").ShouldBeLessThan(json.IndexOf("StepB"));
- json.IndexOf("StepB").ShouldBeLessThan(json.IndexOf("StepC"));
- }
-}
diff --git a/src/PipeForge.Tests.NetCoreApp/PipelineRunner/DiagnosticTests.cs b/src/PipeForge.Tests.NetCoreApp/PipelineRunner/DiagnosticTests.cs
deleted file mode 100644
index 7889cf1..0000000
--- a/src/PipeForge.Tests.NetCoreApp/PipelineRunner/DiagnosticTests.cs
+++ /dev/null
@@ -1,174 +0,0 @@
-using System.Diagnostics;
-using Microsoft.Extensions.Logging.Abstractions;
-
-namespace PipeForge.Tests.PipelineRunner;
-
-public class DiagnosticTests
-{
- private class TestContext { }
-
- private class TestStep : IPipelineStep
- {
- public string Name => "TestStep";
- public string Description => "Test Step Description";
- public bool MayShortCircuit => false;
- public string? ShortCircuitCondition => null;
-
- public Task InvokeAsync(TestContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- {
- return next(context, cancellationToken);
- }
- }
-
- private class ShortCircuitStep : IPipelineStep
- {
- public string Name => "ShortCircuit";
- public string Description => "Short-circuits the pipeline";
- public bool MayShortCircuit => true;
- public string? ShortCircuitCondition => "Always";
-
- public Task InvokeAsync(TestContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- {
- return Task.CompletedTask; // skips `next`
- }
- }
-
- private class FailingStep : IPipelineStep
- {
- public string Name => "Exploder";
- public string Description => "Throws for testing";
- public bool MayShortCircuit => false;
- public string? ShortCircuitCondition => null;
-
- public Task InvokeAsync(TestContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- {
- throw new InvalidOperationException("Kaboom");
- }
- }
-
- [Fact]
- public async Task EmitsStartAndStopEvents()
- {
- var events = new List<(string, object)>();
- var expectedListenerName = $"PipeForge.PipelineRunner<{typeof(TestContext).Name}>";
-
- using var allListenerSubscription = DiagnosticListener.AllListeners.Subscribe(
- new FilteringListener(expectedListenerName,
- (name, payload) => events.Add((name, payload)),
- (name, _, _) => name == "PipelineStep" || name == "PipelineStep.Start" || name == "PipelineStep.Stop"));
-
- var runner = new PipelineRunner(
- new[] { new Lazy>(() => new TestStep()) },
- new NullLoggerFactory());
-
- await runner.ExecuteAsync(new TestContext());
-
- events.Count.ShouldBe(2);
- events[0].Item1.ShouldBe("PipelineStep.Start");
- events[1].Item1.ShouldBe("PipelineStep.Stop");
-
- (events[0].Item2.ToString() ?? string.Empty).ShouldContain("TestStep");
- (events[1].Item2.ToString() ?? string.Empty).ShouldContain("TestStep");
- }
-
- [Fact]
- public async Task EmitsShortCircuitMetadata()
- {
- var events = new List<(string, object)>();
- var expectedListenerName = $"PipeForge.PipelineRunner<{typeof(TestContext).Name}>";
-
- using var allListenerSubscription = DiagnosticListener.AllListeners.Subscribe(
- new FilteringListener(expectedListenerName,
- (name, payload) => events.Add((name, payload)),
- (name, _, _) => name == "PipelineStep" || name == "PipelineStep.Start" || name == "PipelineStep.Stop"));
-
- var runner = new PipelineRunner(
- new[] { new Lazy>(() => new ShortCircuitStep()) },
- new NullLoggerFactory());
-
- await runner.ExecuteAsync(new TestContext());
-
- events.Count.ShouldBe(2);
- (events[1].Item2.ToString() ?? string.Empty).ShouldContain("short_circuited = True", Case.Insensitive);
- }
-
- [Fact]
- public async Task EmitsExceptionMetadata()
- {
- var events = new List<(string, object)>();
- var expectedListenerName = $"PipeForge.PipelineRunner<{typeof(TestContext).Name}>";
-
- using var allListenerSubscription = DiagnosticListener.AllListeners.Subscribe(
- new FilteringListener(expectedListenerName,
- (name, payload) =>
- {
- if (name == "PipelineStep.Exception")
- events.Add((name, payload));
- },
- (name, _, _) => name == "PipelineStep" || name == "PipelineStep.Exception"));
-
- var runner = new PipelineRunner(
- new[] { new Lazy>(() => new FailingStep()) },
- new NullLoggerFactory());
-
- var ex = await Should.ThrowAsync>(async () =>
- {
- await runner.ExecuteAsync(new TestContext());
- });
-
- events.Count.ShouldBe(1);
- events[0].Item1.ShouldBe("PipelineStep.Exception");
- (events[0].Item2.ToString() ?? string.Empty).ShouldContain("Kaboom");
- (events[0].Item2.ToString() ?? string.Empty).ShouldContain("Exploder");
- }
-
-
- private class FilteringListener : IObserver, IDisposable
- {
- private readonly string _targetName;
- private readonly Action _onNext;
- private readonly Func _isEnabled;
- private IDisposable? _innerSubscription;
-
- public FilteringListener(string targetName, Action onNext, Func isEnabled)
- {
- _targetName = targetName;
- _onNext = onNext;
- _isEnabled = isEnabled;
- }
-
- public void OnNext(DiagnosticListener value)
- {
- if (value.Name == _targetName)
- {
- _innerSubscription = value.Subscribe(new AnonymousObserver(_onNext), _isEnabled);
- }
- }
-
- public void OnCompleted() { }
-
- public void OnError(Exception error) { }
-
- public void Dispose()
- {
- _innerSubscription?.Dispose();
- }
- }
-
- private class AnonymousObserver : IObserver>
- {
- private readonly Action _onNext;
-
- public AnonymousObserver(Action onNext)
- {
- _onNext = onNext;
- }
-
- public void OnCompleted() { }
- public void OnError(Exception error) { }
- public void OnNext(KeyValuePair value)
- {
- _onNext(value.Key, value.Value);
- }
- }
-}
diff --git a/src/PipeForge.Tests.NetCoreApp/TestUtils/TestContext.cs b/src/PipeForge.Tests.NetCoreApp/TestUtils/TestContext.cs
deleted file mode 100644
index d037edc..0000000
--- a/src/PipeForge.Tests.NetCoreApp/TestUtils/TestContext.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace PipeForge.Tests.NetCoreApp.TestUtils;
-
-public class TestContext
-{
- public List ExecutedSteps { get; } = [];
-}
diff --git a/src/PipeForge.Tests.Steps/AssemblyInfo.cs b/src/PipeForge.Tests.Steps/AssemblyInfo.cs
deleted file mode 100644
index b5eb3c0..0000000
--- a/src/PipeForge.Tests.Steps/AssemblyInfo.cs
+++ /dev/null
@@ -1,2 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-[assembly: ExcludeFromCodeCoverage]
diff --git a/src/PipeForge.Tests.Steps/ISampleContextStep.cs b/src/PipeForge.Tests.Steps/ISampleContextStep.cs
new file mode 100644
index 0000000..fd0e99c
--- /dev/null
+++ b/src/PipeForge.Tests.Steps/ISampleContextStep.cs
@@ -0,0 +1,4 @@
+namespace PipeForge.Tests.Steps;
+
+public interface ISampleContextStep : IPipelineStep { }
+
diff --git a/src/PipeForge.Tests.Steps/PipeForge.Tests.Steps.csproj b/src/PipeForge.Tests.Steps/PipeForge.Tests.Steps.csproj
index 5d23286..953f11a 100644
--- a/src/PipeForge.Tests.Steps/PipeForge.Tests.Steps.csproj
+++ b/src/PipeForge.Tests.Steps/PipeForge.Tests.Steps.csproj
@@ -2,7 +2,8 @@
net8.0
- latest
+ 12.0
+ enable
enable
@@ -10,4 +11,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/PipeForge.Tests.Steps/SampleContext.cs b/src/PipeForge.Tests.Steps/SampleContext.cs
new file mode 100644
index 0000000..f77870e
--- /dev/null
+++ b/src/PipeForge.Tests.Steps/SampleContext.cs
@@ -0,0 +1,33 @@
+namespace PipeForge.Tests.Steps;
+
+///
+/// Sample context for testing pipeline steps.
+/// This context allows you to:
+///
+/// - Track pipeline progress via AddStep()
+/// - Print step execution history using ToString()
+/// - Assert how many steps ran using StepCount
+/// - Simulate errors by passing null or empty step names
+///
+///
+public class SampleContext
+{
+ public readonly List Steps = [];
+
+ public void AddStep(string stepName)
+ {
+ if (string.IsNullOrWhiteSpace(stepName))
+ {
+ throw new ArgumentException("Step name cannot be null or whitespace.", nameof(stepName));
+ }
+
+ Steps.Add(stepName);
+ }
+
+ public int StepCount => Steps.Count;
+
+ public override string ToString()
+ {
+ return string.Join(",", Steps);
+ }
+}
diff --git a/src/PipeForge.Tests.Steps/SampleContextStep.cs b/src/PipeForge.Tests.Steps/SampleContextStep.cs
new file mode 100644
index 0000000..63327bd
--- /dev/null
+++ b/src/PipeForge.Tests.Steps/SampleContextStep.cs
@@ -0,0 +1,19 @@
+
+namespace PipeForge.Tests.Steps;
+
+public abstract class SampleContextStep : ISampleContextStep
+{
+ public string? Description { get; protected set; } = "Sample Description";
+
+ public bool MayShortCircuit { get; protected set; } = false;
+
+ public string Name { get; protected set; } = "Sample Name";
+
+ public string? ShortCircuitCondition { get; protected set; } = null;
+
+ public virtual Task InvokeAsync(SampleContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
+ {
+ context.AddStep(Name);
+ return next(context, cancellationToken);
+ }
+}
diff --git a/src/PipeForge.Tests.Steps/SampleContextStepA.cs b/src/PipeForge.Tests.Steps/SampleContextStepA.cs
new file mode 100644
index 0000000..48c8faf
--- /dev/null
+++ b/src/PipeForge.Tests.Steps/SampleContextStepA.cs
@@ -0,0 +1,12 @@
+namespace PipeForge.Tests.Steps;
+
+[PipelineStep(1)]
+public class SampleContextStepA : SampleContextStep
+{
+ public static readonly string StepName = "A";
+
+ public SampleContextStepA()
+ {
+ Name = StepName;
+ }
+}
diff --git a/src/PipeForge.Tests.Steps/StepA1.cs b/src/PipeForge.Tests.Steps/StepA1.cs
deleted file mode 100644
index 0d58946..0000000
--- a/src/PipeForge.Tests.Steps/StepA1.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace PipeForge.Tests.Steps;
-
-[PipelineStep(1, "1")]
-public class StepA1 : PipelineStep
-{
- public StepA1()
- {
- Name = "A1";
- }
-
- public override async Task InvokeAsync(StepContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- {
- context.AddStep(Name);
- await next(context, cancellationToken);
- }
-}
diff --git a/src/PipeForge.Tests.Steps/StepA2.cs b/src/PipeForge.Tests.Steps/StepA2.cs
deleted file mode 100644
index 29e61be..0000000
--- a/src/PipeForge.Tests.Steps/StepA2.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace PipeForge.Tests.Steps;
-
-[PipelineStep(1, "2")]
-public class StepA2 : PipelineStep
-{
- public StepA2()
- {
- Name = "A2";
- }
-
- public override async Task InvokeAsync(StepContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- {
- context.AddStep(Name);
- await next(context, cancellationToken);
- }
-}
diff --git a/src/PipeForge.Tests.Steps/StepB1.cs b/src/PipeForge.Tests.Steps/StepB1.cs
deleted file mode 100644
index cb400d6..0000000
--- a/src/PipeForge.Tests.Steps/StepB1.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace PipeForge.Tests.Steps;
-
-[PipelineStep(2, "1")]
-public class StepB1 : PipelineStep
-{
- public StepB1()
- {
- Name = "B1";
- }
-
- public override async Task InvokeAsync(StepContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- {
- context.AddStep(Name);
- await next(context, cancellationToken);
- }
-}
diff --git a/src/PipeForge.Tests.Steps/StepB2.cs b/src/PipeForge.Tests.Steps/StepB2.cs
deleted file mode 100644
index 3001a38..0000000
--- a/src/PipeForge.Tests.Steps/StepB2.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace PipeForge.Tests.Steps;
-
-[PipelineStep(3, "2")]
-public class StepB2 : PipelineStep
-{
- public StepB2()
- {
- Name = "B2";
- }
-
- public override async Task InvokeAsync(StepContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- {
- context.AddStep(Name);
- await next(context, cancellationToken);
- }
-}
diff --git a/src/PipeForge.Tests.Steps/StepC1.cs b/src/PipeForge.Tests.Steps/StepC1.cs
deleted file mode 100644
index 54ac12c..0000000
--- a/src/PipeForge.Tests.Steps/StepC1.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace PipeForge.Tests.Steps;
-
-[PipelineStep(3, "1")]
-public class StepC1 : PipelineStep
-{
- public StepC1()
- {
- Name = "C1";
- }
-
- public override async Task InvokeAsync(StepContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- {
- context.AddStep(Name);
- await next(context, cancellationToken);
- }
-}
diff --git a/src/PipeForge.Tests.Steps/StepC2.cs b/src/PipeForge.Tests.Steps/StepC2.cs
deleted file mode 100644
index 506e362..0000000
--- a/src/PipeForge.Tests.Steps/StepC2.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace PipeForge.Tests.Steps;
-
-[PipelineStep(2, "2")]
-public class StepC2 : PipelineStep
-{
- public StepC2()
- {
- Name = "C2";
- }
-
- public override async Task InvokeAsync(StepContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- {
- context.AddStep(Name);
- await next(context, cancellationToken);
- }
-}
diff --git a/src/PipeForge.Tests.Steps/StepContext.cs b/src/PipeForge.Tests.Steps/StepContext.cs
deleted file mode 100644
index dbbb04d..0000000
--- a/src/PipeForge.Tests.Steps/StepContext.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace PipeForge.Tests.Steps;
-
-public class StepContext
-{
- private readonly List _steps = new();
-
- public void AddStep(string stepName)
- {
- if (string.IsNullOrWhiteSpace(stepName))
- {
- throw new ArgumentException("Step name cannot be null or whitespace.", nameof(stepName));
- }
-
- _steps.Add(stepName);
- }
-
- public int StepCount => _steps.Count;
-
- public override string ToString()
- {
- return string.Join(",", _steps);
- }
-}
diff --git a/src/PipeForge.Tests.Steps/StepD.cs b/src/PipeForge.Tests.Steps/StepD.cs
deleted file mode 100644
index 8d9b3ed..0000000
--- a/src/PipeForge.Tests.Steps/StepD.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace PipeForge.Tests.Steps;
-
-[PipelineStep(5)]
-public class StepD : PipelineStep
-{
- public StepD()
- {
- Name = "D0";
- }
-
- public override async Task InvokeAsync(StepContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- {
- context.AddStep(Name);
- await next(context, cancellationToken);
- }
-}
diff --git a/src/PipeForge.Tests.Steps/StepF.cs b/src/PipeForge.Tests.Steps/StepF.cs
deleted file mode 100644
index a1a232f..0000000
--- a/src/PipeForge.Tests.Steps/StepF.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace PipeForge.Tests.Steps;
-
-public class StepF : PipelineStep
-{
- public StepF()
- {
- Name = "FX";
- }
-
- public override async Task InvokeAsync(StepContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- {
- context.AddStep(Name);
- await next(context, cancellationToken);
- }
-}
diff --git a/src/PipeForge.Tests/Adapters/Diagnostics/ActivitySourceProviderTests.cs b/src/PipeForge.Tests/Adapters/Diagnostics/ActivitySourceProviderTests.cs
new file mode 100644
index 0000000..548b839
--- /dev/null
+++ b/src/PipeForge.Tests/Adapters/Diagnostics/ActivitySourceProviderTests.cs
@@ -0,0 +1,104 @@
+using System.Diagnostics;
+using PipeForge.Adapters.Diagnostics;
+using PipeForge.Tests.Steps;
+
+namespace PipeForge.Tests.Adapters.Diagnostics;
+
+public class ActivitySourceProviderTests
+{
+ private static ActivityListener SetupActivityListener(string sourceName, List activities)
+ {
+ var listener = new ActivityListener
+ {
+ ShouldListenTo = source => source.Name == sourceName,
+ Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded,
+ ActivityStarted = activity => activities.Add(activity),
+ ActivityStopped = _ => { }
+ };
+
+ ActivitySource.AddActivityListener(listener);
+ return listener;
+ }
+
+ [Fact]
+ public void BeginStep_StartsActivity_WithExpectedTags()
+ {
+ var activities = new List();
+ var expectedSource = $"PipeForge.PipelineRunner<{typeof(SampleContext).Name}>";
+
+ using var _ = SetupActivityListener(expectedSource, activities);
+
+ var provider = new ActivitySourceProvider();
+ var step = new SampleContextStepA();
+ using var scope = provider.BeginStep(step, 1);
+
+ activities.ShouldHaveSingleItem();
+ var activity = activities[0];
+
+ activity.DisplayName.ShouldBe("PipelineStep");
+ activity.GetTagItem("pipeline.context_type").ShouldBe(typeof(SampleContext).FullName);
+ activity.GetTagItem("pipeline.step_name").ShouldBe(step.Name);
+ activity.GetTagItem("pipeline.step_order").ShouldBe("1");
+ activity.GetTagItem("pipeline.step_description").ShouldBe(step.Description);
+ }
+
+ [Fact]
+ public void ReportException_SetsExpectedTags_OnCurrentActivity()
+ {
+ var activities = new List();
+ var expectedSource = $"PipeForge.PipelineRunner<{typeof(SampleContext).Name}>";
+
+ using var _ = SetupActivityListener(expectedSource, activities);
+
+ var provider = new ActivitySourceProvider();
+ var step = new SampleContextStepA();
+ using var scope = provider.BeginStep(step, 2);
+ var ex = new InvalidOperationException("oops");
+
+ provider.ReportException(ex, step, 2);
+
+ var activity = activities[0];
+ activity.GetTagItem("exception.type").ShouldBe(typeof(InvalidOperationException).FullName);
+ activity.GetTagItem("exception.message").ShouldBe("oops");
+ activity.GetTagItem("otel.status_code").ShouldBe("ERROR");
+ activity.GetTagItem("otel.status_description").ShouldBe("oops");
+
+ var stacktrace = activity.GetTagItem("exception.stacktrace") as string;
+ stacktrace.ShouldNotBeNull();
+ stacktrace.ShouldContain("InvalidOperationException");
+ }
+
+ [Fact]
+ public void Scope_SetCanceled_SetsTag()
+ {
+ var activities = new List();
+ var expectedSource = $"PipeForge.PipelineRunner<{typeof(SampleContext).Name}>";
+
+ using var _ = SetupActivityListener(expectedSource, activities);
+
+ var provider = new ActivitySourceProvider();
+ var step = new SampleContextStepA();
+ using var scope = provider.BeginStep(step, 3);
+
+ scope.SetCanceled();
+
+ activities[0].GetTagItem("pipeline.cancelled").ShouldBe(true);
+ }
+
+ [Fact]
+ public void Scope_SetShortCircuited_SetsTag()
+ {
+ var activities = new List();
+ var expectedSource = $"PipeForge.PipelineRunner<{typeof(SampleContext).Name}>";
+
+ using var _ = SetupActivityListener(expectedSource, activities);
+
+ var provider = new ActivitySourceProvider();
+ var step = new SampleContextStepA();
+ using var scope = provider.BeginStep(step, 4);
+
+ scope.SetShortCircuited(true);
+
+ activities[0].GetTagItem("pipeline.short_circuited").ShouldBe("True");
+ }
+}
diff --git a/src/PipeForge.Tests/CompositionExtensionTests.cs b/src/PipeForge.Tests/CompositionExtensionTests.cs
deleted file mode 100644
index 1d8d795..0000000
--- a/src/PipeForge.Tests/CompositionExtensionTests.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-using PipeForge.Tests.Steps;
-using PipeForge.Tests.TestUtils;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-
-namespace PipeForge.Tests;
-
-public class CompositionExtensionTests
-{
- [Fact]
- public async Task AddPipelineFor_RegistersAllSteps()
- {
- var context = new StepContext();
- var services = new ServiceCollection();
-
- services.AddScoped();
- services.AddPipelineFor("1");
-
- var provider = services.BuildServiceProvider();
-
- var loggerFactory = provider.GetRequiredService();
- loggerFactory.ShouldNotBeNull();
-
- var logger = loggerFactory.CreateLogger();
- logger.ShouldNotBeNull();
-
- var runner = provider.GetRequiredService>();
- runner.ShouldNotBeNull();
-
- await runner.ExecuteAsync(context);
- var result = context.ToString();
- result.ShouldBe("A1,B1,C1,D0");
- }
-
- [Fact]
- public async Task AddPipelineStep_RegistersStepForPipelineRunnerUsage()
- {
- var context = new StepContext();
- var services = new ServiceCollection();
-
- services.AddScoped();
- services.AddPipelineStep();
- services.AddTransient, PipelineRunner>();
-
- var provider = services.BuildServiceProvider();
- var loggerFactory = provider.GetRequiredService();
- loggerFactory.ShouldNotBeNull();
-
- var logger = loggerFactory.CreateLogger();
- logger.ShouldNotBeNull();
-
- var runner = provider.GetRequiredService>();
- runner.ShouldNotBeNull();
-
- await runner.ExecuteAsync(context);
- var result = context.ToString();
- result.ShouldBe("FX");
- }
-
- [Fact]
- public void AddPipelineStep_ThrowsException_WhenStepDoesNotImplementClosedGenericInterface()
- {
- var typeName = typeof(NotAPipelineStep).FullName ?? typeof(NotAPipelineStep).Name;
- var services = new ServiceCollection();
-
- var exception = Should.Throw(() => services.AddPipelineStep());
-
- exception.Message.ShouldBe(string.Format(CompositionExtensions.ArgumentExceptionMessage, typeName));
- }
-
- [Fact]
- public void AddPipelineStep_ThrowsException_WhenDuplicateStepIsRegistered()
- {
- var services = new ServiceCollection();
- services.AddPipelineStep();
-
- var typeName = typeof(StepF).FullName ?? typeof(StepF).Name;
- var exception = Should.Throw(() => services.AddPipelineStep());
-
- exception.Message.ShouldBe(string.Format(CompositionExtensions.InvalidOperationExceptionMessage, typeName));
- }
-
- private class NotAPipelineStep : IPipelineStep { }
-
- private class NotAContextWithSteps { }
-}
diff --git a/src/PipeForge.Tests/Metadata/PipelineStepDescriptorTests.cs b/src/PipeForge.Tests/Metadata/PipelineStepDescriptorTests.cs
deleted file mode 100644
index cec3f6f..0000000
--- a/src/PipeForge.Tests/Metadata/PipelineStepDescriptorTests.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using PipeForge.Metadata;
-using PipeForge.Tests.TestUtils;
-
-namespace PipeForge.Tests.Metadata;
-
-public class PipelineStepDescriptorTests
-{
- [PipelineStep(42, "QA")]
- private class AnnotatedStep : IPipelineStep
- {
- public string Name => "Test";
- public string Description => "";
- public bool MayShortCircuit => false;
- public string? ShortCircuitCondition => null;
- public Task InvokeAsync(TestContext context, PipelineDelegate next, CancellationToken cancellationToken = default) => Task.CompletedTask;
- }
-
- private class UnannotatedStep : IPipelineStep
- {
- public string Name => "NoAttr";
- public string Description => "";
- public bool MayShortCircuit => false;
- public string? ShortCircuitCondition => null;
- public Task InvokeAsync(TestContext context, PipelineDelegate next, CancellationToken cancellationToken = default) => Task.CompletedTask;
- }
-
- [Fact]
- public void Constructor_Extracts_Metadata_From_Attribute()
- {
- var descriptor = new PipelineStepDescriptor(typeof(AnnotatedStep));
-
- descriptor.ImplementationType.ShouldBe(typeof(AnnotatedStep));
- descriptor.Order.ShouldBe(42);
- descriptor.Filter.ShouldBe("QA");
- }
-
- [Fact]
- public void Constructor_Throws_If_Type_Missing_Attribute()
- {
- var ex = Should.Throw(() =>
- {
- _ = new PipelineStepDescriptor(typeof(UnannotatedStep));
- });
-
- ex.Message.ShouldContain(nameof(UnannotatedStep));
- }
-}
diff --git a/src/PipeForge.Tests/PipeForge.Tests.csproj b/src/PipeForge.Tests/PipeForge.Tests.csproj
index bba44b5..b7890b4 100644
--- a/src/PipeForge.Tests/PipeForge.Tests.csproj
+++ b/src/PipeForge.Tests/PipeForge.Tests.csproj
@@ -2,29 +2,22 @@
net8.0
- latest
enable
enable
+
false
true
- true
-
-
-
+
+
+
+
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
+
+
@@ -38,4 +31,4 @@
-
\ No newline at end of file
+
diff --git a/src/PipeForge.Tests/PipelineBuilderTests.cs b/src/PipeForge.Tests/PipelineBuilderTests.cs
deleted file mode 100644
index fb1d4f4..0000000
--- a/src/PipeForge.Tests/PipelineBuilderTests.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-using PipeForge.Tests.TestUtils;
-
-namespace PipeForge.Tests;
-
-public class PipelineBuilderTests
-{
- [Fact]
- public async Task PipelineBuilder_CreatesAndRunsPipeline_WithoutLoggerFactory()
- {
- var pipeline = Pipeline.CreateFor()
- .WithStep()
- .WithStep()
- .WithStep()
- .Build();
-
- var context = new TestContext();
- await pipeline.ExecuteAsync(context);
-
- string.Join("", context.ExecutedSteps).ShouldBe("ABC");
- }
-
- [Fact]
- public async Task PipelineBuilder_CreatesAndRunsPipeline_WithLoggerFactory()
- {
- var loggerFactory = new TestLoggerProvider();
- var pipeline = Pipeline.CreateFor(loggerFactory)
- .WithStep()
- .WithStep()
- .WithStep()
- .Build();
-
- var context = new TestContext();
- await pipeline.ExecuteAsync(context);
-
- string.Join("", context.ExecutedSteps).ShouldBe("ABC");
-
- var logger = loggerFactory.CreateLogger("something");
- logger.ShouldNotBeNull();
- var testLogger = logger as TestLogger;
- testLogger.ShouldNotBeNull();
-
- testLogger.LogEntries.Count.ShouldBe(6);
- }
-
- [Fact]
- public async Task PipelineBuilder_CreatesAndRunsPipeline_UsingStepFactory()
- {
- var expected = Guid.NewGuid().ToString();
- var pipeline = Pipeline.CreateFor()
- .WithStep()
- .WithStep()
- .WithStep()
- .WithStep(() => new StepD(expected))
- .Build();
-
- var context = new TestContext();
- await pipeline.ExecuteAsync(context);
-
- string.Join("", context.ExecutedSteps).ShouldBe($"ABC{expected}");
- }
-
- private abstract class TestStep : PipelineStep
- {
- public override async Task InvokeAsync(TestContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- {
- context.ExecutedSteps.Add(Name);
- await next(context, cancellationToken);
- }
- }
-
- private class StepA : TestStep
- {
- public StepA() => Name = "A";
- }
-
- private class StepB : TestStep
- {
- public StepB() => Name = "B";
- }
-
- private class StepC : TestStep
- {
- public StepC() => Name = "C";
- }
-
- private class StepD : TestStep
- {
- public StepD(string name) => Name = name;
- }
-}
diff --git a/src/PipeForge.Tests/PipelineRunner/ContextHandlingTests.cs b/src/PipeForge.Tests/PipelineRunner/ContextHandlingTests.cs
deleted file mode 100644
index 9ddbc02..0000000
--- a/src/PipeForge.Tests/PipelineRunner/ContextHandlingTests.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using PipeForge.Tests.TestUtils;
-
-namespace PipeForge.Tests.PipelineRunner;
-
-public class ContextHandlingTests
-{
- private class TrackingStep : IPipelineStep
- {
- public string Name => "Tracking";
- public string? Description => null;
- public bool MayShortCircuit => false;
- public string? ShortCircuitCondition => null;
-
- public TestContext? InvokedWith { get; private set; }
-
- public Task InvokeAsync(TestContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- {
- InvokedWith = context;
- return next(context, cancellationToken);
- }
- }
-
- [Fact]
- public async Task NullContext_ThrowsArgumentNullException()
- {
- var runner = new PipelineRunner(Enumerable.Empty>>());
-
- var ex = await Should.ThrowAsync(() => runner.ExecuteAsync(null!));
-
- ex.ParamName.ShouldBe("context");
- }
-
- [Fact]
- public async Task ValidContext_IsPassedToSteps()
- {
- var context = new TestContext();
- var steps = new List
- {
- new TrackingStep(),
- new TrackingStep()
- };
-
- var lazySteps = steps
- .Select(step => new Lazy>(() => step))
- .ToList();
-
- var runner = new PipelineRunner(lazySteps);
-
- await runner.ExecuteAsync(context);
-
- foreach (var step in steps)
- {
- step.InvokedWith.ShouldBe(context);
- }
- }
-}
diff --git a/src/PipeForge.Tests/PipelineRunner/DescribeTests.cs b/src/PipeForge.Tests/PipelineRunner/DescribeTests.cs
deleted file mode 100644
index c5ea47b..0000000
--- a/src/PipeForge.Tests/PipelineRunner/DescribeTests.cs
+++ /dev/null
@@ -1,84 +0,0 @@
-using System.Text.Json;
-using PipeForge.Tests.TestUtils;
-
-namespace PipeForge.Tests.PipelineRunner;
-
-public class DescribeTests
-{
- private class NamedStep : IPipelineStep
- {
- public string Name { get; }
- public string? Description { get; }
- public bool MayShortCircuit { get; }
- public string? ShortCircuitCondition { get; }
-
- public NamedStep(string name, string? description = null, bool shortCircuiting = false, string? condition = null)
- {
- Name = name;
- Description = description;
- MayShortCircuit = shortCircuiting;
- ShortCircuitCondition = condition;
- }
-
- public Task InvokeAsync(TestContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- {
- return next(context, cancellationToken);
- }
- }
-
- [Fact]
- public void Describe_ReturnsStepDescriptions_InOrderAndWithMetadata()
- {
- // Arrange
- var steps = new List>>
- {
- new(() => new NamedStep("StepA", "First step")),
- new(() => new NamedStep("StepB", "Second step", shortCircuiting: true, "stop if canceled")),
- new(() => new NamedStep("StepC", "Third step"))
- };
-
- var runner = new PipelineRunner(steps);
-
- // Act
- var json = runner.Describe();
-
- // Assert
- json.ShouldContain("\"Name\": \"StepA\"");
- json.ShouldContain("\"Name\": \"StepB\"");
- json.ShouldContain("\"Name\": \"StepC\"");
-
- json.ShouldContain("\"MayShortCircuit\": true");
- json.ShouldContain("\"ShortCircuitCondition\": \"stop if canceled\"");
-
- json.IndexOf("StepA").ShouldBeLessThan(json.IndexOf("StepB"));
- json.IndexOf("StepB").ShouldBeLessThan(json.IndexOf("StepC"));
- }
-
- [Fact]
- public void DescribeSchema_ReturnsValidJsonSchema()
- {
- // Arrange
- var runner = new PipelineRunner(Enumerable.Empty>>());
-
- // Act
- var schema = runner.DescribeSchema();
-
- // Assert
- var document = JsonDocument.Parse(schema);
- var root = document.RootElement;
-
- root.GetProperty("$schema").GetString().ShouldBe("http://json-schema.org/draft-07/schema#");
- root.GetProperty("title").GetString().ShouldBe("PipelineStep");
- root.GetProperty("type").GetString().ShouldBe("object");
-
- var properties = root.GetProperty("properties");
- properties.TryGetProperty("Order", out _).ShouldBeTrue();
- properties.TryGetProperty("Name", out _).ShouldBeTrue();
- properties.TryGetProperty("MayShortCircuit", out _).ShouldBeTrue();
-
- var required = root.GetProperty("required").EnumerateArray().Select(e => e.GetString()).ToArray();
- required.ShouldContain("Order");
- required.ShouldContain("Name");
- required.ShouldContain("MayShortCircuit");
- }
-}
diff --git a/src/PipeForge.Tests/PipelineRunner/DiagnosticsTests.cs b/src/PipeForge.Tests/PipelineRunner/DiagnosticsTests.cs
deleted file mode 100644
index 0ceb276..0000000
--- a/src/PipeForge.Tests/PipelineRunner/DiagnosticsTests.cs
+++ /dev/null
@@ -1,138 +0,0 @@
-using System.Diagnostics;
-using PipeForge.Tests.TestUtils;
-using Microsoft.Extensions.Logging.Abstractions;
-
-namespace PipeForge.Tests.PipelineRunner;
-
-[Collection("DiagnosticTests")]
-public class DiagnosticTests
-{
- private class TestStep : IPipelineStep
- {
- public string Name => "TestStep";
- public string Description => "A step for testing";
- public bool MayShortCircuit => false;
- public string? ShortCircuitCondition => null;
-
- public Task InvokeAsync(TestContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- {
- return next(context, cancellationToken);
- }
- }
-
- private class ShortCircuitingStep : IPipelineStep
- {
- public string Name => "ShortCircuitingStep";
- public string Description => "Stops the pipeline";
- public bool MayShortCircuit => true;
- public string? ShortCircuitCondition => "Always stops";
-
- public Task InvokeAsync(TestContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- {
- return Task.CompletedTask;
- }
- }
-
- private class ExceptionStep : IPipelineStep
- {
- public string Name => "ExceptionStep";
- public string Description => "Throws an error";
- public bool MayShortCircuit => false;
- public string? ShortCircuitCondition => null;
-
- public Task InvokeAsync(TestContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- {
- throw new InvalidOperationException("Boom");
- }
- }
-
- [Fact]
- public async Task StartsAndTagsActivityCorrectly()
- {
- var activities = new List();
- var expectedSource = $"PipeForge.PipelineRunner<{typeof(TestContext).Name}>";
-
- using var listener = new ActivityListener
- {
- ShouldListenTo = source => source.Name == expectedSource,
- Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded,
- ActivityStarted = activity => activities.Add(activity),
- ActivityStopped = _ => { }
- };
-
- ActivitySource.AddActivityListener(listener);
-
- var runner = new PipelineRunner(
- new[] { new Lazy>(() => new TestStep()) },
- NullLoggerFactory.Instance);
-
- await runner.ExecuteAsync(new TestContext());
-
- var activity = activities.SingleOrDefault(a => a.DisplayName == "PipelineStep");
- activity.ShouldNotBeNull();
- activity.Tags.ShouldContain(t => t.Key == "pipeline.step_name" && t.Value == "TestStep");
- activity.Tags.ShouldContain(t => t.Key == "pipeline.short_circuited" && t.Value == "False");
- }
-
- [Fact]
- public async Task RecordsShortCircuitInActivityTag()
- {
- var activities = new List();
- var expectedSource = $"PipeForge.PipelineRunner<{typeof(TestContext).Name}>";
-
- using var listener = new ActivityListener
- {
- ShouldListenTo = source => source.Name == expectedSource,
- Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded,
- ActivityStarted = activity => activities.Add(activity),
- ActivityStopped = _ => { }
- };
-
- ActivitySource.AddActivityListener(listener);
-
- var runner = new PipelineRunner(
- new[] { new Lazy>(() => new ShortCircuitingStep()) },
- NullLoggerFactory.Instance);
-
- await runner.ExecuteAsync(new TestContext());
-
- var activity = activities.SingleOrDefault(a => a.DisplayName == "PipelineStep");
- activity.ShouldNotBeNull();
- activity.Tags.ShouldContain(t => t.Key == "pipeline.step_name" && t.Value == "ShortCircuitingStep");
- activity.Tags.ShouldContain(t => t.Key == "pipeline.short_circuited" && t.Value == "True");
- }
-
- [Fact]
- public async Task RecordsExceptionDetailsInActivityTags()
- {
- var activities = new List();
- var expectedSource = $"PipeForge.PipelineRunner<{typeof(TestContext).Name}>";
-
- using var listener = new ActivityListener
- {
- ShouldListenTo = source => source.Name == expectedSource,
- Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllDataAndRecorded,
- ActivityStarted = activity => activities.Add(activity),
- ActivityStopped = _ => { }
- };
-
- ActivitySource.AddActivityListener(listener);
-
- var runner = new PipelineRunner(
- new[] { new Lazy>(() => new ExceptionStep()) },
- NullLoggerFactory.Instance);
-
- var ex = await Should.ThrowAsync>(async () =>
- {
- await runner.ExecuteAsync(new TestContext());
- });
-
- ex.StepName.ShouldBe("ExceptionStep");
- ex.StepOrder.ShouldBe(0);
-
- var activity = activities.SingleOrDefault(a => a.DisplayName == "PipelineStep");
- activity.ShouldNotBeNull();
- activity.Tags.ShouldContain(t => t.Key == "exception.type" && t.Value == typeof(InvalidOperationException).FullName);
- activity.Tags.ShouldContain(t => t.Key == "otel.status_code" && t.Value == "ERROR");
- }
-}
diff --git a/src/PipeForge.Tests/PipelineRunner/ExceptionHandlingTests.cs b/src/PipeForge.Tests/PipelineRunner/ExceptionHandlingTests.cs
deleted file mode 100644
index 1b02dea..0000000
--- a/src/PipeForge.Tests/PipelineRunner/ExceptionHandlingTests.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-using PipeForge.Tests.TestUtils;
-
-namespace PipeForge.Tests.PipelineRunner;
-
-public class ExceptionHandlingTests
-{
- private class ThrowingStep : IPipelineStep
- {
- public string Name { get; }
- public string? Description => "Always throws";
- public bool MayShortCircuit => false;
- public string? ShortCircuitCondition => null;
-
- public ThrowingStep(string name) => Name = name;
-
- public Task InvokeAsync(TestContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- {
- throw new InvalidOperationException("Step failed.");
- }
- }
-
- private class PreWrappedExceptionStep : IPipelineStep
- {
- public string Name { get; }
- public string? Description => "Throws pre-wrapped";
- public bool MayShortCircuit => false;
- public string? ShortCircuitCondition => null;
-
- public PreWrappedExceptionStep(string name) => Name = name;
-
- public Task InvokeAsync(TestContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- {
- throw new PipelineExecutionException(Name, 0, new Exception("Already wrapped."));
- }
- }
-
- [Fact]
- public async Task ExecuteAsync_ThrowsWrappedException_WhenStepThrows()
- {
- // Arrange
- var steps = new List>>
- {
- new(() => new ThrowingStep("FailingStep"))
- };
-
- var runner = new PipelineRunner(steps);
-
- // Act
- var ex = await Should.ThrowAsync>(
- () => runner.ExecuteAsync(new TestContext()));
-
- // Assert
- ex.StepName.ShouldBe("FailingStep");
- ex.StepOrder.ShouldBe(0);
- ex.InnerException.ShouldBeOfType();
- ex.InnerException?.Message.ShouldBe("Step failed.");
- }
-
- [Fact]
- public async Task ExecuteAsync_DoesNotWrap_PipelineExecutionException()
- {
- // Arrange
- var steps = new List>>
- {
- new(() => new PreWrappedExceptionStep("WrappedStep"))
- };
-
- var runner = new PipelineRunner(steps);
-
- // Act & Assert
- var ex = await Should.ThrowAsync>(
- () => runner.ExecuteAsync(new TestContext()));
-
- ex.StepName.ShouldBe("WrappedStep");
- ex.StepOrder.ShouldBe(0);
- ex.InnerException?.Message.ShouldBe("Already wrapped.");
- }
-}
diff --git a/src/PipeForge.Tests/PipelineRunner/ExecutionFlowTests.cs b/src/PipeForge.Tests/PipelineRunner/ExecutionFlowTests.cs
deleted file mode 100644
index 3b76de3..0000000
--- a/src/PipeForge.Tests/PipelineRunner/ExecutionFlowTests.cs
+++ /dev/null
@@ -1,92 +0,0 @@
-using PipeForge.Tests.TestUtils;
-
-namespace PipeForge.Tests.PipelineRunner;
-
-public class ExecutionFlowTests
-{
- private class StepA : IPipelineStep
- {
- public string Name => "StepA";
- public string? Description => null;
- public bool MayShortCircuit => false;
- public string? ShortCircuitCondition => null;
-
- public Task InvokeAsync(TestContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- {
- context.ExecutedSteps.Add("A");
- return next(context, cancellationToken);
- }
- }
-
- private class StepB : IPipelineStep
- {
- public string Name => "StepB";
- public string? Description => null;
- public bool MayShortCircuit => false;
- public string? ShortCircuitCondition => null;
-
- public Task InvokeAsync(TestContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- {
- context.ExecutedSteps.Add("B");
- return next(context, cancellationToken);
- }
- }
-
- private class StepShortCircuit : IPipelineStep
- {
- public string Name => "StepShortCircuit";
- public string? Description => null;
- public bool MayShortCircuit => true;
- public string? ShortCircuitCondition => "Always short-circuits";
-
- public Task InvokeAsync(TestContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- {
- context.ExecutedSteps.Add("ShortCircuit");
- return Task.CompletedTask;
- }
- }
-
- [Fact]
- public async Task Executes_All_Steps_In_Order()
- {
- var context = new TestContext();
- var steps = new List>>
- {
- new(() => new StepA()),
- new(() => new StepB())
- };
-
- var runner = new PipelineRunner(steps);
- await runner.ExecuteAsync(context);
-
- context.ExecutedSteps.ShouldBe(new[] { "A", "B" });
- }
-
- [Fact]
- public async Task Stops_Execution_When_ShortCircuiting()
- {
- var context = new TestContext();
- var steps = new List>>
- {
- new(() => new StepA()),
- new(() => new StepShortCircuit()),
- new(() => new StepB()) // should not run
- };
-
- var runner = new PipelineRunner(steps);
- await runner.ExecuteAsync(context);
-
- context.ExecutedSteps.ShouldBe(new[] { "A", "ShortCircuit" });
- }
-
- [Fact]
- public async Task Executes_No_Steps_When_Pipeline_Is_Empty()
- {
- var context = new TestContext();
- var runner = new PipelineRunner(Enumerable.Empty>>());
-
- await runner.ExecuteAsync(context);
-
- context.ExecutedSteps.ShouldBeEmpty();
- }
-}
diff --git a/src/PipeForge.Tests/PipelineRunner/LoggingTests.cs b/src/PipeForge.Tests/PipelineRunner/LoggingTests.cs
deleted file mode 100644
index 1563c83..0000000
--- a/src/PipeForge.Tests/PipelineRunner/LoggingTests.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-using Microsoft.Extensions.Logging;
-using PipeForge.Tests.TestUtils;
-
-namespace PipeForge.Tests.PipelineRunner;
-
-public class LoggingTests
-{
- private class TestStep : IPipelineStep
- {
- public string Name => "LogStep";
- public string Description => "Step that always runs";
- public bool MayShortCircuit => false;
- public string? ShortCircuitCondition => null;
-
- public Task InvokeAsync(TestContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- => next(context, cancellationToken);
- }
-
- private class ShortCircuitingStep : IPipelineStep
- {
- public string Name => "ShortCircuitingStep";
- public string Description => "Step that ends execution early";
- public bool MayShortCircuit => true;
- public string? ShortCircuitCondition => "Always";
-
- public Task InvokeAsync(TestContext context, PipelineDelegate next, CancellationToken cancellationToken = default)
- => Task.CompletedTask;
- }
-
- private static (PipelineRunner runner, TestLogger logger) CreateRunnerWithSteps(params IPipelineStep[] steps)
- {
- var provider = new TestLoggerProvider();
- var lazySteps = steps.Select(s => new Lazy>(() => s));
- var runner = new PipelineRunner(lazySteps, provider);
- return (runner, provider.Logger);
- }
-
- [Fact]
- public async Task Logs_Expected_Messages_During_Execution()
- {
- var (runner, logger) = CreateRunnerWithSteps(new TestStep());
-
- await runner.ExecuteAsync(new TestContext());
-
- logger.ShouldContainMessage("Executing pipeline step LogStep", LogLevel.Trace);
- logger.ShouldContainMessage("Completed pipeline step LogStep", LogLevel.Trace);
- }
-
- [Fact]
- public async Task Logs_ShortCircuit_Message_When_Step_Does_Not_Invoke_Next()
- {
- var (runner, logger) = CreateRunnerWithSteps(new ShortCircuitingStep());
-
- await runner.ExecuteAsync(new TestContext());
-
- logger.ShouldContainMessage("short-circuited", LogLevel.Information);
- }
-
- [Fact]
- public async Task Emits_Scope_For_Each_Step()
- {
- var (runner, logger) = CreateRunnerWithSteps(new TestStep());
-
- await runner.ExecuteAsync(new TestContext());
-
- logger.ShouldHaveScopeContaining("PipelineStepName", "LogStep");
- logger.ShouldHaveScopeContaining("PipelineContextType", nameof(TestContext));
- logger.ShouldHaveScopeContaining("PipelineStepOrder", 0);
- }
-}
diff --git a/src/PipeForge.Tests/PipelineStepAttributeTests.cs b/src/PipeForge.Tests/PipelineStepAttributeTests.cs
deleted file mode 100644
index e2da5e6..0000000
--- a/src/PipeForge.Tests/PipelineStepAttributeTests.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using System.Reflection;
-
-namespace PipeForge.Tests;
-
-public class PipelineStepAttributeTests
-{
- [Fact]
- public void Constructor_Assigns_Properties_Correctly()
- {
- var attr = new PipelineStepAttribute(order: 42, filter: "Staging");
-
- attr.Order.ShouldBe(42);
- attr.Filter.ShouldBe("Staging");
- }
-
- [Fact]
- public void Defaults_Filter_To_Null()
- {
- var attr = new PipelineStepAttribute(order: 1);
-
- attr.Filter.ShouldBeNull();
- }
-
- [Fact]
- public void Attribute_Can_Be_Applied_To_Class_Only()
- {
- var usage = typeof(PipelineStepAttribute)
- .GetCustomAttribute();
-
- usage.ShouldNotBeNull();
- usage.ValidOn.ShouldBe(AttributeTargets.Class);
- usage.Inherited.ShouldBeFalse();
- usage.AllowMultiple.ShouldBeFalse();
- }
-}
diff --git a/src/PipeForge.Tests/PipelineTests.cs b/src/PipeForge.Tests/PipelineTests.cs
deleted file mode 100644
index e2c0bbe..0000000
--- a/src/PipeForge.Tests/PipelineTests.cs
+++ /dev/null
@@ -1,161 +0,0 @@
-using System.Reflection;
-using PipeForge.Tests.Steps;
-using PipeForge.Tests.TestUtils;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-
-namespace PipeForge.Tests;
-
-public class PipelineTests
-{
- private static readonly Assembly StepAssembly = typeof(StepA1).Assembly;
-
- [Fact]
- public void Discover_ReturnsNoDescriptors_WhenNoStepsExist()
- {
- var typeName = typeof(NotAContextWithSteps).FullName ?? typeof(NotAContextWithSteps).Name;
- var logger = new TestLogger();
- var descriptors = Pipeline.Discover(logger);
-
- descriptors.ShouldNotBeNull();
- descriptors.ShouldBeEmpty();
-
- logger.LogEntries.Count.ShouldBe(1);
- logger.LogEntries[0].Message.ShouldBe(string.Format(Pipeline.NoStepsFoundMessage, typeName));
- }
-
- [Fact]
- public void Discover_ReturnsDescriptors_WithFilter()
- {
- var descriptors1 = Pipeline.Discover(StepAssembly, "1").ToList();
- var descriptors2 = Pipeline.Discover(typeof(StepF), "2").ToList();
-
- descriptors1.ShouldNotBeNull();
- descriptors1.ShouldNotBeEmpty();
- descriptors1.Count().ShouldBe(4);
-
- descriptors2.ShouldNotBeNull();
- descriptors2.ShouldNotBeEmpty();
- descriptors2.Count().ShouldBe(4);
-
- descriptors1[0].ImplementationType.ShouldBe(typeof(StepA1));
- descriptors1[1].ImplementationType.ShouldBe(typeof(StepB1));
- descriptors1[2].ImplementationType.ShouldBe(typeof(StepC1));
- descriptors1[3].ImplementationType.ShouldBe(typeof(StepD));
-
- descriptors2[0].ImplementationType.ShouldBe(typeof(StepA2));
- descriptors2[1].ImplementationType.ShouldBe(typeof(StepC2));
- descriptors2[2].ImplementationType.ShouldBe(typeof(StepB2));
- descriptors2[3].ImplementationType.ShouldBe(typeof(StepD));
- }
-
- [Fact]
- public void Discover_ReturnsSteps_WithoutFilter()
- {
- var descriptors = Pipeline.Discover();
-
- descriptors.ShouldNotBeNull();
- descriptors.ShouldNotBeEmpty();
- descriptors.Count().ShouldBe(1);
-
- descriptors.First().ImplementationType.ShouldBe(typeof(StepD));
- }
-
- [Fact]
- public void Discover_OutputsRelevantLogs()
- {
- var typeName = typeof(StepContext).FullName ?? typeof(StepContext).Name;
- var logger = new TestLogger();
- _ = Pipeline.Discover([StepAssembly], "1", logger);
-
- logger.LogEntries.ShouldNotBeEmpty();
- logger.LogEntries.Count.ShouldBe(5);
-
- logger.ShouldContainMessage(string.Format(Pipeline.NumberStepsFoundMessage, 4, typeName), LogLevel.Debug);
- logger.ShouldContainMessage(string.Format(Pipeline.StepDiscoveredMessage, "PipeForge.Tests.Steps.StepA1", 1, 1, true), LogLevel.Debug);
- logger.ShouldContainMessage(string.Format(Pipeline.StepDiscoveredMessage, "PipeForge.Tests.Steps.StepB1", 2, 1, true), LogLevel.Debug);
- logger.ShouldContainMessage(string.Format(Pipeline.StepDiscoveredMessage, "PipeForge.Tests.Steps.StepC1", 3, 1, true), LogLevel.Debug);
- logger.ShouldContainMessage(string.Format(Pipeline.StepDiscoveredMessage, "PipeForge.Tests.Steps.StepD", 5, "(none)", true), LogLevel.Debug);
- }
-
- [Fact]
- public void Register_DoesNotRegister_IfPipelineRunnerIsAlreadyRegistered()
- {
- var logger = new TestLogger();
- var typeName = typeof(StepContext).FullName ?? typeof(StepContext).Name;
-
- var services = new ServiceCollection();
- services.AddSingleton, PipelineRunner>();
-
- var descriptors = Pipeline.Discover("1");
- descriptors.ShouldNotBeEmpty();
-
- Pipeline.Register(services, descriptors, logger);
-
- logger.LogEntries.Count.ShouldBe(1);
- logger.ShouldContainMessage(string.Format(Pipeline.RunnerAlreadyRegisteredMessage, typeName), LogLevel.Debug);
-
- services.Any(s => s.ServiceType == typeof(IPipelineStep)).ShouldBeFalse();
- }
-
- [Theory]
- [InlineData(ServiceLifetime.Singleton)]
- [InlineData(ServiceLifetime.Scoped)]
- [InlineData(ServiceLifetime.Transient)]
- public void Register_RegistersStepsAndRunner(ServiceLifetime lifetime)
- {
- var logger = new TestLogger();
- var runnerTypeName = typeof(IPipelineRunner).FullName ?? typeof(IPipelineRunner).Name;
-
- var descriptors = Pipeline.Discover("1");
- descriptors.ShouldNotBeEmpty();
-
- var services = new ServiceCollection();
-
- Pipeline.Register(services, descriptors, lifetime, logger);
-
- var stepTypeA = typeof(StepA1).FullName ?? typeof(StepA1).Name;
- var stepTypeB = typeof(StepB1).FullName ?? typeof(StepB1).Name;
- var stepTypeC = typeof(StepC1).FullName ?? typeof(StepC1).Name;
- var stepTypeD = typeof(StepD).FullName ?? typeof(StepD).Name;
-
- logger.LogEntries.Count.ShouldBe(5);
- logger.ShouldContainMessage(string.Format(Pipeline.StepRegistrationMessage, stepTypeA, lifetime), LogLevel.Debug);
- logger.ShouldContainMessage(string.Format(Pipeline.StepRegistrationMessage, stepTypeB, lifetime), LogLevel.Debug);
- logger.ShouldContainMessage(string.Format(Pipeline.StepRegistrationMessage, stepTypeC, lifetime), LogLevel.Debug);
- logger.ShouldContainMessage(string.Format(Pipeline.StepRegistrationMessage, stepTypeD, lifetime), LogLevel.Debug);
- logger.ShouldContainMessage(string.Format(Pipeline.RunnerRegistrationMessage, runnerTypeName, lifetime), LogLevel.Debug);
- }
-
- [Fact]
- public void SafeGetTypes_ReturnsTypes_WhenNoException()
- {
- var expected = new[] { typeof(string), typeof(int) };
-
- var result = Pipeline.SafeGetTypes(() => expected);
-
- result.ShouldBe(expected);
- }
-
- [Fact]
- public void SafeGetTypes_ReturnsNonNullTypes_WhenReflectionTypeLoadException()
- {
- var expected = new[] { typeof(string), null, typeof(int) };
-
- var ex = new ReflectionTypeLoadException(expected, new Exception[0]);
-
- var result = Pipeline.SafeGetTypes(() => throw ex);
-
- result.ShouldBe(new[] { typeof(string), typeof(int) });
- }
-
- [Fact]
- public void SafeGetTypes_ReturnsEmpty_WhenUnknownException()
- {
- var result = Pipeline.SafeGetTypes(() => throw new InvalidOperationException());
-
- result.ShouldBeEmpty();
- }
-
- private class NotAContextWithSteps { }
-}
diff --git a/src/PipeForge.Tests/TestUtils/DiagnosticTestCollection.cs b/src/PipeForge.Tests/TestUtils/DiagnosticTestCollection.cs
deleted file mode 100644
index 98f1093..0000000
--- a/src/PipeForge.Tests/TestUtils/DiagnosticTestCollection.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace PipeForge.Tests.TestUtils;
-
-[CollectionDefinition("DiagnosticTests", DisableParallelization = true)]
-public class DiagnosticTestCollection : ICollectionFixture