Skip to content

Commit 2097b4d

Browse files
authored
fix: For does not repeat correctly (#450)
This PR fixes the behavior of the `For` modifier in the Mockolate library to properly repeat callback patterns instead of limiting usage. Previously, `For` would limit callbacks to execute only once for the specified number of times. Now, `For` repeats the entire callback pattern indefinitely. ### Key Changes: - Modified `For` behavior to repeat patterns instead of limiting them to one-time execution - Renamed `For` tests to `Returns_For_ShouldRepeatUsage_ForTheSpecifiedNumber` to reflect new behavior - Moved old `For` tests to `Only` (which maintains the original limiting behavior) - Updated documentation to show the new repeating pattern behavior
1 parent 466708f commit 2097b4d

11 files changed

Lines changed: 1494 additions & 1131 deletions

Docs/pages/advanced-features/02-advanced-callback-features.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ sut.SetupMock.Property.TotalDispensed
3636
.Returns(10).For(1)
3737
.Returns(20).For(2)
3838
.Returns(30).For(3);
39-
// Reads: 10, 20, 20, 30, 30, 30, 0, 0, 0, 0
39+
// Reads: 10, 20, 20, 30, 30, 30, 10, 20, 20, 30, 30, 30
4040
```
4141

4242
### Repeat `Forever`

Source/Mockolate/Setup/Callback.cs

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,7 @@ public class Callback
3030
/// A <see cref="Callback" /> is active, until it has reached the <see cref="Only" /> limit, if specified.
3131
/// </remarks>
3232
protected bool IsActive(int matchingCount)
33-
{
34-
if (_forTimes is not null)
35-
{
36-
matchingCount -= _forTimes.Value;
37-
}
38-
39-
return _onlyTimes == 0 || matchingCount < _onlyTimes;
40-
}
33+
=> _onlyTimes == 0 || matchingCount < _onlyTimes * (_forTimes ?? 1);
4134

4235
/// <summary>
4336
/// Limits the callback to only execute for property accesses where the predicate returns <see langword="true" />.
@@ -105,6 +98,7 @@ protected bool CheckMatching(int matchingCount)
10598
/// </summary>
10699
public class Callback<TDelegate>(TDelegate @delegate) : Callback where TDelegate : Delegate
107100
{
101+
private int _forIterationCount;
108102
private int _invocationCount;
109103
private int _matchingCount;
110104

@@ -116,7 +110,7 @@ public bool Invoke(bool wasInvoked, ref int index, Action<int, TDelegate> callba
116110
{
117111
if (IsActive(_matchingCount) && CheckInvocations(_invocationCount))
118112
{
119-
if (CheckMatching(_matchingCount))
113+
if (CheckMatching(_forIterationCount))
120114
{
121115
_invocationCount++;
122116

@@ -127,14 +121,20 @@ public bool Invoke(bool wasInvoked, ref int index, Action<int, TDelegate> callba
127121
Interlocked.Increment(ref index);
128122
}
129123

124+
_forIterationCount++;
130125
_matchingCount++;
131126
callback(_invocationCount - 1, @delegate);
132127
}
133128
else if (!wasInvoked)
134129
{
135-
if (!HasForSpecified || !CheckMatching(_matchingCount + 1))
130+
if (!HasForSpecified || !CheckMatching(_forIterationCount + 1))
136131
{
137132
Interlocked.Increment(ref index);
133+
_forIterationCount = 0;
134+
}
135+
else
136+
{
137+
_forIterationCount++;
138138
}
139139

140140
_matchingCount++;
@@ -144,6 +144,7 @@ public bool Invoke(bool wasInvoked, ref int index, Action<int, TDelegate> callba
144144
return !RunInParallel;
145145
}
146146

147+
_forIterationCount++;
147148
_matchingCount++;
148149
}
149150

@@ -159,11 +160,16 @@ public bool Invoke(ref int index, Action<int, TDelegate> callback)
159160
{
160161
if (IsActive(_matchingCount) && CheckInvocations(_invocationCount))
161162
{
162-
if (CheckMatching(_matchingCount))
163+
if (CheckMatching(_forIterationCount))
163164
{
164-
if (!HasForSpecified || !CheckMatching(_matchingCount + 1))
165+
if (!HasForSpecified || !CheckMatching(_forIterationCount + 1))
165166
{
166167
Interlocked.Increment(ref index);
168+
_forIterationCount = 0;
169+
}
170+
else
171+
{
172+
_forIterationCount++;
167173
}
168174

169175
_invocationCount++;
@@ -172,6 +178,7 @@ public bool Invoke(ref int index, Action<int, TDelegate> callback)
172178
return true;
173179
}
174180

181+
_forIterationCount++;
175182
_matchingCount++;
176183
}
177184

@@ -188,11 +195,16 @@ public bool Invoke<TReturn>(ref int index, Func<int, TDelegate, TReturn> callbac
188195
{
189196
if (IsActive(_matchingCount) && CheckInvocations(_invocationCount))
190197
{
191-
if (CheckMatching(_matchingCount))
198+
if (CheckMatching(_forIterationCount))
192199
{
193-
if (!HasForSpecified || !CheckMatching(_matchingCount + 1))
200+
if (!HasForSpecified || !CheckMatching(_forIterationCount + 1))
194201
{
195202
Interlocked.Increment(ref index);
203+
_forIterationCount = 0;
204+
}
205+
else
206+
{
207+
_forIterationCount++;
196208
}
197209

198210
_invocationCount++;
@@ -201,6 +213,7 @@ public bool Invoke<TReturn>(ref int index, Func<int, TDelegate, TReturn> callbac
201213
return true;
202214
}
203215

216+
_forIterationCount++;
204217
_matchingCount++;
205218
}
206219

Tests/Mockolate.Internal.Tests/CallbackTests.cs

Lines changed: 17 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public async Task ShouldIncludeIndexWhenMatching()
1313
bool wasInvoked = false;
1414
List<int> values = [];
1515
Callback<Action> sut = new(() => { });
16-
sut.For(2);
16+
sut.Only(2);
1717
sut.When(v => v > 1);
1818

1919
int index = 0;
@@ -26,16 +26,16 @@ public async Task ShouldIncludeIndexWhenMatching()
2626
}
2727

2828
[Theory]
29-
[InlineData(2, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1)]
30-
[InlineData(2, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1)]
29+
[InlineData(2, 0, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2)]
30+
[InlineData(2, 1, 0, 0, 1, 2, 2, 2, 2, 2, 2, 2)]
3131
public async Task ShouldIncrementIndexOnceWhenCallbackIsExhausted(
32-
int forValue, int whenMinimum, params int[] expectResult)
32+
int only, int when, params int[] expectResult)
3333
{
3434
bool wasInvoked = false;
3535
List<int> indexValues = [];
3636
Callback<Action> sut = new(() => { });
37-
sut.For(forValue);
38-
sut.When(v => v > whenMinimum);
37+
sut.Only(only);
38+
sut.When(v => v > when);
3939

4040
int index = 0;
4141
for (int iteration = 1; iteration <= 10; iteration++)
@@ -51,7 +51,7 @@ public async Task ShouldIncrementIndexOnceWhenCallbackIsExhausted(
5151
public async Task WhenCheck_ShouldOnlyBeCalledWhenOnlyLimitIsNotReached()
5252
{
5353
bool wasInvoked = false;
54-
int[] expectResult = [0, 1, 2, 3, 4, 5,];
54+
int[] expectResult = [0, 1, 2, 3, 4, 5, 6, 7,];
5555
List<int> invocationChecks = [];
5656
int invocationCount = 0;
5757
Callback<Action> sut = new(() => { invocationCount++; });
@@ -70,7 +70,7 @@ public async Task WhenCheck_ShouldOnlyBeCalledWhenOnlyLimitIsNotReached()
7070
}
7171

7272
await That(invocationChecks).IsEqualTo(expectResult);
73-
await That(invocationCount).IsEqualTo(2);
73+
await That(invocationCount).IsEqualTo(8);
7474
}
7575

7676
[Theory]
@@ -84,7 +84,7 @@ public async Task WithForAndWhen_ShouldMatchInExpectedIterations(
8484
{
8585
bool wasInvoked = false;
8686
Callback<Action> sut = new(() => { });
87-
sut.For(2);
87+
sut.Only(2);
8888
sut.When(v => v > 1);
8989

9090
int index = 0;
@@ -105,7 +105,7 @@ public async Task ShouldIncludeIndexWhenMatching()
105105
{
106106
List<int> values = [];
107107
Callback<Action> sut = new(() => { });
108-
sut.For(2);
108+
sut.Only(2);
109109
sut.When(v => v > 1);
110110

111111
int index = 0;
@@ -117,39 +117,10 @@ public async Task ShouldIncludeIndexWhenMatching()
117117
await That(values).IsEqualTo([2, 3,]);
118118
}
119119

120-
[Theory]
121-
[InlineData(2, 0, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9)]
122-
[InlineData(2, 1, 1, 2, 2, 3, 4, 5, 6, 7, 8, 9)]
123-
[InlineData(3, 1, 1, 2, 2, 2, 3, 4, 5, 6, 7, 8)]
124-
[InlineData(5, 4, 1, 2, 3, 4, 5, 5, 5, 5, 5, 6)]
125-
[InlineData(6, 4, 1, 2, 3, 4, 5, 5, 5, 5, 5, 5)]
126-
[InlineData(7, 4, 1, 2, 3, 4, 5, 5, 5, 5, 5, 5)]
127-
[InlineData(3, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)]
128-
public async Task ShouldIncrementIndexWhenForIsNotActive(
129-
int forValue, int whenMinimum, params int[] expectResult)
130-
{
131-
List<int> indexValues = [];
132-
int invocationCount = 0;
133-
int expectedInvocationCount = Math.Max(0, Math.Min(forValue, 9 - whenMinimum));
134-
Callback<Action> sut = new(() => { invocationCount++; });
135-
sut.For(forValue);
136-
sut.When(v => v > whenMinimum);
137-
138-
int index = 0;
139-
for (int iteration = 1; iteration <= 10; iteration++)
140-
{
141-
sut.Invoke(ref index, (_, c) => c());
142-
indexValues.Add(index);
143-
}
144-
145-
await That(indexValues).IsEqualTo(expectResult);
146-
await That(invocationCount).IsEqualTo(expectedInvocationCount);
147-
}
148-
149120
[Fact]
150121
public async Task WhenCheck_ShouldOnlyBeCalledWhenOnlyLimitIsNotReached()
151122
{
152-
int[] expectResult = [0, 1, 2, 3, 4, 5,];
123+
int[] expectResult = [0, 1, 2, 3, 4, 5, 6, 7,];
153124
List<int> invocationChecks = [];
154125
int invocationCount = 0;
155126
Callback<Action> sut = new(() => { invocationCount++; });
@@ -168,7 +139,7 @@ public async Task WhenCheck_ShouldOnlyBeCalledWhenOnlyLimitIsNotReached()
168139
}
169140

170141
await That(invocationChecks).IsEqualTo(expectResult);
171-
await That(invocationCount).IsEqualTo(2);
142+
await That(invocationCount).IsEqualTo(8);
172143
}
173144

174145
[Theory]
@@ -181,7 +152,7 @@ public async Task WithForAndWhen_ShouldMatchInExpectedIterations(
181152
int iterations, bool expectResult)
182153
{
183154
Callback<Action> sut = new(() => { });
184-
sut.For(2);
155+
sut.Only(2);
185156
sut.When(v => v > 1);
186157

187158
int index = 0;
@@ -202,7 +173,7 @@ public async Task ShouldIncludeIndexWhenMatching()
202173
{
203174
List<int> values = [];
204175
Callback<Action> sut = new(() => { });
205-
sut.For(2);
176+
sut.Only(2);
206177
sut.When(v => v > 1);
207178

208179
int index = 0;
@@ -218,43 +189,10 @@ public async Task ShouldIncludeIndexWhenMatching()
218189
await That(values).IsEqualTo([2, 3,]);
219190
}
220191

221-
[Theory]
222-
[InlineData(2, 0, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9)]
223-
[InlineData(2, 1, 1, 2, 2, 3, 4, 5, 6, 7, 8, 9)]
224-
[InlineData(3, 1, 1, 2, 2, 2, 3, 4, 5, 6, 7, 8)]
225-
[InlineData(5, 4, 1, 2, 3, 4, 5, 5, 5, 5, 5, 6)]
226-
[InlineData(6, 4, 1, 2, 3, 4, 5, 5, 5, 5, 5, 5)]
227-
[InlineData(7, 4, 1, 2, 3, 4, 5, 5, 5, 5, 5, 5)]
228-
[InlineData(3, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)]
229-
public async Task ShouldIncrementIndexWhenForIsNotActive(
230-
int forValue, int whenMinimum, params int[] expectResult)
231-
{
232-
List<int> indexValues = [];
233-
int invocationCount = 0;
234-
int expectedInvocationCount = Math.Max(0, Math.Min(forValue, 9 - whenMinimum));
235-
Callback<Action> sut = new(() => { invocationCount++; });
236-
sut.For(forValue);
237-
sut.When(v => v > whenMinimum);
238-
239-
int index = 0;
240-
for (int iteration = 1; iteration <= 10; iteration++)
241-
{
242-
sut.Invoke(ref index, (_, c) =>
243-
{
244-
c();
245-
return "foo";
246-
}, out string? _);
247-
indexValues.Add(index);
248-
}
249-
250-
await That(indexValues).IsEqualTo(expectResult);
251-
await That(invocationCount).IsEqualTo(expectedInvocationCount);
252-
}
253-
254192
[Fact]
255193
public async Task WhenCheck_ShouldOnlyBeCalledWhenOnlyLimitIsNotReached()
256194
{
257-
int[] expectResult = [0, 1, 2, 3, 4, 5,];
195+
int[] expectResult = [0, 1, 2, 3, 4, 5, 6, 7,];
258196
List<int> invocationChecks = [];
259197
int invocationCount = 0;
260198
Callback<Action> sut = new(() => { invocationCount++; });
@@ -277,7 +215,7 @@ public async Task WhenCheck_ShouldOnlyBeCalledWhenOnlyLimitIsNotReached()
277215
}
278216

279217
await That(invocationChecks).IsEqualTo(expectResult);
280-
await That(invocationCount).IsEqualTo(2);
218+
await That(invocationCount).IsEqualTo(8);
281219
}
282220

283221
[Theory]
@@ -290,7 +228,7 @@ public async Task WithForAndWhen_ShouldMatchInExpectedIterations(
290228
int iterations, bool expectResult)
291229
{
292230
Callback<Action> sut = new(() => { });
293-
sut.For(2);
231+
sut.Only(2);
294232
sut.When(v => v > 1);
295233

296234
int index = 0;

0 commit comments

Comments
 (0)