Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions WorkflowFramework.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@
<Project Path="tests/WorkflowFramework.Extensions.HumanTasks.Tests/WorkflowFramework.Extensions.HumanTasks.Tests.csproj" />
<Project Path="tests/WorkflowFramework.Extensions.Connectors.Tests/WorkflowFramework.Extensions.Connectors.Tests.csproj" />
<Project Path="tests/WorkflowFramework.Extensions.Http.Tests/WorkflowFramework.Extensions.Http.Tests.csproj" />
<Project Path="tests/WorkflowFramework.Extensions.Diagnostics.Tests/WorkflowFramework.Extensions.Diagnostics.Tests.csproj" />
<Project Path="tests/WorkflowFramework.Extensions.Events.Tests/WorkflowFramework.Extensions.Events.Tests.csproj" />
</Folder>
<Folder Name="/benchmarks/">
<Project Path="benchmarks/WorkflowFramework.Benchmarks/WorkflowFramework.Benchmarks.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using FluentAssertions;
using TinyBDD;
using TinyBDD.Xunit;
using WorkflowFramework.Extensions.Diagnostics;
using Xunit;
using Xunit.Abstractions;

namespace WorkflowFramework.Extensions.Diagnostics.Tests.Diagnostics;

[Feature("MetricsMiddleware — collects workflow execution counters")]
public class MetricsMiddlewareScenarios : TinyBddXunitBase
{
public MetricsMiddlewareScenarios(ITestOutputHelper output) : base(output) { }

private sealed class LambdaStep : IStep
{
private readonly Func<IWorkflowContext, Task> _fn;
public LambdaStep(string name, Func<IWorkflowContext, Task> fn) { Name = name; _fn = fn; }
public string Name { get; }
public Task ExecuteAsync(IWorkflowContext context) => _fn(context);
}

[Scenario("Initial state has zero counters"), Fact]
public async Task InitialState_ZeroCounters()
{
var mw = new MetricsMiddleware();

await Given("a fresh MetricsMiddleware", () => mw)
.Then("TotalSteps=0, FailedSteps=0, AverageDuration=Zero", m =>
{
m.TotalSteps.Should().Be(0);
m.FailedSteps.Should().Be(0);
m.AverageDuration.Should().Be(TimeSpan.Zero);
return true;
})
.AssertPassed();
}

[Scenario("Successful step increments TotalSteps"), Fact]
public async Task SuccessfulStep_IncrementsTotalSteps()
{
var mw = new MetricsMiddleware();
var step = new LambdaStep("ok", _ => Task.CompletedTask);
var ctx = new WorkflowContext();

await mw.InvokeAsync(ctx, step, c => step.ExecuteAsync(c));

await Given("one successful step", () => mw.TotalSteps)
.Then("TotalSteps is 1", t =>
{
t.Should().Be(1);
return true;
})
.AssertPassed();
}

[Scenario("Failing step increments both TotalSteps and FailedSteps"), Fact]
public async Task FailingStep_IncrementsBothCounters()
{
var mw = new MetricsMiddleware();
var step = new LambdaStep("fail", _ => throw new InvalidOperationException("boom"));
var ctx = new WorkflowContext();

try { await mw.InvokeAsync(ctx, step, c => step.ExecuteAsync(c)); }
catch { /* expected */ }

await Given("one failing step", () => (mw.TotalSteps, mw.FailedSteps))
.Then("TotalSteps=1 and FailedSteps=1", t =>
{
t.TotalSteps.Should().Be(1);
t.FailedSteps.Should().Be(1);
return true;
})
.AssertPassed();
}

[Scenario("AverageDuration is positive after step execution"), Fact]
public async Task AverageDuration_IsPositive_AfterExecution()
{
var mw = new MetricsMiddleware();
var step = new LambdaStep("timed", _ => Task.CompletedTask);
var ctx = new WorkflowContext();

await mw.InvokeAsync(ctx, step, c => step.ExecuteAsync(c));

await Given("one step executed", () => mw.AverageDuration)
.Then("AverageDuration is >= zero", d =>
{
d.Should().BeGreaterThanOrEqualTo(TimeSpan.Zero);
return true;
})
.AssertPassed();
}

[Scenario("Multiple steps accumulate TotalSteps correctly"), Fact]
public async Task MultipleSteps_AccumulateCount()
{
var mw = new MetricsMiddleware();
var ctx = new WorkflowContext();
for (int i = 0; i < 5; i++)
{
var step = new LambdaStep($"step-{i}", _ => Task.CompletedTask);
await mw.InvokeAsync(ctx, step, c => step.ExecuteAsync(c));
}

await Given("5 steps executed", () => mw.TotalSteps)
.Then("TotalSteps is 5", t =>
{
t.Should().Be(5);
return true;
})
.AssertPassed();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
using FluentAssertions;
using TinyBDD;
using TinyBDD.Xunit;
using WorkflowFramework.Extensions.Diagnostics;
using Xunit;
using Xunit.Abstractions;

namespace WorkflowFramework.Extensions.Diagnostics.Tests.Diagnostics;

[Feature("TimingMiddleware — measures and stores step execution time")]
public class TimingMiddlewareScenarios : TinyBddXunitBase
{
public TimingMiddlewareScenarios(ITestOutputHelper output) : base(output) { }

private static IStep MakeStep(string name, int delayMs = 0) =>
new LambdaStep(name, async ctx =>
{
if (delayMs > 0) await Task.Delay(delayMs);
});

private sealed class LambdaStep : IStep
{
private readonly Func<IWorkflowContext, Task> _fn;
public LambdaStep(string name, Func<IWorkflowContext, Task> fn) { Name = name; _fn = fn; }
public string Name { get; }
public Task ExecuteAsync(IWorkflowContext context) => _fn(context);
}

[Scenario("TimingsKey constant has expected value"), Fact]
public async Task TimingsKey_HasExpectedValue()
{
await Given("TimingMiddleware.TimingsKey constant", () => TimingMiddleware.TimingsKey)
.Then("value is 'WorkflowFramework.StepTimings'", k =>
{
k.Should().Be("WorkflowFramework.StepTimings");
return true;
})
.AssertPassed();
}

[Scenario("InvokeAsync stores timing for the step"), Fact]
public async Task InvokeAsync_StoresTiming()
{
var middleware = new TimingMiddleware();
var context = new WorkflowContext();
var step = MakeStep("my-step");

await middleware.InvokeAsync(context, step, ctx => step.ExecuteAsync(ctx));

await Given("TimingMiddleware applied to my-step", () => context.Properties)
.Then("timings dictionary contains 'my-step'", props =>
{
props.Should().ContainKey(TimingMiddleware.TimingsKey);
var timings = (Dictionary<string, TimeSpan>)props[TimingMiddleware.TimingsKey];
timings.Should().ContainKey("my-step");
return true;
})
.AssertPassed();
}

[Scenario("Multiple steps each get their own timing entry"), Fact]
public async Task MultipleSteps_EachGetTimingEntry()
{
var middleware = new TimingMiddleware();
var context = new WorkflowContext();
var step1 = MakeStep("step-a");
var step2 = MakeStep("step-b");

await middleware.InvokeAsync(context, step1, ctx => step1.ExecuteAsync(ctx));
await middleware.InvokeAsync(context, step2, ctx => step2.ExecuteAsync(ctx));

await Given("two steps both measured by TimingMiddleware", () => context.Properties[TimingMiddleware.TimingsKey])
.Then("both entries exist in timings", obj =>
{
var timings = (Dictionary<string, TimeSpan>)obj!;
timings.Should().ContainKey("step-a").And.ContainKey("step-b");
return true;
})
.AssertPassed();
}

[Scenario("Timing is still recorded when step throws"), Fact]
public async Task InvokeAsync_StillRecordsTiming_WhenStepThrows()
{
var middleware = new TimingMiddleware();
var context = new WorkflowContext();
var step = new LambdaStep("throwing-step", _ => throw new InvalidOperationException("boom"));

try { await middleware.InvokeAsync(context, step, ctx => step.ExecuteAsync(ctx)); }
catch { /* expected */ }

await Given("step throws during execution", () => context.Properties)
.Then("timing entry still recorded (finally block)", props =>
{
props.Should().ContainKey(TimingMiddleware.TimingsKey);
var timings = (Dictionary<string, TimeSpan>)props[TimingMiddleware.TimingsKey];
timings.Should().ContainKey("throwing-step");
return true;
})
.AssertPassed();
}

[Scenario("Recorded elapsed time is non-negative"), Fact]
public async Task RecordedElapsedTime_IsNonNegative()
{
var middleware = new TimingMiddleware();
var context = new WorkflowContext();
var step = MakeStep("quick");

await middleware.InvokeAsync(context, step, ctx => step.ExecuteAsync(ctx));

var timings = (Dictionary<string, TimeSpan>)context.Properties[TimingMiddleware.TimingsKey];

await Given("step executed and timed", () => timings["quick"])
.Then("elapsed is >= 0", elapsed =>
{
elapsed.Should().BeGreaterThanOrEqualTo(TimeSpan.Zero);
return true;
})
.AssertPassed();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>WorkflowFramework.Extensions.Diagnostics.Tests</RootNamespace>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<!-- Source projects under test -->
<ProjectReference Include="..\..\src\WorkflowFramework\WorkflowFramework.csproj" />
<ProjectReference Include="..\..\src\WorkflowFramework.Extensions.Diagnostics\WorkflowFramework.Extensions.Diagnostics.csproj" />
</ItemGroup>
<ItemGroup>
<!-- Test frameworks -->
<PackageReference Include="TinyBDD" />
<PackageReference Include="TinyBDD.Xunit" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="NSubstitute" />
<PackageReference Include="coverlet.collector" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>
</Project>
Loading
Loading