Skip to content

Commit 0262946

Browse files
committed
chore: add test coverage and update docs
1 parent c2adecb commit 0262946

5 files changed

Lines changed: 133 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
All notable changes to this project will be documented in this file. This project adhere to the [Semantic Versioning](http://semver.org/) standard.
44

5-
## [0.1.0] 2025-12-16
5+
## [0.1.0] 2025-12-17
66

77
* Feature - Introduces a method `run` to the Regulator class which enables running a set of tasks synchronously.
88

CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,9 @@ shepherd()->dispatch(new My_Task($arg1, $arg2), 300); // 5 minutes
110110
shepherd()->run(
111111
[ new My_Task($arg1, $arg2), new Another_Task() ],
112112
[
113-
'before' => function( $task ) { /* called before each task */ },
114-
'after' => function( $task ) { /* called after each task */ },
115-
'always' => function( $tasks ) { /* called after all tasks */ },
113+
'before' => function( $task ) { /* called before each task */ },
114+
'after' => function( $task ) { /* called after each task */ },
115+
'always' => function( $tasks ) { /* called after all tasks complete successfully */ },
116116
]
117117
);
118118

docs/advanced-usage.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ shepherd()->run( $tasks, [
409409
error_log( 'Completed task: ' . get_class( $task ) );
410410
},
411411

412-
// Called after all tasks complete (even if some failed)
412+
// Called after all tasks complete (even on error)
413413
'always' => function( array $tasks ): void {
414414
error_log( 'Finished processing ' . count( $tasks ) . ' tasks' );
415415
},
@@ -483,6 +483,7 @@ register_rest_route( 'myapp/v1', '/process', [
483483
- **Already scheduled tasks**: If a task was previously dispatched via `dispatch()`, `run()` will execute it without re-dispatching
484484
- **Fallback mode**: When Shepherd's database tables are not registered, tasks execute immediately via `process()` without Action Scheduler
485485
- **Context detection**: Shepherd automatically detects CLI and REST contexts for proper logging
486+
- **Exception handling**: Exceptions or Throwables thrown inside callables (`before`, `after`, `always`) are caught and trigger the `tasks_run_failed` action
486487

487488
### WordPress Hooks
488489

@@ -501,12 +502,13 @@ add_action( "shepherd_{$prefix}_task_after_run", function( Task $task ) {
501502
// Post-task cleanup or notifications
502503
}, 10, 1 );
503504

504-
// Fired when any task fails
505-
add_action( "shepherd_{$prefix}_tasks_run_failed", function( ?Task $task, Exception $e ) {
506-
// Handle batch failure
505+
// Fired when any task or callable fails (catches Exception and Throwable)
506+
add_action( "shepherd_{$prefix}_tasks_run_failed", function( array $tasks, Throwable $e ) {
507+
// Handle batch failure - receives all tasks and the exception/error
508+
error_log( 'Tasks failed: ' . $e->getMessage() );
507509
}, 10, 2 );
508510

509-
// Fired after all tasks complete
511+
// Fired after all tasks complete successfully
510512
add_action( "shepherd_{$prefix}_tasks_finished", function( array $tasks ) {
511513
// Batch completion handling
512514
}, 10, 1 );

docs/api-reference.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -561,8 +561,8 @@ Table name: `shepherd_{prefix}_task_logs`
561561
- `shepherd_{prefix}_task_after_run` - Fired after a task completes via `run()` (since 0.1.0)
562562
- Parameters: `$task` (Task instance)
563563

564-
- `shepherd_{prefix}_tasks_run_failed` - Fired when a task fails during `run()` (since 0.1.0)
565-
- Parameters: `$task` (Task instance or null), `$exception` (Exception)
564+
- `shepherd_{prefix}_tasks_run_failed` - Fired when a task or callable fails during `run()` (since 0.1.0)
565+
- Parameters: `$tasks` (array of Task instances), `$exception` (Exception or Throwable)
566566

567567
- `shepherd_{prefix}_tasks_finished` - Fired after all tasks have been processed via `run()` (since 0.1.0)
568568
- Parameters: `$tasks` (array of Task instances)

tests/integration/Regulator_Test.php

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,4 +337,124 @@ public function it_should_run_single_task_successfully(): void {
337337
$this->assertTrue( $after_called, 'after callable should have been called' );
338338
$this->assertTrue( $always_called, 'always callable should have been called' );
339339
}
340+
341+
/**
342+
* @test
343+
*/
344+
public function it_should_catch_exception_thrown_in_before_callable(): void {
345+
$shepherd = shepherd();
346+
$prefix = tests_shepherd_get_hook_prefix();
347+
348+
$task = new Do_Action_Task();
349+
350+
$run_failed_count = did_action( "shepherd_{$prefix}_tasks_run_failed" );
351+
$captured_tasks = null;
352+
$captured_exception = null;
353+
354+
add_action( "shepherd_{$prefix}_tasks_run_failed", function( $tasks, $e ) use ( &$captured_tasks, &$captured_exception ) {
355+
$captured_tasks = $tasks;
356+
$captured_exception = $e;
357+
}, 10, 2 );
358+
359+
$shepherd->run( [ $task ], [
360+
'before' => function() {
361+
throw new Exception( 'Before callable failed' );
362+
},
363+
] );
364+
365+
$this->assertSame( $run_failed_count + 1, did_action( "shepherd_{$prefix}_tasks_run_failed" ), 'tasks_run_failed action should have fired' );
366+
$this->assertIsArray( $captured_tasks );
367+
$this->assertCount( 1, $captured_tasks );
368+
$this->assertInstanceOf( Exception::class, $captured_exception );
369+
$this->assertSame( 'Before callable failed', $captured_exception->getMessage() );
370+
}
371+
372+
/**
373+
* @test
374+
*/
375+
public function it_should_catch_exception_thrown_in_after_callable(): void {
376+
$shepherd = shepherd();
377+
$prefix = tests_shepherd_get_hook_prefix();
378+
379+
$task = new Do_Action_Task();
380+
381+
$run_failed_count = did_action( "shepherd_{$prefix}_tasks_run_failed" );
382+
$captured_exception = null;
383+
384+
add_action( "shepherd_{$prefix}_tasks_run_failed", function( $tasks, $e ) use ( &$captured_exception ) {
385+
$captured_exception = $e;
386+
}, 10, 2 );
387+
388+
$shepherd->run( [ $task ], [
389+
'after' => function() {
390+
throw new Exception( 'After callable failed' );
391+
},
392+
] );
393+
394+
// Task should have run before the after callable threw
395+
$this->assertSame( 1, did_action( $task->get_task_name() ), 'Task should have run before after callable failed' );
396+
$this->assertSame( $run_failed_count + 1, did_action( "shepherd_{$prefix}_tasks_run_failed" ), 'tasks_run_failed action should have fired' );
397+
$this->assertInstanceOf( Exception::class, $captured_exception );
398+
$this->assertSame( 'After callable failed', $captured_exception->getMessage() );
399+
}
400+
401+
/**
402+
* @test
403+
*/
404+
public function it_should_catch_exception_thrown_in_always_callable(): void {
405+
$shepherd = shepherd();
406+
$prefix = tests_shepherd_get_hook_prefix();
407+
408+
$task = new Do_Action_Task();
409+
410+
$run_failed_count = did_action( "shepherd_{$prefix}_tasks_run_failed" );
411+
$tasks_finished_count = did_action( "shepherd_{$prefix}_tasks_finished" );
412+
$captured_exception = null;
413+
414+
add_action( "shepherd_{$prefix}_tasks_run_failed", function( $tasks, $e ) use ( &$captured_exception ) {
415+
$captured_exception = $e;
416+
}, 10, 2 );
417+
418+
$shepherd->run( [ $task ], [
419+
'always' => function() {
420+
throw new Exception( 'Always callable failed' );
421+
},
422+
] );
423+
424+
// Task should have run successfully
425+
$this->assertSame( 1, did_action( $task->get_task_name() ), 'Task should have run' );
426+
// tasks_finished should NOT have fired because always callable threw before it
427+
$this->assertSame( $tasks_finished_count, did_action( "shepherd_{$prefix}_tasks_finished" ), 'tasks_finished should not have fired' );
428+
// tasks_run_failed should have fired
429+
$this->assertSame( $run_failed_count + 1, did_action( "shepherd_{$prefix}_tasks_run_failed" ), 'tasks_run_failed action should have fired' );
430+
$this->assertInstanceOf( Exception::class, $captured_exception );
431+
$this->assertSame( 'Always callable failed', $captured_exception->getMessage() );
432+
}
433+
434+
/**
435+
* @test
436+
*/
437+
public function it_should_catch_throwable_in_callable(): void {
438+
$shepherd = shepherd();
439+
$prefix = tests_shepherd_get_hook_prefix();
440+
441+
$task = new Do_Action_Task();
442+
443+
$run_failed_count = did_action( "shepherd_{$prefix}_tasks_run_failed" );
444+
$captured_throwable = null;
445+
446+
add_action( "shepherd_{$prefix}_tasks_run_failed", function( $tasks, $e ) use ( &$captured_throwable ) {
447+
$captured_throwable = $e;
448+
}, 10, 2 );
449+
450+
$shepherd->run( [ $task ], [
451+
'before' => function() {
452+
throw new \Error( 'Type error in callable' );
453+
},
454+
] );
455+
456+
$this->assertSame( $run_failed_count + 1, did_action( "shepherd_{$prefix}_tasks_run_failed" ), 'tasks_run_failed action should have fired for Throwable' );
457+
$this->assertInstanceOf( \Throwable::class, $captured_throwable );
458+
$this->assertSame( 'Type error in callable', $captured_throwable->getMessage() );
459+
}
340460
}

0 commit comments

Comments
 (0)