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 -{ - // This class has no code, and is never created. Its purpose is just to attach the attribute. -} diff --git a/src/PipeForge.Tests/TestUtils/TestContext.cs b/src/PipeForge.Tests/TestUtils/TestContext.cs deleted file mode 100644 index ba036cf..0000000 --- a/src/PipeForge.Tests/TestUtils/TestContext.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace PipeForge.Tests.TestUtils; - -public class TestContext -{ - public List ExecutedSteps { get; } = []; -} diff --git a/src/PipeForge.Tests/TestUtils/TestLogEntry.cs b/src/PipeForge.Tests/TestUtils/TestLogEntry.cs deleted file mode 100644 index 7112145..0000000 --- a/src/PipeForge.Tests/TestUtils/TestLogEntry.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace PipeForge.Tests.TestUtils; - -public class TestLogEntry -{ - public LogLevel LogLevel { get; } - public EventId EventId { get; } - public Type StateType { get; } - public Exception? Exception { get; } - public string? Message { get; } - public object? State { get; } - - public TestLogEntry(LogLevel logLevel, EventId eventId, Type stateType, Exception? exception, string? message, object? state) - { - LogLevel = logLevel; - EventId = eventId; - State = state; - StateType = stateType; - Exception = exception; - Message = message; - } -} diff --git a/src/PipeForge.Tests/TestUtils/TestLogger.cs b/src/PipeForge.Tests/TestUtils/TestLogger.cs deleted file mode 100644 index 32273da..0000000 --- a/src/PipeForge.Tests/TestUtils/TestLogger.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace PipeForge.Tests.TestUtils; - -public class TestLogger : ILogger -{ - public List LogEntries { get; } = new(); - - public List Scopes { get; } = new(); - - public IDisposable? BeginScope(TState state) where TState : notnull - { - Scopes.Add(state); - return NullScope.Instance; - } - - public bool IsEnabled(LogLevel logLevel) - { - return true; // Always enabled for testing purposes - } - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) - { - var logEntry = new TestLogEntry(logLevel, eventId, typeof(TState), exception, formatter(state, exception), state); - LogEntries.Add(logEntry); - } - - private class NullScope : IDisposable - { - public static readonly NullScope Instance = new(); - public void Dispose() { } - } -} diff --git a/src/PipeForge.Tests/TestUtils/TestLoggerProvider.cs b/src/PipeForge.Tests/TestUtils/TestLoggerProvider.cs deleted file mode 100644 index 1d77786..0000000 --- a/src/PipeForge.Tests/TestUtils/TestLoggerProvider.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace PipeForge.Tests.TestUtils; - -public class TestLoggerProvider : ILoggerProvider, ILoggerFactory -{ - public readonly TestLogger Logger = new(); - - public void AddProvider(ILoggerProvider provider) - { - return; - } - - public ILogger CreateLogger(string categoryName) => Logger; - - public void Dispose() { } -} diff --git a/src/PipeForge.Tests/TestUtils/TestLoggingExtensions.cs b/src/PipeForge.Tests/TestUtils/TestLoggingExtensions.cs deleted file mode 100644 index 4223b31..0000000 --- a/src/PipeForge.Tests/TestUtils/TestLoggingExtensions.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace PipeForge.Tests.TestUtils; - -public static class TestLoggerExtensions -{ - public static TestLogEntry ShouldContainMessage(this TestLogger logger, string expectedMessage, LogLevel? level = null) - { - var matching = logger.LogEntries - .Where(e => e.Message?.Contains(expectedMessage) == true) - .ToList(); - - if (level.HasValue) - matching = matching.Where(e => e.LogLevel == level.Value).ToList(); - - matching.Count.ShouldBeGreaterThan(0, $"Expected to find log message containing \"{expectedMessage}\" at level {level?.ToString() ?? "(any)"}"); - - return matching.First(); - } - - public static TestLogEntry ShouldContainMessageMatching(this TestLogger logger, Func predicate, LogLevel? level = null) - { - var matching = logger.LogEntries - .Where(e => e.Message != null && predicate(e.Message)) - .ToList(); - - if (level.HasValue) - matching = matching.Where(e => e.LogLevel == level.Value).ToList(); - - matching.Count.ShouldBeGreaterThan(0, "Expected to find a log message matching predicate"); - - return matching.First(); - } - - public static void ShouldHaveLoggedError(this TestLogger logger, Type expectedExceptionType) - { - logger.LogEntries - .Any(e => e.LogLevel == LogLevel.Error && e.Exception?.GetType() == expectedExceptionType) - .ShouldBeTrue($"Expected an error log entry with exception type {expectedExceptionType.Name}"); - } - - public static object ShouldHaveScopeContaining(this TestLogger logger, string key, object? expectedValue) - { - var match = logger.Scopes - .OfType>>() - .FirstOrDefault(scope => scope.Any(kv => kv.Key == key && Equals(kv.Value, expectedValue))); - - match.ShouldNotBeNull($"Expected a logging scope containing key '{key}' with value '{expectedValue}'"); - - return match!; - } - - public static object ShouldHaveScopeMatching(this TestLogger logger, Func>, bool> predicate) - { - var match = logger.Scopes - .OfType>>() - .FirstOrDefault(predicate); - - match.ShouldNotBeNull("Expected a logging scope matching the specified predicate"); - - return match!; - } -} - diff --git a/src/PipeForge.sln b/src/PipeForge.sln index 3c77dc3..a295d8d 100644 --- a/src/PipeForge.sln +++ b/src/PipeForge.sln @@ -7,8 +7,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PipeForge", "PipeForge\Pipe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PipeForge.Tests", "PipeForge.Tests\PipeForge.Tests.csproj", "{A6A88EE6-9E4B-475E-9494-E760BC377178}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PipeForge.Tests.NetCoreApp", "PipeForge.Tests.NetCoreApp\PipeForge.Tests.NetCoreApp.csproj", "{36C877F0-0A53-45EA-BB0C-C41423A8F87B}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PipeForge.Tests.Steps", "PipeForge.Tests.Steps\PipeForge.Tests.Steps.csproj", "{A6B8AAC9-BE46-4A7B-BE75-AF0E95D04EC7}" EndProject Global @@ -45,18 +43,6 @@ Global {A6A88EE6-9E4B-475E-9494-E760BC377178}.Release|x64.Build.0 = Release|Any CPU {A6A88EE6-9E4B-475E-9494-E760BC377178}.Release|x86.ActiveCfg = Release|Any CPU {A6A88EE6-9E4B-475E-9494-E760BC377178}.Release|x86.Build.0 = Release|Any CPU - {36C877F0-0A53-45EA-BB0C-C41423A8F87B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {36C877F0-0A53-45EA-BB0C-C41423A8F87B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {36C877F0-0A53-45EA-BB0C-C41423A8F87B}.Debug|x64.ActiveCfg = Debug|Any CPU - {36C877F0-0A53-45EA-BB0C-C41423A8F87B}.Debug|x64.Build.0 = Debug|Any CPU - {36C877F0-0A53-45EA-BB0C-C41423A8F87B}.Debug|x86.ActiveCfg = Debug|Any CPU - {36C877F0-0A53-45EA-BB0C-C41423A8F87B}.Debug|x86.Build.0 = Debug|Any CPU - {36C877F0-0A53-45EA-BB0C-C41423A8F87B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {36C877F0-0A53-45EA-BB0C-C41423A8F87B}.Release|Any CPU.Build.0 = Release|Any CPU - {36C877F0-0A53-45EA-BB0C-C41423A8F87B}.Release|x64.ActiveCfg = Release|Any CPU - {36C877F0-0A53-45EA-BB0C-C41423A8F87B}.Release|x64.Build.0 = Release|Any CPU - {36C877F0-0A53-45EA-BB0C-C41423A8F87B}.Release|x86.ActiveCfg = Release|Any CPU - {36C877F0-0A53-45EA-BB0C-C41423A8F87B}.Release|x86.Build.0 = Release|Any CPU {A6B8AAC9-BE46-4A7B-BE75-AF0E95D04EC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A6B8AAC9-BE46-4A7B-BE75-AF0E95D04EC7}.Debug|Any CPU.Build.0 = Debug|Any CPU {A6B8AAC9-BE46-4A7B-BE75-AF0E95D04EC7}.Debug|x64.ActiveCfg = Debug|Any CPU diff --git a/src/PipeForge/Adapters/ActivitySourceProvider.cs b/src/PipeForge/Adapters/Diagnostics/ActivitySourceProvider.cs similarity index 90% rename from src/PipeForge/Adapters/ActivitySourceProvider.cs rename to src/PipeForge/Adapters/Diagnostics/ActivitySourceProvider.cs index 8374705..343a1d0 100644 --- a/src/PipeForge/Adapters/ActivitySourceProvider.cs +++ b/src/PipeForge/Adapters/Diagnostics/ActivitySourceProvider.cs @@ -1,9 +1,9 @@ #if !NETSTANDARD2_0 using System.Diagnostics; -namespace PipeForge.Adapters; +namespace PipeForge.Adapters.Diagnostics; -internal sealed class ActivitySourceProvider : IPipelineDiagnostics +internal sealed class ActivitySourceProvider : IPipelineDiagnostics where T : class { private static readonly ActivitySource _source = new($"PipeForge.PipelineRunner<{typeof(T).Name}>"); private static readonly string _activityName = "PipelineStep"; @@ -48,6 +48,11 @@ public void Dispose() _activity?.Dispose(); } + public void SetCanceled() + { + _activity?.SetTag("pipeline.cancelled", true); + } + public void SetShortCircuited(bool value) { _activity?.SetTag("pipeline.short_circuited", value.ToString()); diff --git a/src/PipeForge/Adapters/DiagnosticListenerProvider.cs b/src/PipeForge/Adapters/Diagnostics/DiagnosticListenerProvider.cs similarity index 89% rename from src/PipeForge/Adapters/DiagnosticListenerProvider.cs rename to src/PipeForge/Adapters/Diagnostics/DiagnosticListenerProvider.cs index bb51ae2..5660896 100644 --- a/src/PipeForge/Adapters/DiagnosticListenerProvider.cs +++ b/src/PipeForge/Adapters/Diagnostics/DiagnosticListenerProvider.cs @@ -1,9 +1,9 @@ #if NETSTANDARD2_0 using System.Diagnostics; -namespace PipeForge.Adapters; +namespace PipeForge.Adapters.Diagnostics; -internal sealed class DiagnosticListenerProvider : IPipelineDiagnostics +internal sealed class DiagnosticListenerProvider : IPipelineDiagnostics where T : class { private static readonly DiagnosticListener _listener = new($"PipeForge.PipelineRunner<{typeof(T).Name}>"); private static readonly string _activityName = "PipelineStep"; @@ -55,6 +55,11 @@ public DiagnosticListenerScope(DiagnosticListener listener, Activity activity, A public void Dispose() => _listener.StopActivity(_activity, _metadata); + public void SetCanceled() + { + _metadata.cancelled = true; + } + public void SetShortCircuited(bool value) { _metadata.short_circuited = value; @@ -68,10 +73,11 @@ private sealed class ActivityMetadata public int step_order { get; set; } public string? step_description { get; set; } public bool short_circuited { get; set; } + public bool cancelled { get; set; } public override string ToString() { - return $"{{ context_type = {context_type}, step_name = {step_name}, step_order = {step_order}, step_description = {step_description}, short_circuited = {short_circuited} }}"; + return $"{{ context_type = {context_type}, step_name = {step_name}, step_order = {step_order}, step_description = {step_description}, short_circuited = {short_circuited}, cancelled = {cancelled} }}"; } } } diff --git a/src/PipeForge/Adapters/Diagnostics/IPipelineDiagnostics.cs b/src/PipeForge/Adapters/Diagnostics/IPipelineDiagnostics.cs new file mode 100644 index 0000000..dc70abb --- /dev/null +++ b/src/PipeForge/Adapters/Diagnostics/IPipelineDiagnostics.cs @@ -0,0 +1,20 @@ +namespace PipeForge.Adapters.Diagnostics; + +internal interface IPipelineDiagnostics where T : class +{ + /// + /// Begins a diagnostics scope for the given step. + /// + /// + /// + /// + IPipelineDiagnosticsScope BeginStep(IPipelineStep step, int order); + + /// + /// Reports an exception that occurred during the execution of a step in the pipeline. + /// + /// + /// + /// + void ReportException(Exception ex, IPipelineStep step, int order); +} diff --git a/src/PipeForge/Adapters/Diagnostics/IPipelineDiagnosticsScope.cs b/src/PipeForge/Adapters/Diagnostics/IPipelineDiagnosticsScope.cs new file mode 100644 index 0000000..d34ef02 --- /dev/null +++ b/src/PipeForge/Adapters/Diagnostics/IPipelineDiagnosticsScope.cs @@ -0,0 +1,18 @@ +namespace PipeForge.Adapters.Diagnostics; + +internal interface IPipelineDiagnosticsScope : IDisposable +{ + /// + /// Sets a tag for the diagnostics scope indicating that the pipeline was short-circuited. + /// This is used to indicate that the pipeline execution was intentionally stopped before completion. + /// + /// + void SetShortCircuited(bool value); + + /// + /// Sets a tag for the diagnostics scope indicating that the pipeline was canceled. + /// This is used to indicate that the pipeline execution was canceled, either by user action or + /// due to some other condition that prevents further processing. + /// + void SetCanceled(); +} diff --git a/src/PipeForge/Adapters/Diagnostics/NullScope.cs b/src/PipeForge/Adapters/Diagnostics/NullScope.cs new file mode 100644 index 0000000..4fb2bfb --- /dev/null +++ b/src/PipeForge/Adapters/Diagnostics/NullScope.cs @@ -0,0 +1,17 @@ +using System.Diagnostics.CodeAnalysis; + +namespace PipeForge.Adapters.Diagnostics; + +[ExcludeFromCodeCoverage] +internal sealed class NullScope : IPipelineDiagnosticsScope +{ + public static readonly NullScope Instance = new(); + + private NullScope() { } + + public void Dispose() { } + + public void SetCanceled() { } + + public void SetShortCircuited(bool value) { } +} diff --git a/src/PipeForge/Adapters/Diagnostics/PipelineDiagnosticsFactory.cs b/src/PipeForge/Adapters/Diagnostics/PipelineDiagnosticsFactory.cs new file mode 100644 index 0000000..d9b18ab --- /dev/null +++ b/src/PipeForge/Adapters/Diagnostics/PipelineDiagnosticsFactory.cs @@ -0,0 +1,21 @@ +using System.Diagnostics.CodeAnalysis; + +namespace PipeForge.Adapters.Diagnostics; + +[ExcludeFromCodeCoverage] +internal static class PipelineDiagnosticsFactory +{ + /// + /// Returns an implementation of IPipelineDiagnostics for the given class T. + /// + /// + /// + public static IPipelineDiagnostics Create() where T : class + { +#if NETSTANDARD2_0 + return new DiagnosticListenerProvider(); +#else + return new ActivitySourceProvider(); +#endif + } +} diff --git a/src/PipeForge/Adapters/IPipelineDiagnostics.cs b/src/PipeForge/Adapters/IPipelineDiagnostics.cs deleted file mode 100644 index 65a56b5..0000000 --- a/src/PipeForge/Adapters/IPipelineDiagnostics.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace PipeForge.Adapters; - -internal interface IPipelineDiagnostics -{ - /// - /// Begins a diagnostics scope for the given step. - /// - /// - /// - /// - IPipelineDiagnosticsScope BeginStep(IPipelineStep step, int order); - - /// - /// Reports an exception that occurred during the execution of a step in the pipeline. - /// - /// - /// - /// - void ReportException(Exception ex, IPipelineStep step, int order); -} - -internal interface IPipelineDiagnosticsScope : IDisposable -{ - /// - /// Sets a tag for the diagnostics scope. - /// - /// - void SetShortCircuited(bool value); -} - -internal static class PipelineDiagnosticsFactory -{ - /// - /// Returns an implementation of IPipelineDiagnostics for the given type T. - /// - /// - /// - public static IPipelineDiagnostics Create() - { -#if NETSTANDARD2_0 - return new DiagnosticListenerProvider(); -#else - return new ActivitySourceProvider(); -#endif - } -} - -[ExcludeFromCodeCoverage] -internal sealed class NullScope : IPipelineDiagnosticsScope -{ - public static readonly NullScope Instance = new(); - public void Dispose() { } - public void SetShortCircuited(bool value) { } -} diff --git a/src/PipeForge/Adapters/IJsonSerializer.cs b/src/PipeForge/Adapters/Json/IJsonSerializer.cs similarity index 66% rename from src/PipeForge/Adapters/IJsonSerializer.cs rename to src/PipeForge/Adapters/Json/IJsonSerializer.cs index 178e470..35b763c 100644 --- a/src/PipeForge/Adapters/IJsonSerializer.cs +++ b/src/PipeForge/Adapters/Json/IJsonSerializer.cs @@ -1,4 +1,4 @@ -namespace PipeForge.Adapters; +namespace PipeForge.Adapters.Json; internal interface IJsonSerializer { @@ -18,15 +18,3 @@ internal interface IJsonSerializer /// T Deserialize(string json); } - -internal static class JsonSerializerFactory -{ - public static IJsonSerializer Create() - { -#if NETSTANDARD2_0 - return new NewtonsoftJsonSerializer(); -#else - return new SystemTextJsonSerializer(); -#endif - } -} diff --git a/src/PipeForge/Adapters/Json/JsonSerializerFactory.cs b/src/PipeForge/Adapters/Json/JsonSerializerFactory.cs new file mode 100644 index 0000000..24b43d6 --- /dev/null +++ b/src/PipeForge/Adapters/Json/JsonSerializerFactory.cs @@ -0,0 +1,16 @@ +using System.Diagnostics.CodeAnalysis; + +namespace PipeForge.Adapters.Json; + +[ExcludeFromCodeCoverage] +internal static class JsonSerializerFactory +{ + public static IJsonSerializer Create() + { +#if NETSTANDARD2_0 + return NewtonsoftJsonSerializer.GetInstance(); +#else + return SystemTextJsonSerializer.GetInstance(); +#endif + } +} diff --git a/src/PipeForge/Adapters/NewtonsoftJsonSerializer.cs b/src/PipeForge/Adapters/Json/NewtonsoftJsonSerializer.cs similarity index 61% rename from src/PipeForge/Adapters/NewtonsoftJsonSerializer.cs rename to src/PipeForge/Adapters/Json/NewtonsoftJsonSerializer.cs index 9bb2695..42994d0 100644 --- a/src/PipeForge/Adapters/NewtonsoftJsonSerializer.cs +++ b/src/PipeForge/Adapters/Json/NewtonsoftJsonSerializer.cs @@ -2,13 +2,19 @@ using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json; -namespace PipeForge.Adapters; +namespace PipeForge.Adapters.Json; [ExcludeFromCodeCoverage] internal class NewtonsoftJsonSerializer : IJsonSerializer { + private static readonly NewtonsoftJsonSerializer _instance = new(); + + private NewtonsoftJsonSerializer() { } + public string Serialize(T obj) => JsonConvert.SerializeObject(obj, Formatting.Indented); public T Deserialize(string json) => JsonConvert.DeserializeObject(json)!; + + public static NewtonsoftJsonSerializer GetInstance() => _instance; } #endif diff --git a/src/PipeForge/Adapters/SystemTextJsonSerializer.cs b/src/PipeForge/Adapters/Json/SystemTextJsonSerializer.cs similarity index 71% rename from src/PipeForge/Adapters/SystemTextJsonSerializer.cs rename to src/PipeForge/Adapters/Json/SystemTextJsonSerializer.cs index 7715a7f..3e77aa5 100644 --- a/src/PipeForge/Adapters/SystemTextJsonSerializer.cs +++ b/src/PipeForge/Adapters/Json/SystemTextJsonSerializer.cs @@ -3,11 +3,15 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace PipeForge.Adapters; +namespace PipeForge.Adapters.Json; [ExcludeFromCodeCoverage] internal class SystemTextJsonSerializer : IJsonSerializer { + private static readonly SystemTextJsonSerializer _instance = new(); + + private SystemTextJsonSerializer() { } + private static readonly JsonSerializerOptions _options = new() { WriteIndented = true, @@ -17,5 +21,7 @@ internal class SystemTextJsonSerializer : IJsonSerializer public string Serialize(T obj) => JsonSerializer.Serialize(obj, _options); public T Deserialize(string json) => JsonSerializer.Deserialize(json, _options)!; + + public static SystemTextJsonSerializer GetInstance() => _instance; } #endif diff --git a/src/PipeForge/PipelineBuilder.cs b/src/PipeForge/PipelineBuilder.cs index 1c0ce81..510ca86 100644 --- a/src/PipeForge/PipelineBuilder.cs +++ b/src/PipeForge/PipelineBuilder.cs @@ -7,6 +7,7 @@ namespace PipeForge; /// /// public class PipelineBuilder + where T : class { private readonly List>> _steps = new(); private readonly ILoggerFactory? _loggerFactory; diff --git a/src/PipeForge/PipelineRunner.cs b/src/PipeForge/PipelineRunner.cs index 6c85caa..6e30b01 100644 --- a/src/PipeForge/PipelineRunner.cs +++ b/src/PipeForge/PipelineRunner.cs @@ -1,11 +1,12 @@ using System.Diagnostics; -using PipeForge.Adapters; - using Microsoft.Extensions.Logging; +using PipeForge.Adapters.Json; +using PipeForge.Adapters.Diagnostics; namespace PipeForge; public class PipelineRunner : IPipelineRunner + where T : class { private static readonly IJsonSerializer _jsonSerializer = JsonSerializerFactory.Create(); private static readonly IPipelineDiagnostics _diagnostics = PipelineDiagnosticsFactory.Create();