Skip to content

Commit 082d159

Browse files
Merge branch 'main' into develop
2 parents af01c98 + 99ae698 commit 082d159

File tree

7 files changed

+156
-5
lines changed

7 files changed

+156
-5
lines changed

Hyperbee.Pipeline.sln.DotSettings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
2+
<s:Boolean x:Key="/Default/GrammarAndSpelling/GrammarChecking/Exceptions/=Setup_0020the/@EntryIndexedValue">True</s:Boolean>
23
<s:Boolean x:Key="/Default/UserDictionary/Words/=Hyperbee/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

src/Hyperbee.Pipeline/Commands/CommandFunction.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ protected CommandFunction( IPipelineContextFactory pipelineContextFactory, ILogg
2222

2323
public virtual async Task<CommandResult<TOutput>> ExecuteAsync( TStart argument, CancellationToken cancellation = default )
2424
{
25-
var context = ContextFactory.Create( Logger );
25+
var context = ContextFactory.Create( Logger, cancellation );
2626

2727
return new CommandResult<TOutput>
2828
{

src/Hyperbee.Pipeline/Commands/CommandProcedure.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ protected CommandProcedure( IPipelineContextFactory pipelineContextFactory, ILog
2222

2323
public virtual async Task<CommandResult> ExecuteAsync( TStart argument, CancellationToken cancellation = default )
2424
{
25-
var context = ContextFactory.Create( Logger );
25+
var context = ContextFactory.Create( Logger, cancellation );
2626

2727
await Pipeline.Value( context, argument ).ConfigureAwait( false );
2828

src/Hyperbee.Pipeline/Context/IPipelineContextFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ namespace Hyperbee.Pipeline.Context;
44

55
public interface IPipelineContextFactory
66
{
7-
IPipelineContext Create( ILogger logger );
7+
IPipelineContext Create( ILogger logger, CancellationToken cancellation = default );
88
}

src/Hyperbee.Pipeline/Context/PipelineContextFactory.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ private PipelineContextFactory( IServiceProvider serviceProvider )
1515
_serviceProvider = serviceProvider;
1616
}
1717

18-
public IPipelineContext Create( ILogger logger )
18+
public IPipelineContext Create( ILogger logger, CancellationToken cancellation = default )
1919
{
20-
return new PipelineContext
20+
return new PipelineContext( cancellation )
2121
{
2222
Logger = logger,
2323
ServiceProvider = _serviceProvider
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
using System.Threading;
2+
using System.Threading.Tasks;
3+
using Hyperbee.Pipeline.Commands;
4+
using Hyperbee.Pipeline.Context;
5+
using Microsoft.Extensions.Logging;
6+
using Microsoft.VisualStudio.TestTools.UnitTesting;
7+
using NSubstitute;
8+
9+
namespace Hyperbee.Pipeline.Tests;
10+
11+
[TestClass]
12+
public class CommandFunctionTests
13+
{
14+
[TestMethod]
15+
public async Task CommandFunction_ExecuteAsync_Should_Pass_CancellationToken_To_Context()
16+
{
17+
// Arrange
18+
var logger = Substitute.For<ILogger>();
19+
var mockContextFactory = Substitute.For<IPipelineContextFactory>();
20+
var mockContext = Substitute.For<IPipelineContext>();
21+
22+
using var cancellationTokenSource = new CancellationTokenSource();
23+
var cancellationToken = cancellationTokenSource.Token;
24+
25+
// Setup the factory to return our mock context
26+
mockContextFactory.Create( logger, cancellationToken ).Returns( mockContext );
27+
28+
// Create a test command function
29+
var testCommand = new TestCommandFunction( mockContextFactory, logger );
30+
31+
// Act
32+
await testCommand.ExecuteAsync( 0, cancellationToken );
33+
34+
// Assert
35+
mockContextFactory.Received( 1 ).Create( logger, cancellationToken );
36+
}
37+
38+
[TestMethod]
39+
public async Task CommandFunction_ExecuteAsync_Should_Pass_Default_CancellationToken_When_None_Provided()
40+
{
41+
// Arrange
42+
var logger = Substitute.For<ILogger>();
43+
var mockContextFactory = Substitute.For<IPipelineContextFactory>();
44+
var mockContext = Substitute.For<IPipelineContext>();
45+
46+
// Setup the factory to return our mock context with default cancellation token
47+
mockContextFactory.Create( logger, default( CancellationToken ) ).Returns( mockContext );
48+
49+
var testCommand = new TestCommandFunction( mockContextFactory, logger );
50+
51+
// Act
52+
await testCommand.ExecuteAsync( 0 );
53+
54+
// Assert
55+
mockContextFactory.Received( 1 ).Create( logger, default( CancellationToken ) );
56+
}
57+
58+
[TestMethod]
59+
public async Task CommandFunction_ExecuteAsync_Should_Link_And_Propagate_Cancellation()
60+
{
61+
// Arrange
62+
var logger = Substitute.For<ILogger>();
63+
var contextFactory = PipelineContextFactory.CreateFactory( resetFactory: true );
64+
65+
using var cts = new CancellationTokenSource();
66+
var originalToken = cts.Token;
67+
68+
var testCommand = new TestCommandFunction( contextFactory, logger );
69+
70+
// Act
71+
var result = await testCommand.ExecuteAsync( 0, originalToken );
72+
var contextToken = result.Context.CancellationToken;
73+
74+
// Assert
75+
// The context creates a linked token source, so the tokens should not be equal
76+
Assert.AreNotEqual( originalToken, contextToken, "Context should use a linked CancellationTokenSource." );
77+
78+
Assert.IsFalse( originalToken.IsCancellationRequested, "Original token should not be canceled yet." );
79+
Assert.IsFalse( contextToken.IsCancellationRequested, "Linked context token should not be canceled yet." );
80+
81+
// Cancel the original and ensure propagation to the linked token
82+
await cts.CancelAsync();
83+
84+
Assert.IsTrue( originalToken.IsCancellationRequested, "Original token should be canceled." );
85+
Assert.IsTrue( contextToken.IsCancellationRequested, "Linked context token should be canceled when original is canceled." );
86+
}
87+
88+
[TestMethod]
89+
public async Task CommandFunction_Context_Cancellation_Does_Not_Cancel_Original_Token()
90+
{
91+
// Arrange
92+
var logger = Substitute.For<ILogger>();
93+
var contextFactory = PipelineContextFactory.CreateFactory( resetFactory: true );
94+
95+
using var cts = new CancellationTokenSource();
96+
var originalToken = cts.Token;
97+
98+
var testCommand = new TestCommandFunction( contextFactory, logger );
99+
100+
// Act
101+
var result = await testCommand.ExecuteAsync( 0, originalToken );
102+
var contextToken = result.Context.CancellationToken;
103+
104+
// Assert preconditions
105+
Assert.AreNotEqual( originalToken, contextToken );
106+
Assert.IsFalse( originalToken.IsCancellationRequested );
107+
Assert.IsFalse( contextToken.IsCancellationRequested );
108+
109+
// Cancel via context (one-way: does not flow back to original)
110+
result.Context.CancelAfter();
111+
112+
Assert.IsTrue( contextToken.IsCancellationRequested, "Context token should be canceled." );
113+
Assert.IsFalse( originalToken.IsCancellationRequested, "Original token should not be canceled by context cancellation." );
114+
}
115+
116+
// Test implementation of CommandFunction<TStart, TOutput>
117+
private class TestCommandFunction : CommandFunction<int, int>
118+
{
119+
public TestCommandFunction( IPipelineContextFactory pipelineContextFactory, ILogger logger )
120+
: base( pipelineContextFactory, logger )
121+
{
122+
}
123+
124+
protected override FunctionAsync<int, int> CreatePipeline()
125+
{
126+
return PipelineFactory
127+
.Start<int>()
128+
.Pipe( ( context, input ) => 42 )
129+
.Build();
130+
}
131+
}
132+
133+
// Test implementation of CommandFunction<TOutput> (parameterless version)
134+
private class TestCommandFunctionParameterless : CommandFunction<int>
135+
{
136+
public TestCommandFunctionParameterless( IPipelineContextFactory pipelineContextFactory, ILogger logger )
137+
: base( pipelineContextFactory, logger )
138+
{
139+
}
140+
141+
protected override FunctionAsync<Arg.Empty, int> CreatePipeline()
142+
{
143+
return PipelineFactory
144+
.Start<Arg.Empty>()
145+
.Pipe( ( context, input ) => 42 )
146+
.Build();
147+
}
148+
}
149+
}

test/Hyperbee.Pipeline.Tests/Hyperbee.Pipeline.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<PackageReference Include="MSTest.TestAdapter" Version="3.9.3" />
88
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
99
<PackageReference Include="MSTest.TestFramework" Version="3.9.3" />
10+
<PackageReference Include="NSubstitute" Version="5.3.0" />
1011
</ItemGroup>
1112
<ItemGroup>
1213
<ProjectReference Include="..\..\src\Hyperbee.Pipeline\Hyperbee.Pipeline.csproj" />

0 commit comments

Comments
 (0)