Skip to content

Commit c80d530

Browse files
author
vp
committed
test: Add new tests for pipeline execution behaviors and diagnostics handling
1 parent 5aab547 commit c80d530

1 file changed

Lines changed: 264 additions & 0 deletions

File tree

tests/Common.UnitTests/Utilities/Pipeline/PipelineExecutionTests.cs

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,47 @@ public async Task ExecuteAsync_ThrownStepException_ReturnsFailedResultWithExcept
5757
result.Messages.ShouldContain("boom");
5858
}
5959

60+
[Fact]
61+
public async Task ExecuteAsync_FailedResult_DefaultStopsRemainingSteps()
62+
{
63+
var services = CreateServices();
64+
services.AddPipelines()
65+
.WithPipeline<ContinueOnFailurePipeline>();
66+
67+
var provider = services.BuildServiceProvider();
68+
var pipeline = provider.GetRequiredService<IPipelineFactory>()
69+
.Create<ContinueOnFailurePipeline, TestContext>();
70+
var context = new TestContext();
71+
72+
var result = await pipeline.ExecuteAsync(context);
73+
74+
result.IsFailure.ShouldBeTrue();
75+
result.Messages.ShouldContain("failed-step");
76+
context.AfterFailureExecuted.ShouldBeFalse();
77+
context.Pipeline.ExecutedStepCount.ShouldBe(1);
78+
}
79+
80+
[Fact]
81+
public async Task ExecuteAsync_FailedResult_WhenContinueOnFailure_ContinuesRemainingSteps()
82+
{
83+
var services = CreateServices();
84+
services.AddPipelines()
85+
.WithPipeline<ContinueOnFailurePipeline>();
86+
87+
var provider = services.BuildServiceProvider();
88+
var pipeline = provider.GetRequiredService<IPipelineFactory>()
89+
.Create<ContinueOnFailurePipeline, TestContext>();
90+
var context = new TestContext();
91+
92+
var result = await pipeline.ExecuteAsync(context, builder => builder.ContinueOnFailure());
93+
94+
result.IsFailure.ShouldBeTrue();
95+
result.Messages.ShouldContain("failed-step");
96+
result.Messages.ShouldContain("after-failure");
97+
context.AfterFailureExecuted.ShouldBeTrue();
98+
context.Pipeline.ExecutedStepCount.ShouldBe(2);
99+
}
100+
60101
[Fact]
61102
public async Task ExecuteAsync_Retry_ReexecutesStepUntilSuccess()
62103
{
@@ -78,6 +119,26 @@ public async Task ExecuteAsync_Retry_ReexecutesStepUntilSuccess()
78119
context.Pipeline.ExecutedStepCount.ShouldBe(2);
79120
}
80121

122+
[Fact]
123+
public async Task ExecuteAsync_RetryExhaustion_ReturnsFailedResultWithRetryError()
124+
{
125+
var services = CreateServices();
126+
services.AddPipelines()
127+
.WithPipeline<RetryExhaustionPipeline>();
128+
129+
var provider = services.BuildServiceProvider();
130+
var pipeline = provider.GetRequiredService<IPipelineFactory>()
131+
.Create<RetryExhaustionPipeline, TestContext>();
132+
var context = new TestContext();
133+
134+
var result = await pipeline.ExecuteAsync(context, builder => builder.MaxRetryAttemptsPerStep(2));
135+
136+
result.IsFailure.ShouldBeTrue();
137+
result.Errors.OfType<Error>().Any(e => e.Message.Contains("exhausted retry attempts")).ShouldBeTrue();
138+
context.RetryAttempts.ShouldBe(3);
139+
context.Pipeline.ExecutedStepCount.ShouldBe(3);
140+
}
141+
81142
[Fact]
82143
public async Task ExecuteAsync_Break_StopsRemainingSteps()
83144
{
@@ -97,6 +158,46 @@ public async Task ExecuteAsync_Break_StopsRemainingSteps()
97158
context.Pipeline.ExecutedStepCount.ShouldBe(2);
98159
}
99160

161+
[Fact]
162+
public async Task ExecuteAsync_AccumulateDiagnosticsOnFailureFalse_StripsFailureDiagnostics()
163+
{
164+
var services = CreateServices();
165+
services.AddPipelines()
166+
.WithPipeline<ContinueOnFailurePipeline>();
167+
168+
var provider = services.BuildServiceProvider();
169+
var pipeline = provider.GetRequiredService<IPipelineFactory>()
170+
.Create<ContinueOnFailurePipeline, TestContext>();
171+
172+
var result = await pipeline.ExecuteAsync(
173+
new TestContext(),
174+
builder => builder.AccumulateDiagnosticsOnFailure(false));
175+
176+
result.IsFailure.ShouldBeTrue();
177+
result.Messages.ShouldBeEmpty();
178+
result.Errors.ShouldBeEmpty();
179+
}
180+
181+
[Fact]
182+
public async Task ExecuteAsync_AccumulateDiagnosticsOnBreakFalse_StripsBreakDiagnostics()
183+
{
184+
var services = CreateServices();
185+
services.AddPipelines()
186+
.WithPipeline<BreakDiagnosticsPipeline>();
187+
188+
var provider = services.BuildServiceProvider();
189+
var pipeline = provider.GetRequiredService<IPipelineFactory>()
190+
.Create<BreakDiagnosticsPipeline, TestContext>();
191+
192+
var result = await pipeline.ExecuteAsync(
193+
new TestContext(),
194+
builder => builder.AccumulateDiagnosticsOnBreak(false));
195+
196+
result.IsSuccess.ShouldBeTrue();
197+
result.Messages.ShouldBeEmpty();
198+
result.Errors.ShouldBeEmpty();
199+
}
200+
100201
[Fact]
101202
public async Task ExecuteAsync_InlineSteps_WorkForSyncAsyncAndContextAwareDelegates()
102203
{
@@ -134,6 +235,40 @@ public async Task ExecuteAsync_InlineSteps_WorkForSyncAsyncAndContextAwareDelega
134235
context.Pipeline.ExecutedStepCount.ShouldBe(5);
135236
}
136237

238+
[Fact]
239+
public async Task ExecuteAsync_ProgressReporter_IsExposedToClassAndInlineSteps()
240+
{
241+
var services = CreateServices();
242+
var progress = new RecordingProgress();
243+
244+
services.AddPipelines()
245+
.WithPipeline<TestContext>("progress", builder => builder
246+
.AddStep<ProgressStep>()
247+
.AddStep(execution =>
248+
{
249+
execution.Options.Progress?.Report(new ProgressReport(
250+
execution.Name,
251+
["inline-progress"],
252+
100,
253+
isCompleted: true));
254+
255+
return execution.Continue();
256+
}));
257+
258+
var provider = services.BuildServiceProvider();
259+
var pipeline = provider.GetRequiredService<IPipelineFactory>()
260+
.Create<TestContext>("progress");
261+
262+
var result = await pipeline.ExecuteAsync(
263+
new TestContext(),
264+
builder => builder.WithProgress(progress));
265+
266+
result.IsSuccess.ShouldBeTrue();
267+
progress.Reports.Count.ShouldBe(2);
268+
progress.Reports[0].Messages.ShouldContain("class-progress");
269+
progress.Reports[1].Messages.ShouldContain("inline-progress");
270+
}
271+
137272
[Fact]
138273
public async Task ExecuteAsync_BaseContextHookAndBehavior_AreAppliedToDerivedContextPipeline()
139274
{
@@ -204,6 +339,51 @@ public async Task ExecuteAndForgetAsync_TracksExecutionAndInvokesCompletionCallb
204339
completion.Status.ShouldBe(PipelineExecutionStatus.Completed);
205340
}
206341

342+
[Fact]
343+
public async Task ExecuteAndForgetAsync_CompletionCallbackFailure_DoesNotOverwriteCompletedSnapshot()
344+
{
345+
var services = CreateServices();
346+
services.AddPipelines()
347+
.WithPipeline<BackgroundPipeline>();
348+
349+
var provider = services.BuildServiceProvider();
350+
var factory = provider.GetRequiredService<IPipelineFactory>();
351+
var tracker = provider.GetRequiredService<IPipelineExecutionTracker>();
352+
var pipeline = factory.Create<BackgroundPipeline, TestContext>();
353+
var callbackSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
354+
PipelineExecutionSnapshot snapshotObservedInCallback = null;
355+
356+
var handle = await pipeline.ExecuteAndForgetAsync(
357+
new TestContext(),
358+
builder => builder.WhenCompleted(async completion =>
359+
{
360+
snapshotObservedInCallback = await tracker.GetAsync(completion.ExecutionId);
361+
callbackSource.TrySetResult();
362+
throw new InvalidOperationException("callback boom");
363+
}));
364+
365+
await callbackSource.Task.WaitAsync(TimeSpan.FromSeconds(5));
366+
367+
PipelineExecutionSnapshot snapshot = null;
368+
for (var i = 0; i < 40; i++)
369+
{
370+
snapshot = await tracker.GetAsync(handle.ExecutionId);
371+
if (snapshot?.Status == PipelineExecutionStatus.Completed)
372+
{
373+
break;
374+
}
375+
376+
await Task.Delay(25);
377+
}
378+
379+
snapshot.ShouldNotBeNull();
380+
snapshot.Status.ShouldBe(PipelineExecutionStatus.Completed);
381+
snapshot.Result.IsSuccess.ShouldBeTrue();
382+
snapshotObservedInCallback.ShouldNotBeNull();
383+
snapshotObservedInCallback.Status.ShouldBe(PipelineExecutionStatus.Completed);
384+
snapshotObservedInCallback.Result.IsSuccess.ShouldBeTrue();
385+
}
386+
207387
[Fact]
208388
public async Task ExecuteAsync_TracingBehavior_CreatesPipelineAndStepActivities()
209389
{
@@ -251,6 +431,8 @@ public sealed class TestContext : PipelineContextBase
251431

252432
public bool AfterBreakExecuted { get; set; }
253433

434+
public bool AfterFailureExecuted { get; set; }
435+
254436
public int ContextStepCount { get; set; }
255437
}
256438

@@ -289,6 +471,32 @@ protected override void Configure(IPipelineDefinitionBuilder<TestContext> builde
289471
}
290472
}
291473

474+
public sealed class ContinueOnFailurePipeline : PipelineDefinition<TestContext>
475+
{
476+
protected override void Configure(IPipelineDefinitionBuilder<TestContext> builder)
477+
{
478+
builder.AddStep<FailedResultStep>()
479+
.AddStep<AfterFailureStep>();
480+
}
481+
}
482+
483+
public sealed class RetryExhaustionPipeline : PipelineDefinition<TestContext>
484+
{
485+
protected override void Configure(IPipelineDefinitionBuilder<TestContext> builder)
486+
{
487+
builder.AddStep<AlwaysRetryStep>();
488+
}
489+
}
490+
491+
public sealed class BreakDiagnosticsPipeline : PipelineDefinition<TestContext>
492+
{
493+
protected override void Configure(IPipelineDefinitionBuilder<TestContext> builder)
494+
{
495+
builder.AddStep<BreakWithDiagnosticsStep>()
496+
.AddStep<AfterBreakStep>();
497+
}
498+
}
499+
292500
public sealed class BackgroundPipeline : PipelineDefinition<TestContext>
293501
{
294502
protected override void Configure(IPipelineDefinitionBuilder<TestContext> builder)
@@ -322,6 +530,26 @@ protected override PipelineControl Execute(TestContext context, Result result, P
322530
}
323531
}
324532

533+
public sealed class FailedResultStep : PipelineStep<TestContext>
534+
{
535+
protected override PipelineControl Execute(TestContext context, Result result, PipelineExecutionOptions options)
536+
{
537+
return PipelineControl.Continue(
538+
Result.Failure()
539+
.WithMessage("failed-step")
540+
.WithError(new Error("failed-error")));
541+
}
542+
}
543+
544+
public sealed class AfterFailureStep : PipelineStep<TestContext>
545+
{
546+
protected override PipelineControl Execute(TestContext context, Result result, PipelineExecutionOptions options)
547+
{
548+
context.AfterFailureExecuted = true;
549+
return PipelineControl.Continue(result.WithMessage("after-failure"));
550+
}
551+
}
552+
325553
public sealed class RetryStep : PipelineStep<TestContext>
326554
{
327555
protected override PipelineControl Execute(TestContext context, Result result, PipelineExecutionOptions options)
@@ -333,6 +561,15 @@ protected override PipelineControl Execute(TestContext context, Result result, P
333561
}
334562
}
335563

564+
public sealed class AlwaysRetryStep : PipelineStep<TestContext>
565+
{
566+
protected override PipelineControl Execute(TestContext context, Result result, PipelineExecutionOptions options)
567+
{
568+
context.RetryAttempts++;
569+
return PipelineControl.Retry(result.WithMessage($"retry-{context.RetryAttempts}"), "retry");
570+
}
571+
}
572+
336573
public sealed class BreakStep : PipelineStep<TestContext>
337574
{
338575
protected override PipelineControl Execute(TestContext context, Result result, PipelineExecutionOptions options)
@@ -341,6 +578,14 @@ protected override PipelineControl Execute(TestContext context, Result result, P
341578
}
342579
}
343580

581+
public sealed class BreakWithDiagnosticsStep : PipelineStep<TestContext>
582+
{
583+
protected override PipelineControl Execute(TestContext context, Result result, PipelineExecutionOptions options)
584+
{
585+
return PipelineControl.Break(result.WithMessage("break-diagnostic"), "break");
586+
}
587+
}
588+
344589
public sealed class AfterBreakStep : PipelineStep<TestContext>
345590
{
346591
protected override PipelineControl Execute(TestContext context, Result result, PipelineExecutionOptions options)
@@ -350,6 +595,15 @@ protected override PipelineControl Execute(TestContext context, Result result, P
350595
}
351596
}
352597

598+
public sealed class ProgressStep : PipelineStep<TestContext>
599+
{
600+
protected override PipelineControl Execute(TestContext context, Result result, PipelineExecutionOptions options)
601+
{
602+
options.Progress?.Report(new ProgressReport(this.Name, ["class-progress"], 50));
603+
return PipelineControl.Continue(result);
604+
}
605+
}
606+
353607
public sealed class BackgroundStep : AsyncPipelineStep<TestContext>
354608
{
355609
protected override async ValueTask<PipelineControl> ExecuteAsync(TestContext context, Result result, PipelineExecutionOptions options, CancellationToken cancellationToken)
@@ -370,6 +624,16 @@ public sealed class ExecutionProbe
370624
public int StepBehaviorCount { get; set; }
371625
}
372626

627+
public sealed class RecordingProgress : IProgress<ProgressReport>
628+
{
629+
public List<ProgressReport> Reports { get; } = [];
630+
631+
public void Report(ProgressReport value)
632+
{
633+
this.Reports.Add(value);
634+
}
635+
}
636+
373637
public sealed class ProbeHook(ExecutionProbe probe) : PipelineHook<PipelineContextBase>
374638
{
375639
public override ValueTask OnPipelineStartingAsync(PipelineContextBase context, CancellationToken cancellationToken)

0 commit comments

Comments
 (0)