Merge branch 'develop/3.6.0' into feat/unit-test-coverage-while
This commit is contained in:
commit
a643a5bc32
|
|
@ -0,0 +1,300 @@
|
|||
using System.Net;
|
||||
using System.Text;
|
||||
using Elsa.Http;
|
||||
using Elsa.Http.ContentWriters;
|
||||
using Elsa.Http.Options;
|
||||
using Elsa.Http.Parsers;
|
||||
using Elsa.Testing.Shared;
|
||||
using Elsa.Workflows;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Elsa.Activities.UnitTests.Http;
|
||||
|
||||
public class WriteHttpResponseTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(HttpStatusCode.OK, 200)]
|
||||
[InlineData(HttpStatusCode.Created, 201)]
|
||||
[InlineData(HttpStatusCode.NotFound, 404)]
|
||||
[InlineData(HttpStatusCode.InternalServerError, 500)]
|
||||
public async Task Should_Set_Correct_Status_Code(HttpStatusCode statusCode, int expectedStatusCode)
|
||||
{
|
||||
// Arrange
|
||||
var (activity, httpContext) = CreateWriteHttpResponseActivity();
|
||||
activity.StatusCode = new Input<HttpStatusCode>(statusCode);
|
||||
|
||||
// Act
|
||||
var context = await ExecuteActivityAsync(activity, httpContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expectedStatusCode, httpContext.Response.StatusCode);
|
||||
Assert.True(context.IsCompleted);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Hello World", "text/plain", "Hello World")]
|
||||
[InlineData("{\"name\": \"John\"}", "application/json", "{\"name\": \"John\"}")]
|
||||
[InlineData("<root><name>John</name></root>", "application/xml", "<root><name>John</name></root>")]
|
||||
public async Task Should_Write_String_Content_With_Correct_Content_Type(string content, string contentType, string expectedContent)
|
||||
{
|
||||
// Arrange
|
||||
var (activity, httpContext) = CreateWriteHttpResponseActivity();
|
||||
activity.Content = new Input<object?>(content);
|
||||
activity.ContentType = new Input<string?>(contentType);
|
||||
|
||||
// Act
|
||||
await ExecuteActivityAsync(activity, httpContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(contentType, httpContext.Response.ContentType);
|
||||
var responseContent = GetResponseContent(httpContext);
|
||||
Assert.Equal(expectedContent, responseContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Serialize_Object_To_Json_When_No_Content_Type_Specified()
|
||||
{
|
||||
// Arrange
|
||||
var (activity, httpContext) = CreateWriteHttpResponseActivity();
|
||||
var testObject = new { Name = "John", Age = 30 };
|
||||
activity.Content = new Input<object?>(testObject);
|
||||
|
||||
// Act
|
||||
await ExecuteActivityAsync(activity, httpContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("application/json", httpContext.Response.ContentType);
|
||||
var responseContent = GetResponseContent(httpContext);
|
||||
Assert.Contains("John", responseContent);
|
||||
Assert.Contains("30", responseContent);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Custom-Header", "CustomValue")]
|
||||
[InlineData("X-Rate-Limit", "100")]
|
||||
[InlineData("Cache-Control", "no-cache")]
|
||||
public async Task Should_Add_Response_Headers(string headerName, string headerValue)
|
||||
{
|
||||
// Arrange
|
||||
var (activity, httpContext) = CreateWriteHttpResponseActivity();
|
||||
var headers = new HttpHeaders { { headerName, new[] { headerValue } } };
|
||||
activity.ResponseHeaders = new Input<HttpHeaders?>(headers);
|
||||
|
||||
// Act
|
||||
await ExecuteActivityAsync(activity, httpContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(httpContext.Response.Headers.ContainsKey(headerName));
|
||||
Assert.Equal(headerValue, httpContext.Response.Headers[headerName]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Add_Multiple_Response_Headers()
|
||||
{
|
||||
// Arrange
|
||||
var (activity, httpContext) = CreateWriteHttpResponseActivity();
|
||||
var headers = new HttpHeaders
|
||||
{
|
||||
{ "Custom-Header-1", new[] { "Value1" } },
|
||||
{ "Custom-Header-2", new[] { "Value2" } },
|
||||
{ "X-Rate-Limit", new[] { "100" } }
|
||||
};
|
||||
activity.ResponseHeaders = new Input<HttpHeaders?>(headers);
|
||||
|
||||
// Act
|
||||
await ExecuteActivityAsync(activity, httpContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(httpContext.Response.Headers.ContainsKey("Custom-Header-1"));
|
||||
Assert.True(httpContext.Response.Headers.ContainsKey("Custom-Header-2"));
|
||||
Assert.True(httpContext.Response.Headers.ContainsKey("X-Rate-Limit"));
|
||||
Assert.Equal("Value1", httpContext.Response.Headers["Custom-Header-1"]);
|
||||
Assert.Equal("Value2", httpContext.Response.Headers["Custom-Header-2"]);
|
||||
Assert.Equal("100", httpContext.Response.Headers["X-Rate-Limit"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Not_Write_Content_When_Status_Code_Is_NoContent()
|
||||
{
|
||||
// Arrange
|
||||
var (activity, httpContext) = CreateWriteHttpResponseActivity();
|
||||
activity.StatusCode = new Input<HttpStatusCode>(HttpStatusCode.NoContent);
|
||||
activity.Content = new Input<object?>("This should not be written");
|
||||
|
||||
// Act
|
||||
await ExecuteActivityAsync(activity, httpContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(204, httpContext.Response.StatusCode);
|
||||
var responseContent = GetResponseContent(httpContext);
|
||||
Assert.Empty(responseContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Handle_Null_Content()
|
||||
{
|
||||
// Arrange
|
||||
var (activity, httpContext) = CreateWriteHttpResponseActivity();
|
||||
activity.Content = new Input<object?>((object?)null);
|
||||
|
||||
// Act
|
||||
await ExecuteActivityAsync(activity, httpContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(200, httpContext.Response.StatusCode);
|
||||
var responseContent = GetResponseContent(httpContext);
|
||||
Assert.Empty(responseContent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Call_CompleteAsync_When_WriteHttpResponseSynchronously_Is_True()
|
||||
{
|
||||
// Arrange
|
||||
var activity = new WriteHttpResponse();
|
||||
var mockHttpContext = Substitute.For<HttpContext>();
|
||||
var mockResponse = Substitute.For<HttpResponse>();
|
||||
var mockHeaders = new HeaderDictionary();
|
||||
var responseBody = new MemoryStream();
|
||||
|
||||
mockResponse.StatusCode.Returns(200);
|
||||
mockResponse.Headers.Returns(mockHeaders);
|
||||
mockResponse.Body.Returns(responseBody);
|
||||
mockHttpContext.Response.Returns(mockResponse);
|
||||
|
||||
// Act
|
||||
await ExecuteActivityAsync(activity, mockHttpContext, writeResponseSynchronously: true);
|
||||
|
||||
// Assert
|
||||
await mockResponse.Received(1).CompleteAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Not_Call_CompleteAsync_When_WriteHttpResponseSynchronously_Is_False()
|
||||
{
|
||||
// Arrange
|
||||
var activity = new WriteHttpResponse();
|
||||
var mockHttpContext = Substitute.For<HttpContext>();
|
||||
var mockResponse = Substitute.For<HttpResponse>();
|
||||
var mockHeaders = new HeaderDictionary();
|
||||
var responseBody = new MemoryStream();
|
||||
|
||||
mockResponse.StatusCode.Returns(200);
|
||||
mockResponse.Headers.Returns(mockHeaders);
|
||||
mockResponse.Body.Returns(responseBody);
|
||||
mockHttpContext.Response.Returns(mockResponse);
|
||||
|
||||
// Act
|
||||
await ExecuteActivityAsync(activity, mockHttpContext, writeResponseSynchronously: false);
|
||||
|
||||
// Assert
|
||||
await mockResponse.DidNotReceive().CompleteAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Create_Bookmark_When_No_HttpContext_Available()
|
||||
{
|
||||
// Arrange
|
||||
var activity = new WriteHttpResponse();
|
||||
var fixture = new ActivityTestFixture(activity);
|
||||
fixture.ConfigureServices(services =>
|
||||
{
|
||||
var mockHttpContextAccessor = Substitute.For<IHttpContextAccessor>();
|
||||
mockHttpContextAccessor.HttpContext.Returns((HttpContext?)null);
|
||||
services.AddSingleton(mockHttpContextAccessor);
|
||||
services.AddSingleton(CreateMockHttpActivityOptions());
|
||||
AddHttpContentFactories(services);
|
||||
services.AddSingleton(Substitute.For<IStimulusHasher>());
|
||||
});
|
||||
|
||||
// Act
|
||||
var context = await fixture.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
Assert.False(context.IsCompleted);
|
||||
var bookmarks = context.WorkflowExecutionContext.Bookmarks.ToList();
|
||||
Assert.Single(bookmarks);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Should_Use_Default_Status_Code_When_Not_Specified()
|
||||
{
|
||||
// Arrange
|
||||
var (activity, httpContext) = CreateWriteHttpResponseActivity();
|
||||
// StatusCode is not set, should default to OK (200)
|
||||
|
||||
// Act
|
||||
await ExecuteActivityAsync(activity, httpContext);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(200, httpContext.Response.StatusCode);
|
||||
}
|
||||
|
||||
private static (WriteHttpResponse activity, HttpContext httpContext) CreateWriteHttpResponseActivity()
|
||||
{
|
||||
var activity = new WriteHttpResponse();
|
||||
var httpContext = new DefaultHttpContext
|
||||
{
|
||||
Response =
|
||||
{
|
||||
Body = new MemoryStream()
|
||||
}
|
||||
};
|
||||
|
||||
return (activity, httpContext);
|
||||
}
|
||||
|
||||
private static async Task<ActivityExecutionContext> ExecuteActivityAsync(WriteHttpResponse activity, HttpContext httpContext, bool writeResponseSynchronously = false)
|
||||
{
|
||||
var fixture = new ActivityTestFixture(activity);
|
||||
fixture.ConfigureServices(services =>
|
||||
{
|
||||
var mockHttpContextAccessor = Substitute.For<IHttpContextAccessor>();
|
||||
mockHttpContextAccessor.HttpContext.Returns(httpContext);
|
||||
services.AddSingleton(mockHttpContextAccessor);
|
||||
services.AddSingleton(CreateMockHttpActivityOptions(writeResponseSynchronously));
|
||||
AddHttpContentFactories(services);
|
||||
AddHttpContentParsers(services);
|
||||
services.AddLogging();
|
||||
});
|
||||
|
||||
return await fixture.ExecuteAsync();
|
||||
}
|
||||
|
||||
private static IOptions<HttpActivityOptions> CreateMockHttpActivityOptions(bool writeResponseSynchronously = false)
|
||||
{
|
||||
var options = new HttpActivityOptions
|
||||
{
|
||||
WriteHttpResponseSynchronously = writeResponseSynchronously
|
||||
};
|
||||
var mockOptions = Substitute.For<IOptions<HttpActivityOptions>>();
|
||||
mockOptions.Value.Returns(options);
|
||||
return mockOptions;
|
||||
}
|
||||
|
||||
private static void AddHttpContentFactories(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IHttpContentFactory, JsonContentFactory>();
|
||||
services.AddSingleton<IHttpContentFactory, TextContentFactory>();
|
||||
services.AddSingleton<IHttpContentFactory, XmlContentFactory>();
|
||||
services.AddSingleton<IHttpContentFactory, FormUrlEncodedHttpContentFactory>();
|
||||
}
|
||||
|
||||
private static void AddHttpContentParsers(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IHttpContentParser, JsonHttpContentParser>();
|
||||
services.AddSingleton<IHttpContentParser, PlainTextHttpContentParser>();
|
||||
services.AddSingleton<IHttpContentParser, XmlHttpContentParser>();
|
||||
services.AddSingleton<IHttpContentParser, TextHtmlHttpContentParser>();
|
||||
services.AddSingleton<IHttpContentParser, FileHttpContentParser>();
|
||||
}
|
||||
|
||||
private static string GetResponseContent(HttpContext httpContext)
|
||||
{
|
||||
httpContext.Response.Body.Seek(0, SeekOrigin.Begin);
|
||||
using var reader = new StreamReader(httpContext.Response.Body);
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
475
test/unit/Elsa.Activities.UnitTests/Looping/ForTests.cs
Normal file
475
test/unit/Elsa.Activities.UnitTests/Looping/ForTests.cs
Normal file
|
|
@ -0,0 +1,475 @@
|
|||
using Elsa.Testing.Shared;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Behaviors;
|
||||
using Elsa.Workflows.Exceptions;
|
||||
|
||||
namespace Elsa.Activities.UnitTests.Looping;
|
||||
|
||||
public class ForTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(1, 3, 1)] // Ascending loop
|
||||
[InlineData(5, 1, -1)] // Descending loop
|
||||
[InlineData(-1, -5, -1)] // Descending negative loop
|
||||
public async Task ExecutesValidLoopConfigurations_SchedulesChildActivity(int start, int end, int step)
|
||||
{
|
||||
// Arrange
|
||||
var mockBody = new MockBodyActivity();
|
||||
var forActivity = new For(start, end, step) { Body = mockBody };
|
||||
var fixture = new ActivityTestFixture(forActivity);
|
||||
|
||||
// Act
|
||||
var context = await fixture.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
var currentValue = context.GetActivityOutput(() => forActivity.CurrentValue);
|
||||
Assert.Equal(start, currentValue);
|
||||
|
||||
// Verify that child activity is scheduled for valid configurations
|
||||
Assert.True(context.HasScheduledActivity(mockBody), "Expected child activity to be scheduled");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1, 3, true)] // Inclusive bounds
|
||||
[InlineData(1, 3, false)] // Exclusive bounds
|
||||
[InlineData(5, 5, true)] // Empty range inclusive
|
||||
[InlineData(5, 5, false)] // Empty range exclusive
|
||||
public async Task ExecutesBoundaryConditions(int start, int end, bool inclusive)
|
||||
{
|
||||
// Arrange
|
||||
var mockBody = new MockBodyActivity();
|
||||
var forActivity = new For
|
||||
{
|
||||
Start = new Input<int>(start),
|
||||
End = new Input<int>(end),
|
||||
Step = new Input<int>(1),
|
||||
OuterBoundInclusive = new Input<bool>(inclusive),
|
||||
Body = mockBody
|
||||
};
|
||||
var fixture = new ActivityTestFixture(forActivity);
|
||||
|
||||
// Act
|
||||
var context = await fixture.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
bool shouldSchedule = ShouldExecuteLoopWithBounds(start, end, 1, inclusive);
|
||||
Assert.Equal(shouldSchedule, context.HasScheduledActivity(mockBody));
|
||||
|
||||
if (shouldSchedule)
|
||||
{
|
||||
var currentValue = context.GetActivityOutput(() => forActivity.CurrentValue);
|
||||
Assert.Equal(start, currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(5, 1, 1)] // Positive step with descending range - won't execute
|
||||
[InlineData(1, 5, -1)] // Negative step with ascending range - won't execute
|
||||
public async Task SkipsLoopWhenInvalidConfiguration(int start, int end, int step)
|
||||
{
|
||||
// Arrange
|
||||
var mockBody = new MockBodyActivity();
|
||||
var forActivity = new For(start, end, step) { Body = mockBody };
|
||||
var fixture = new ActivityTestFixture(forActivity);
|
||||
|
||||
// Act
|
||||
var context = await fixture.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
Assert.False(context.HasScheduledActivity(mockBody));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BodyIsNull_DoesNotScheduleActivity()
|
||||
{
|
||||
// Arrange
|
||||
var forActivity = new For(1, 5, 1) { Body = null };
|
||||
var fixture = new ActivityTestFixture(forActivity);
|
||||
|
||||
// Act
|
||||
var context = await fixture.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
var allScheduledActivities = context.WorkflowExecutionContext.Scheduler.List().ToList();
|
||||
Assert.Empty(allScheduledActivities);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CurrentValueOutputTypeCheck_PreservesIntegerType()
|
||||
{
|
||||
// Arrange
|
||||
var mockBody = new MockBodyActivity();
|
||||
var forActivity = new For(1, 3, 1) { Body = mockBody };
|
||||
var fixture = new ActivityTestFixture(forActivity);
|
||||
|
||||
// Act
|
||||
var context = await fixture.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
var currentValue = context.GetActivityOutput(() => forActivity.CurrentValue);
|
||||
Assert.IsType<int>(currentValue);
|
||||
Assert.Equal(1, currentValue);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 5, true)] // Start with default value (0), should execute
|
||||
[InlineData(1, 0, false)] // End with explicit zero value, should not execute (wrong direction)
|
||||
public async Task HandlesDefaultValues(int start, int end, bool shouldExecute)
|
||||
{
|
||||
// Arrange
|
||||
var mockBody = new MockBodyActivity();
|
||||
var forActivity = new For
|
||||
{
|
||||
Start = new Input<int>(start),
|
||||
End = new Input<int>(end),
|
||||
Step = new Input<int>(1),
|
||||
Body = mockBody
|
||||
};
|
||||
var fixture = new ActivityTestFixture(forActivity);
|
||||
|
||||
// Act
|
||||
var context = await fixture.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(shouldExecute, context.HasScheduledActivity(mockBody));
|
||||
|
||||
if (shouldExecute)
|
||||
{
|
||||
var currentValue = context.GetActivityOutput(() => forActivity.CurrentValue);
|
||||
Assert.Equal(start, currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyActivityAttributes()
|
||||
{
|
||||
// Arrange
|
||||
var forActivity = new For();
|
||||
var fixture = new ActivityTestFixture(forActivity);
|
||||
|
||||
// Act & Assert
|
||||
fixture.AssertActivityAttributes(
|
||||
expectedNamespace: "Elsa",
|
||||
expectedKind: ActivityKind.Action,
|
||||
expectedCategory: "Looping",
|
||||
expectedDisplayName: null,
|
||||
expectedDescription: "Iterate over a sequence of steps between a start and an end number."
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VerifyBreakBehaviorIsRegistered()
|
||||
{
|
||||
// Arrange
|
||||
var forActivity = new For();
|
||||
|
||||
// Act & Assert
|
||||
var breakBehavior = forActivity.Behaviors.OfType<BreakBehavior>().FirstOrDefault();
|
||||
Assert.NotNull(breakBehavior);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultPropertyValues()
|
||||
{
|
||||
// Arrange
|
||||
var forActivity = new For();
|
||||
|
||||
// Act & Assert
|
||||
Assert.NotNull(forActivity.Start);
|
||||
Assert.NotNull(forActivity.End);
|
||||
Assert.NotNull(forActivity.Step);
|
||||
Assert.NotNull(forActivity.OuterBoundInclusive);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1, 3, 1, true)]
|
||||
[InlineData(3, 1, -1, true)]
|
||||
[InlineData(5, 5, 1, true)]
|
||||
[InlineData(1, 5, 0, true)] // Zero step - actually schedules activity (infinite loop potential)
|
||||
[InlineData(5, 1, 1, false)] // Wrong direction
|
||||
[InlineData(1, 5, -1, false)] // Wrong direction
|
||||
public async Task LoopDecisionLogic_ValidatesCorrectly(int start, int end, int step, bool shouldExecute)
|
||||
{
|
||||
// Arrange
|
||||
var mockBody = new MockBodyActivity();
|
||||
var forActivity = new For(start, end, step) { Body = mockBody };
|
||||
var fixture = new ActivityTestFixture(forActivity);
|
||||
|
||||
// Act
|
||||
var context = await fixture.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(shouldExecute, context.HasScheduledActivity(mockBody));
|
||||
}
|
||||
|
||||
// Zero step with different bounds & inclusivity (current contract: schedules at least first body)
|
||||
[Theory]
|
||||
[InlineData(1, 5, true, true)] // within ascending range, inclusive -> schedules
|
||||
[InlineData(1, 5, false, true)] // within ascending range, exclusive -> schedules (start < end)
|
||||
[InlineData(5, 1, true, false)] // start > end with ascending step (step=0 treated as positive) -> no schedule
|
||||
[InlineData(5, 1, false, false)] // start > end with ascending step (step=0 treated as positive) -> no schedule
|
||||
[InlineData(6, 5, true, false)] // start already outside ascending range -> no schedule
|
||||
[InlineData(0, -1, false, false)]// start already outside descending (exclusive) -> no schedule
|
||||
public async Task ZeroStep_RespectsInitialBoundCheck(int start, int end, bool inclusive, bool shouldSchedule)
|
||||
{
|
||||
// Arrange
|
||||
var body = new MockBodyActivity();
|
||||
var forActivity = new For
|
||||
{
|
||||
Start = new Input<int>(start),
|
||||
End = new Input<int>(end),
|
||||
Step = new Input<int>(0), // zero step: current contract allows first schedule
|
||||
OuterBoundInclusive = new Input<bool>(inclusive),
|
||||
Body = body
|
||||
};
|
||||
var fixture = new ActivityTestFixture(forActivity);
|
||||
|
||||
// Act
|
||||
var context = await fixture.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(shouldSchedule, context.HasScheduledActivity(body));
|
||||
if (shouldSchedule)
|
||||
{
|
||||
var currentValue = context.GetActivityOutput(() => forActivity.CurrentValue);
|
||||
Assert.IsType<int>(currentValue);
|
||||
Assert.Equal(start, currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1, 5, 10)] // ascending, step too large
|
||||
[InlineData(5, 1, -10)] // descending, step too large
|
||||
public async Task StepLargerThanRange_StillSchedulesOnce(int start, int end, int step)
|
||||
{
|
||||
// Arrange
|
||||
var body = new MockBodyActivity();
|
||||
var forActivity = new For(start, end, step) { Body = body };
|
||||
var fixture = new ActivityTestFixture(forActivity);
|
||||
|
||||
// Act
|
||||
var context = await fixture.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
Assert.True(context.HasScheduledActivity(body));
|
||||
var currentValue = context.GetActivityOutput(() => forActivity.CurrentValue);
|
||||
Assert.Equal(start, currentValue);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(5, 5, 1)]
|
||||
[InlineData(5, 5, -1)]
|
||||
public async Task EqualBounds_Exclusive_DoesNotExecute(int start, int end, int step)
|
||||
{
|
||||
// Arrange
|
||||
var body = new MockBodyActivity();
|
||||
var forActivity = new For
|
||||
{
|
||||
Start = new Input<int>(start),
|
||||
End = new Input<int>(end),
|
||||
Step = new Input<int>(step),
|
||||
OuterBoundInclusive = new Input<bool>(false),
|
||||
Body = body
|
||||
};
|
||||
var fixture = new ActivityTestFixture(forActivity);
|
||||
|
||||
// Act
|
||||
var context = await fixture.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
Assert.False(context.HasScheduledActivity(body));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(3, 1, -1, true, true)] // inclusive: start (3) >= end (1) -> schedules
|
||||
[InlineData(3, 3, -1, false, false)] // exclusive: start == end -> no schedule
|
||||
[InlineData(2, 3, -1, true, false)] // start already below end for descending -> no schedule
|
||||
public async Task Descending_InclusiveExclusive_OffByOne(int start, int end, int step, bool inclusive, bool shouldSchedule)
|
||||
{
|
||||
// Arrange
|
||||
var body = new MockBodyActivity();
|
||||
var forActivity = new For
|
||||
{
|
||||
Start = new Input<int>(start),
|
||||
End = new Input<int>(end),
|
||||
Step = new Input<int>(step),
|
||||
OuterBoundInclusive = new Input<bool>(inclusive),
|
||||
Body = body
|
||||
};
|
||||
var fixture = new ActivityTestFixture(forActivity);
|
||||
|
||||
// Act
|
||||
var context = await fixture.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(shouldSchedule, context.HasScheduledActivity(body));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(int.MaxValue, int.MaxValue, 1, true, true)] // inclusive equal -> schedules
|
||||
[InlineData(int.MaxValue, int.MaxValue, 1, false, false)] // exclusive equal -> no schedule
|
||||
[InlineData(int.MinValue, int.MinValue, -1, true, true)]
|
||||
[InlineData(int.MinValue, int.MinValue, -1, false, false)]
|
||||
public async Task ExtremeBounds_NoOverflow_OnInitialDecision(int start, int end, int step, bool inclusive, bool shouldSchedule)
|
||||
{
|
||||
// Arrange
|
||||
var body = new MockBodyActivity();
|
||||
var forActivity = new For
|
||||
{
|
||||
Start = new Input<int>(start),
|
||||
End = new Input<int>(end),
|
||||
Step = new Input<int>(step),
|
||||
OuterBoundInclusive = new Input<bool>(inclusive),
|
||||
Body = body
|
||||
};
|
||||
var fixture = new ActivityTestFixture(forActivity);
|
||||
|
||||
// Act
|
||||
var context = await fixture.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(shouldSchedule, context.HasScheduledActivity(body));
|
||||
if (shouldSchedule)
|
||||
{
|
||||
var currentValue = context.GetActivityOutput(() => forActivity.CurrentValue);
|
||||
Assert.Equal(start, currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("start")]
|
||||
[InlineData("end")]
|
||||
[InlineData("step")]
|
||||
public async Task InputExpressions_Throw_DoNotSchedule(string which)
|
||||
{
|
||||
// Arrange
|
||||
var body = new MockBodyActivity();
|
||||
var forActivity = new For
|
||||
{
|
||||
Start = which == "start" ? new Input<int>((Func<int>)(() => throw new ApplicationException("start!"))) : new Input<int>(1),
|
||||
End = which == "end" ? new Input<int>((Func<int>)(() => throw new ApplicationException("end!"))) : new Input<int>(3),
|
||||
Step = which == "step" ? new Input<int>((Func<int>)(() => throw new ApplicationException("step!"))) : new Input<int>(1),
|
||||
Body = body
|
||||
};
|
||||
var fixture = new ActivityTestFixture(forActivity);
|
||||
|
||||
// Act + Assert
|
||||
var ex = await Assert.ThrowsAsync<InputEvaluationException>(() => fixture.ExecuteAsync());
|
||||
Assert.Contains(which, ex.Message, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BodyThrows_ActivitySchedules_WithoutBreaking()
|
||||
{
|
||||
// Arrange
|
||||
var body = new ThrowingBody(new InvalidOperationException("boom"));
|
||||
var forActivity = new For(1, 3, 1) { Body = body };
|
||||
var fixture = new ActivityTestFixture(forActivity);
|
||||
|
||||
// Act
|
||||
var context = await fixture.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(context);
|
||||
Assert.True(context.HasScheduledActivity(body));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, 1, 5)] // positive step computed -> ascending executes
|
||||
[InlineData(false, 5, 1)] // negative step computed -> descending executes
|
||||
public async Task DynamicStep_EvaluatedAtExecutionTime(bool usePositive, int start, int end)
|
||||
{
|
||||
// Arrange
|
||||
var body = new MockBodyActivity();
|
||||
var stepValue = usePositive ? 1 : -1; // Compute step value directly from parameter
|
||||
var forActivity = new For
|
||||
{
|
||||
Start = new Input<int>(start),
|
||||
End = new Input<int>(end),
|
||||
Step = new Input<int>((Func<int>)(() => stepValue)),
|
||||
Body = body
|
||||
};
|
||||
|
||||
var fixture = new ActivityTestFixture(forActivity);
|
||||
|
||||
// Act
|
||||
var context = await fixture.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
Assert.True(context.HasScheduledActivity(body));
|
||||
var currentValue = context.GetActivityOutput(() => forActivity.CurrentValue);
|
||||
Assert.Equal(start, currentValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NegativeStart_NegativeStep_CurrentValueIsIntAndMatchesStart()
|
||||
{
|
||||
// Arrange
|
||||
var body = new MockBodyActivity();
|
||||
var forActivity = new For(-2, -10, -2) { Body = body };
|
||||
var fixture = new ActivityTestFixture(forActivity);
|
||||
|
||||
// Act
|
||||
var context = await fixture.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
Assert.True(context.HasScheduledActivity(body));
|
||||
var currentValue = context.GetActivityOutput(() => forActivity.CurrentValue);
|
||||
Assert.IsType<int>(currentValue);
|
||||
Assert.Equal(-2, currentValue);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(10, 5, 1, true)] // ascending + inclusive, start > end -> no schedule
|
||||
[InlineData(10, 5, 1, false)] // ascending + exclusive, start > end -> no schedule
|
||||
[InlineData(0, 5, -1, true)] // descending + inclusive, start < end -> no schedule
|
||||
[InlineData(0, 5, -1, false)] // descending + exclusive, start < end -> no schedule
|
||||
public async Task StartOutsideRange_DoesNotSchedule(int start, int end, int step, bool inclusive)
|
||||
{
|
||||
// Arrange
|
||||
var body = new MockBodyActivity();
|
||||
var forActivity = new For
|
||||
{
|
||||
Start = new Input<int>(start),
|
||||
End = new Input<int>(end),
|
||||
Step = new Input<int>(step),
|
||||
OuterBoundInclusive = new Input<bool>(inclusive),
|
||||
Body = body
|
||||
};
|
||||
var fixture = new ActivityTestFixture(forActivity);
|
||||
|
||||
// Act
|
||||
var context = await fixture.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
Assert.False(context.HasScheduledActivity(body));
|
||||
}
|
||||
|
||||
private static bool ShouldExecuteLoopWithBounds(int start, int end, int step, bool inclusive)
|
||||
{
|
||||
// Match the actual For activity logic exactly
|
||||
var increment = step >= 0;
|
||||
|
||||
return increment && inclusive ? start <= end
|
||||
: increment && !inclusive ? start < end
|
||||
: !increment && inclusive ? start >= end
|
||||
: !increment && !inclusive && start > end;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mock activity to represent the body of the For loop
|
||||
/// </summary>
|
||||
private class MockBodyActivity : Activity
|
||||
{
|
||||
protected override ValueTask ExecuteAsync(ActivityExecutionContext context) => ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A body that throws a provided exception when executed
|
||||
/// </summary>
|
||||
private class ThrowingBody(Exception exception) : Activity
|
||||
{
|
||||
protected override ValueTask ExecuteAsync(ActivityExecutionContext context) => throw exception;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue