From 0533d5ce09980e41308e83a159356ca2e8123112 Mon Sep 17 00:00:00 2001 From: "lucas.hipolito" Date: Mon, 27 Oct 2025 14:55:50 +0100 Subject: [PATCH] Refactoring and improvements of For unit tests --- .../Looping/ForTests.cs | 272 +++++++++++++++++- 1 file changed, 257 insertions(+), 15 deletions(-) diff --git a/test/unit/Elsa.Activities.UnitTests/Looping/ForTests.cs b/test/unit/Elsa.Activities.UnitTests/Looping/ForTests.cs index c60367e91..4428660ec 100644 --- a/test/unit/Elsa.Activities.UnitTests/Looping/ForTests.cs +++ b/test/unit/Elsa.Activities.UnitTests/Looping/ForTests.cs @@ -2,6 +2,7 @@ using Elsa.Testing.Shared; using Elsa.Extensions; using Elsa.Workflows; using Elsa.Workflows.Behaviors; +using Elsa.Workflows.Exceptions; namespace Elsa.Activities.UnitTests.Looping; @@ -201,23 +202,259 @@ public class ForTests // Assert Assert.Equal(shouldExecute, context.HasScheduledActivity(mockBody)); } - - // Private helper methods - private static bool ShouldExecuteLoop(int start, int end, int step) + + // 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) { - return ShouldExecuteLoopWithBounds(start, end, step, true); - } + // Arrange + var body = new MockBodyActivity(); + var forActivity = new For + { + Start = new Input(start), + End = new Input(end), + Step = new Input(0), // zero step: current contract allows first schedule + OuterBoundInclusive = new Input(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(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(start), + End = new Input(end), + Step = new Input(step), + OuterBoundInclusive = new Input(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(start), + End = new Input(end), + Step = new Input(step), + OuterBoundInclusive = new Input(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(start), + End = new Input(end), + Step = new Input(step), + OuterBoundInclusive = new Input(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((Func)(() => throw new ApplicationException("start!"))) : new Input(1), + End = which == "end" ? new Input((Func)(() => throw new ApplicationException("end!"))) : new Input(3), + Step = which == "step" ? new Input((Func)(() => throw new ApplicationException("step!"))) : new Input(1), + Body = body + }; + var fixture = new ActivityTestFixture(forActivity); + + // Act + Assert + var ex = await Assert.ThrowsAsync(() => 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(start), + End = new Input(end), + Step = new Input((Func)(() => 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(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(start), + End = new Input(end), + Step = new Input(step), + OuterBoundInclusive = new Input(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; - var currentValue = start; - - return increment && inclusive ? currentValue <= end - : increment && !inclusive ? currentValue < end - : !increment && inclusive ? currentValue >= end - : !increment && !inclusive ? currentValue > end : false; + + return increment && inclusive ? start <= end + : increment && !inclusive ? start < end + : !increment && inclusive ? start >= end + : !increment && !inclusive && start > end; } /// @@ -225,9 +462,14 @@ public class ForTests /// private class MockBodyActivity : Activity { - protected override ValueTask ExecuteAsync(ActivityExecutionContext context) - { - return ValueTask.CompletedTask; - } + protected override ValueTask ExecuteAsync(ActivityExecutionContext context) => ValueTask.CompletedTask; + } + + /// + /// A body that throws a provided exception when executed + /// + private class ThrowingBody(Exception exception) : Activity + { + protected override ValueTask ExecuteAsync(ActivityExecutionContext context) => throw exception; } }