Skip to content

Commit 3456f71

Browse files
Add unit coverage for probe replay branches
1 parent d8d4219 commit 3456f71

7 files changed

Lines changed: 327 additions & 0 deletions

File tree

tests/Unit/ChildWorkflowStubTest.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,77 @@ public function testSkipsStoredExceptionForDifferentSourceClass(): void
131131
$this->assertSame(2, WorkflowStub::getContext()->index);
132132
}
133133

134+
public function testMarksProbeMatchedForMatchingStoredException(): void
135+
{
136+
$workflow = WorkflowStub::load(WorkflowStub::make(TestParentWorkflow::class)->id());
137+
$storedWorkflow = StoredWorkflow::findOrFail($workflow->id());
138+
$storedWorkflow->update([
139+
'arguments' => Serializer::serialize([]),
140+
'status' => WorkflowPendingStatus::$name,
141+
]);
142+
$storedWorkflow->logs()
143+
->create([
144+
'index' => 0,
145+
'now' => WorkflowStub::now(),
146+
'class' => WorkflowException::class,
147+
'result' => Serializer::serialize([
148+
'class' => Exception::class,
149+
'message' => 'matching child failure',
150+
'code' => 0,
151+
]),
152+
]);
153+
154+
WorkflowStub::setContext([
155+
'storedWorkflow' => $storedWorkflow,
156+
'index' => 0,
157+
'now' => now(),
158+
'replaying' => true,
159+
'probing' => true,
160+
'probeIndex' => 0,
161+
'probeClass' => TestChildWorkflow::class,
162+
'probeMatched' => false,
163+
]);
164+
165+
try {
166+
ChildWorkflowStub::make(TestChildWorkflow::class);
167+
$this->fail('Expected child exception to be thrown.');
168+
} catch (Exception $exception) {
169+
$this->assertSame('matching child failure', $exception->getMessage());
170+
}
171+
172+
$this->assertTrue(WorkflowStub::probeMatched());
173+
}
174+
175+
public function testReturnsUnresolvedPromiseWhenProbingWithoutStoredChildWorkflow(): void
176+
{
177+
$workflow = WorkflowStub::load(WorkflowStub::make(TestParentWorkflow::class)->id());
178+
$storedWorkflow = StoredWorkflow::findOrFail($workflow->id());
179+
$result = null;
180+
$storedWorkflow->update([
181+
'arguments' => Serializer::serialize([]),
182+
'status' => WorkflowPendingStatus::$name,
183+
]);
184+
185+
WorkflowStub::setContext([
186+
'storedWorkflow' => $storedWorkflow,
187+
'index' => 0,
188+
'now' => now(),
189+
'replaying' => true,
190+
'probing' => true,
191+
'probeIndex' => 0,
192+
'probeClass' => TestChildWorkflow::class,
193+
'probeMatched' => false,
194+
]);
195+
196+
ChildWorkflowStub::make(TestChildWorkflow::class)
197+
->then(static function ($value) use (&$result) {
198+
$result = $value;
199+
});
200+
201+
$this->assertNull($result);
202+
$this->assertSame(1, WorkflowStub::getContext()->index);
203+
}
204+
134205
public function testDoesNotResumeRunningStartedChildWorkflow(): void
135206
{
136207
$childWorkflow = Mockery::mock();

tests/Unit/ExceptionTest.php

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
namespace Tests\Unit;
66

77
use Exception as BaseException;
8+
use Illuminate\Contracts\Queue\Job as JobContract;
9+
use Illuminate\Support\Facades\Cache;
810
use InvalidArgumentException;
11+
use Mockery;
12+
use ReflectionMethod;
913
use RuntimeException;
1014
use Tests\Fixtures\TestActivity;
1115
use Tests\Fixtures\TestProbeBackToBackWorkflow;
@@ -54,6 +58,98 @@ public function testExceptionWorkflowRunning(): void
5458
$this->assertSame(WorkflowRunningStatus::class, $workflow->status());
5559
}
5660

61+
public function testHandleResumesWorkflowWhenLogAlreadyExists(): void
62+
{
63+
$lock = Mockery::mock();
64+
$lock->shouldReceive('get')
65+
->once()
66+
->andReturn(true);
67+
$lock->shouldReceive('release')
68+
->once();
69+
70+
Cache::shouldReceive('lock')
71+
->once()
72+
->with('laravel-workflow-exception:123', 15)
73+
->andReturn($lock);
74+
75+
$workflow = Mockery::mock();
76+
$workflow->shouldReceive('resume')
77+
->once();
78+
79+
$storedWorkflow = Mockery::mock(StoredWorkflow::class)
80+
->makePartial();
81+
$storedWorkflow->id = 123;
82+
$storedWorkflow->shouldReceive('effectiveConnection')
83+
->andReturn(null);
84+
$storedWorkflow->shouldReceive('effectiveQueue')
85+
->andReturn(null);
86+
$storedWorkflow->shouldReceive('toWorkflow')
87+
->once()
88+
->andReturn($workflow);
89+
$storedWorkflow->shouldReceive('hasLogByIndex')
90+
->once()
91+
->with(0)
92+
->andReturn(true);
93+
94+
$exception = new Exception(0, now()->toDateTimeString(), $storedWorkflow, new BaseException('existing log'));
95+
$exception->handle();
96+
97+
$this->assertSame(123, $storedWorkflow->id);
98+
99+
Mockery::close();
100+
}
101+
102+
public function testHandleReleasesWhenExceptionLockUnavailable(): void
103+
{
104+
$workflow = WorkflowStub::load(WorkflowStub::make(TestWorkflow::class)->id());
105+
$storedWorkflow = StoredWorkflow::findOrFail($workflow->id());
106+
107+
$lock = Mockery::mock();
108+
$lock->shouldReceive('get')
109+
->once()
110+
->andReturn(false);
111+
112+
Cache::shouldReceive('lock')
113+
->once()
114+
->with('laravel-workflow-exception:' . $storedWorkflow->id, 15)
115+
->andReturn($lock);
116+
117+
$job = Mockery::mock(JobContract::class);
118+
$job->shouldReceive('release')
119+
->once()
120+
->with(0);
121+
122+
$exception = new Exception(0, now()->toDateTimeString(), $storedWorkflow, [
123+
'class' => BaseException::class,
124+
'message' => 'locked',
125+
'code' => 0,
126+
]);
127+
$exception->setJob($job);
128+
$exception->handle();
129+
130+
$this->assertFalse($storedWorkflow->hasLogByIndex(0));
131+
132+
Mockery::close();
133+
}
134+
135+
public function testProbeReplayShortCircuitsWhenWorkflowClassIsInvalid(): void
136+
{
137+
$workflow = WorkflowStub::load(WorkflowStub::make(TestWorkflow::class)->id());
138+
$storedWorkflow = StoredWorkflow::findOrFail($workflow->id());
139+
$storedWorkflow->class = '';
140+
141+
$exception = new Exception(0, now()->toDateTimeString(), $storedWorkflow, [
142+
'class' => BaseException::class,
143+
'message' => 'invalid workflow class',
144+
'code' => 0,
145+
]);
146+
147+
$method = new ReflectionMethod(Exception::class, 'shouldPersistAfterProbeReplay');
148+
$method->setAccessible(true);
149+
150+
$this->assertTrue($method->invoke($exception));
151+
}
152+
57153
public function testSkipsWriteWhenProbeDoesNotReachCandidateException(): void
58154
{
59155
$workflow = WorkflowStub::load(WorkflowStub::make(TestProbeParallelChildWorkflow::class)->id());
@@ -116,7 +212,13 @@ public function testPersistsWriteWhenProbeReachesCandidateException(): void
116212
], sourceClass: TestProbeRetryActivity::class);
117213
$exception->handle();
118214

215+
$log = $storedWorkflow->fresh()
216+
->logs()
217+
->firstWhere('index', 1);
218+
219+
$this->assertNotNull($log);
119220
$this->assertTrue($storedWorkflow->fresh()->hasLogByIndex(1));
221+
$this->assertSame(TestProbeRetryActivity::class, Serializer::unserialize($log->result)['sourceClass']);
120222
}
121223

122224
public function testSkipsWriteWhenProbeReachesDifferentActivityClassAtSameIndex(): void

tests/Unit/Traits/AwaitsTest.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,36 @@ public function testResolvesConflictingResult(): void
102102
$this->assertFalse(Serializer::unserialize($workflow->logs()->firstWhere('index', 0)->result));
103103
}
104104

105+
public function testDefersWhenProbing(): void
106+
{
107+
$workflow = WorkflowStub::load(WorkflowStub::make(TestWorkflow::class)->id());
108+
$storedWorkflow = StoredWorkflow::findOrFail($workflow->id());
109+
$conditionEvaluated = false;
110+
$result = null;
111+
112+
WorkflowStub::setContext([
113+
'storedWorkflow' => $storedWorkflow,
114+
'index' => 0,
115+
'now' => now(),
116+
'replaying' => true,
117+
'probing' => true,
118+
]);
119+
120+
WorkflowStub::await(static function () use (&$conditionEvaluated): bool {
121+
$conditionEvaluated = true;
122+
123+
return true;
124+
})
125+
->then(static function ($value) use (&$result) {
126+
$result = $value;
127+
});
128+
129+
$this->assertFalse($conditionEvaluated);
130+
$this->assertNull($result);
131+
$this->assertSame(0, $workflow->logs()->count());
132+
$this->assertSame(1, WorkflowStub::getContext()->index);
133+
}
134+
105135
public function testThrowsQueryExceptionWhenNotDuplicateKey(): void
106136
{
107137
$workflow = WorkflowStub::load(WorkflowStub::make(TestWorkflow::class)->id());

tests/Unit/Traits/SideEffectsTest.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,36 @@ public function testResolvesConflictingResult(): void
8888
$this->assertSame('test', Serializer::unserialize($workflow->logs()->firstWhere('index', 0)->result));
8989
}
9090

91+
public function testDefersWhenProbing(): void
92+
{
93+
$workflow = WorkflowStub::load(WorkflowStub::make(TestWorkflow::class)->id());
94+
$storedWorkflow = StoredWorkflow::findOrFail($workflow->id());
95+
$callableEvaluated = false;
96+
$result = null;
97+
98+
WorkflowStub::setContext([
99+
'storedWorkflow' => $storedWorkflow,
100+
'index' => 0,
101+
'now' => now(),
102+
'replaying' => true,
103+
'probing' => true,
104+
]);
105+
106+
WorkflowStub::sideEffect(static function () use (&$callableEvaluated): string {
107+
$callableEvaluated = true;
108+
109+
return 'test';
110+
})
111+
->then(static function ($value) use (&$result) {
112+
$result = $value;
113+
});
114+
115+
$this->assertFalse($callableEvaluated);
116+
$this->assertNull($result);
117+
$this->assertSame(0, $workflow->logs()->count());
118+
$this->assertSame(1, WorkflowStub::getContext()->index);
119+
}
120+
91121
public function testThrowsQueryExceptionWhenNotDuplicateKey(): void
92122
{
93123
$workflow = WorkflowStub::load(WorkflowStub::make(TestWorkflow::class)->id());

tests/Unit/Traits/TimersTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,38 @@ public function testTimerReturnsUnresolvedPromiseWhenReplayingAndNoTimer(): void
242242
]);
243243
}
244244

245+
public function testTimerReturnsUnresolvedPromiseWhenProbingAndNoTimer(): void
246+
{
247+
$workflow = WorkflowStub::load(WorkflowStub::make(TestWorkflow::class)->id());
248+
$storedWorkflow = StoredWorkflow::findOrFail($workflow->id());
249+
$result = null;
250+
$storedWorkflow->update([
251+
'arguments' => Serializer::serialize([]),
252+
'status' => WorkflowPendingStatus::$name,
253+
]);
254+
255+
WorkflowStub::setContext([
256+
'storedWorkflow' => $storedWorkflow,
257+
'index' => 0,
258+
'now' => now(),
259+
'replaying' => true,
260+
'probing' => true,
261+
]);
262+
263+
WorkflowStub::timer('1 minute')
264+
->then(static function ($value) use (&$result) {
265+
$result = $value;
266+
});
267+
268+
$this->assertNull($result);
269+
$this->assertSame(1, WorkflowStub::getContext()->index);
270+
$this->assertSame(0, $workflow->logs()->count());
271+
$this->assertDatabaseMissing('workflow_timers', [
272+
'stored_workflow_id' => $workflow->id(),
273+
'index' => 0,
274+
]);
275+
}
276+
245277
public function testTimerCapsDelayForSqsDriver(): void
246278
{
247279
Bus::fake();

tests/Unit/Traits/VersionsTest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,30 @@ public function testResolvesConflictingResultThrowsWhenVersionNotSupported(): vo
191191
Mockery::close();
192192
}
193193

194+
public function testReturnsUnresolvedPromiseWhenProbingWithoutStoredVersion(): void
195+
{
196+
$workflow = WorkflowStub::load(WorkflowStub::make(TestWorkflow::class)->id());
197+
$storedWorkflow = StoredWorkflow::findOrFail($workflow->id());
198+
$result = null;
199+
200+
WorkflowStub::setContext([
201+
'storedWorkflow' => $storedWorkflow,
202+
'index' => 0,
203+
'now' => now(),
204+
'replaying' => true,
205+
'probing' => true,
206+
]);
207+
208+
WorkflowStub::getVersion('test-change', WorkflowStub::DEFAULT_VERSION, 1)
209+
->then(static function ($value) use (&$result): void {
210+
$result = $value;
211+
});
212+
213+
$this->assertNull($result);
214+
$this->assertSame(1, WorkflowStub::getContext()->index);
215+
$this->assertSame(0, $workflow->logs()->count());
216+
}
217+
194218
public function testThrowsQueryExceptionWhenNotDuplicateKey(): void
195219
{
196220
$workflow = WorkflowStub::load(WorkflowStub::make(TestWorkflow::class)->id());

tests/Unit/WorkflowStubTest.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use Illuminate\Support\Carbon;
99
use Illuminate\Support\Facades\Cache;
1010
use Illuminate\Support\Facades\Queue;
11+
use ReflectionProperty;
12+
use stdClass;
1113
use Tests\Fixtures\TestAwaitWorkflow;
1214
use Tests\Fixtures\TestBadConnectionWorkflow;
1315
use Tests\Fixtures\TestChatBotWorkflow;
@@ -231,6 +233,42 @@ public function testConnection(): void
231233
$this->assertSame('default', WorkflowStub::queue());
232234
}
233235

236+
public function testProbeHelpers(): void
237+
{
238+
$contextProperty = new ReflectionProperty(WorkflowStub::class, 'context');
239+
$contextProperty->setAccessible(true);
240+
$previousContext = $contextProperty->getValue();
241+
$contextProperty->setValue(null);
242+
243+
try {
244+
$this->assertInstanceOf(stdClass::class, WorkflowStub::getContext());
245+
$this->assertFalse(WorkflowStub::isProbing());
246+
$this->assertNull(WorkflowStub::probeIndex());
247+
$this->assertNull(WorkflowStub::probeClass());
248+
$this->assertFalse(WorkflowStub::probeMatched());
249+
250+
WorkflowStub::markProbeMatched();
251+
252+
$this->assertFalse(WorkflowStub::probeMatched());
253+
254+
WorkflowStub::setContext([
255+
'probing' => true,
256+
'probeIndex' => 7,
257+
'probeClass' => TestWorkflow::class,
258+
'probeMatched' => false,
259+
]);
260+
261+
WorkflowStub::markProbeMatched();
262+
263+
$this->assertTrue(WorkflowStub::isProbing());
264+
$this->assertSame(7, WorkflowStub::probeIndex());
265+
$this->assertSame(TestWorkflow::class, WorkflowStub::probeClass());
266+
$this->assertTrue(WorkflowStub::probeMatched());
267+
} finally {
268+
$contextProperty->setValue($previousContext);
269+
}
270+
}
271+
234272
public function testHandlesDuplicateLogInsertionProperly(): void
235273
{
236274
Queue::fake();

0 commit comments

Comments
 (0)