forked from laravel/framework
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[8.x] Add scheduler integration tests (laravel#39862)
* Add scheduler test for callback events * Add exception handling test * Added integration test for command scheduler * Move everything to our `artisan` script * Add a cache implementation * Adjust container call * Restore original artisan command just in case * Code style * Clear container during teardown * Formatting
- Loading branch information
Showing
2 changed files
with
349 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
<?php | ||
|
||
namespace Illuminate\Tests\Integration\Console; | ||
|
||
use Illuminate\Cache\ArrayStore; | ||
use Illuminate\Cache\Repository; | ||
use Illuminate\Console\Events\ScheduledTaskFailed; | ||
use Illuminate\Console\Scheduling\CacheEventMutex; | ||
use Illuminate\Console\Scheduling\CacheSchedulingMutex; | ||
use Illuminate\Console\Scheduling\EventMutex; | ||
use Illuminate\Console\Scheduling\Schedule; | ||
use Illuminate\Console\Scheduling\SchedulingMutex; | ||
use Illuminate\Container\Container; | ||
use Illuminate\Contracts\Cache\Factory; | ||
use Illuminate\Contracts\Events\Dispatcher; | ||
use Orchestra\Testbench\TestCase; | ||
use RuntimeException; | ||
|
||
class CallbackSchedulingTest extends TestCase | ||
{ | ||
protected $log = []; | ||
|
||
protected function setUp(): void | ||
{ | ||
parent::setUp(); | ||
|
||
$cache = new class implements Factory | ||
{ | ||
public $store; | ||
|
||
public function __construct() | ||
{ | ||
$this->store = new Repository(new ArrayStore(true)); | ||
} | ||
|
||
public function store($name = null) | ||
{ | ||
return $this->store; | ||
} | ||
}; | ||
|
||
$container = Container::getInstance(); | ||
|
||
$container->instance(EventMutex::class, new CacheEventMutex($cache)); | ||
$container->instance(SchedulingMutex::class, new CacheSchedulingMutex($cache)); | ||
} | ||
|
||
protected function tearDown(): void | ||
{ | ||
Container::setInstance(null); | ||
|
||
parent::tearDown(); | ||
} | ||
|
||
/** | ||
* @dataProvider executionProvider | ||
*/ | ||
public function testExecutionOrder($background) | ||
{ | ||
$event = $this->app->make(Schedule::class) | ||
->call($this->logger('call')) | ||
->after($this->logger('after 1')) | ||
->before($this->logger('before 1')) | ||
->after($this->logger('after 2')) | ||
->before($this->logger('before 2')); | ||
|
||
if ($background) { | ||
$event->runInBackground(); | ||
} | ||
|
||
$this->artisan('schedule:run'); | ||
|
||
$this->assertLogged('before 1', 'before 2', 'call', 'after 1', 'after 2'); | ||
} | ||
|
||
public function testExceptionHandlingInCallback() | ||
{ | ||
$event = $this->app->make(Schedule::class) | ||
->call($this->logger('call')) | ||
->name('test-event') | ||
->withoutOverlapping(); | ||
|
||
// Set up "before" and "after" hooks to ensure they're called | ||
$event->before($this->logger('before'))->after($this->logger('after')); | ||
|
||
// Register a hook to validate that the mutex was initially created | ||
$mutexWasCreated = false; | ||
$event->before(function () use (&$mutexWasCreated, $event) { | ||
$mutexWasCreated = $event->mutex->exists($event); | ||
}); | ||
|
||
// We'll trigger an exception in an "after" hook to test exception handling | ||
$event->after(function () { | ||
throw new RuntimeException; | ||
}); | ||
|
||
// Because exceptions are caught by the ScheduleRunCommand, we need to listen for | ||
// the "failed" event to check whether our exception was actually thrown | ||
$failed = false; | ||
$this->app->make(Dispatcher::class) | ||
->listen(ScheduledTaskFailed::class, function (ScheduledTaskFailed $failure) use (&$failed, $event) { | ||
if ($failure->task === $event) { | ||
$failed = true; | ||
} | ||
}); | ||
|
||
$this->artisan('schedule:run'); | ||
|
||
// Hooks and execution should happn in correct order | ||
$this->assertLogged('before', 'call', 'after'); | ||
|
||
// Our exception should have resulted in a failure event | ||
$this->assertTrue($failed); | ||
|
||
// Validate that the mutex was originally created, but that it's since | ||
// been removed (even though an exception was thrown) | ||
$this->assertTrue($mutexWasCreated); | ||
$this->assertFalse($event->mutex->exists($event)); | ||
} | ||
|
||
public function executionProvider() | ||
{ | ||
return [ | ||
'Foreground' => [false], | ||
'Background' => [true], | ||
]; | ||
} | ||
|
||
protected function logger($message) | ||
{ | ||
return function () use ($message) { | ||
$this->log[] = $message; | ||
}; | ||
} | ||
|
||
protected function assertLogged(...$message) | ||
{ | ||
$this->assertEquals($message, $this->log); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,209 @@ | ||
<?php | ||
|
||
namespace Illuminate\Tests\Integration\Console; | ||
|
||
use Illuminate\Console\Scheduling\Schedule; | ||
use Illuminate\Filesystem\Filesystem; | ||
use Illuminate\Support\Str; | ||
use Orchestra\Testbench\TestCase; | ||
|
||
class CommandSchedulingTest extends TestCase | ||
{ | ||
/** | ||
* Each run of this test is assigned a random ID to ensure that separate runs | ||
* do not interfere with each other. | ||
* | ||
* @var string | ||
*/ | ||
protected $id; | ||
|
||
/** | ||
* The path to the file that execution logs will be written to. | ||
* | ||
* @var string | ||
*/ | ||
protected $logfile; | ||
|
||
/** | ||
* Just in case Testbench starts to ship an `artisan` script, we'll check and save a backup. | ||
* | ||
* @var string|null | ||
*/ | ||
protected $originalArtisan; | ||
|
||
/** | ||
* The Filesystem instance for writing stubs and logs. | ||
* | ||
* @var \Illuminate\Filesystem\Filesystem | ||
*/ | ||
protected $fs; | ||
|
||
protected function setUp(): void | ||
{ | ||
parent::setUp(); | ||
|
||
$this->fs = new Filesystem; | ||
|
||
$this->id = Str::random(); | ||
$this->logfile = storage_path("logs/command_scheduling_test_{$this->id}.log"); | ||
|
||
$this->writeArtisanScript(); | ||
} | ||
|
||
protected function tearDown(): void | ||
{ | ||
$this->fs->delete($this->logfile); | ||
$this->fs->delete(base_path('artisan')); | ||
|
||
if (! is_null($this->originalArtisan)) { | ||
$this->fs->put(base_path('artisan'), $this->originalArtisan); | ||
} | ||
|
||
parent::tearDown(); | ||
} | ||
|
||
/** | ||
* @dataProvider executionProvider | ||
*/ | ||
public function testExecutionOrder($background) | ||
{ | ||
$event = $this->app->make(Schedule::class) | ||
->command("test:{$this->id}") | ||
->onOneServer() | ||
->after(function () { | ||
$this->fs->append($this->logfile, "after\n"); | ||
}) | ||
->before(function () { | ||
$this->fs->append($this->logfile, "before\n"); | ||
}); | ||
|
||
if ($background) { | ||
$event->runInBackground(); | ||
} | ||
|
||
// We'll trigger the scheduler three times to simulate multiple servers | ||
$this->artisan('schedule:run'); | ||
$this->artisan('schedule:run'); | ||
$this->artisan('schedule:run'); | ||
|
||
if ($background) { | ||
// Since our command is running in a separate process, we need to wait | ||
// until it has finished executing before running our assertions. | ||
$this->waitForLogMessages('before', 'handled', 'after'); | ||
} | ||
|
||
$this->assertLogged('before', 'handled', 'after'); | ||
} | ||
|
||
public function executionProvider() | ||
{ | ||
return [ | ||
'Foreground' => [false], | ||
'Background' => [true], | ||
]; | ||
} | ||
|
||
protected function waitForLogMessages(...$messages) | ||
{ | ||
$tries = 0; | ||
$sleep = 100000; // 100K microseconds = 0.1 second | ||
$limit = 50; // 0.1s * 50 = 5 second wait limit | ||
|
||
do { | ||
$log = $this->fs->get($this->logfile); | ||
|
||
if (Str::containsAll($log, $messages)) { | ||
return; | ||
} | ||
|
||
$tries++; | ||
usleep($sleep); | ||
} while ($tries < $limit); | ||
} | ||
|
||
protected function assertLogged(...$messages) | ||
{ | ||
$log = trim($this->fs->get($this->logfile)); | ||
|
||
$this->assertEquals(implode("\n", $messages), $log); | ||
} | ||
|
||
protected function writeArtisanScript() | ||
{ | ||
$path = base_path('artisan'); | ||
|
||
// Save existing artisan script if there is one | ||
if ($this->fs->exists($path)) { | ||
$this->originalArtisan = $this->fs->get($path); | ||
} | ||
|
||
$thisFile = __FILE__; | ||
$logfile = var_export($this->logfile, true); | ||
|
||
$script = <<<PHP | ||
#!/usr/bin/env php | ||
<?php | ||
// This is a custom artisan script made specifically for: | ||
// | ||
// {$thisFile} | ||
// | ||
// It should be automatically cleaned up when the tests have finished executing. | ||
// If you are seeing this file, an unexpected error must have occurred. Please | ||
// manually remove it. | ||
define('LARAVEL_START', microtime(true)); | ||
require __DIR__.'/../../../autoload.php'; | ||
\$app = require_once __DIR__.'/bootstrap/app.php'; | ||
\$kernel = \$app->make(Illuminate\Contracts\Console\Kernel::class); | ||
// Here is our custom command for the test | ||
class CommandSchedulingTestCommand_{$this->id} extends Illuminate\Console\Command | ||
{ | ||
protected \$signature = 'test:{$this->id}'; | ||
public function handle() | ||
{ | ||
\$logfile = {$logfile}; | ||
(new Illuminate\Filesystem\Filesystem)->append(\$logfile, "handled\\n"); | ||
} | ||
} | ||
// Register command with Kernel | ||
Illuminate\Console\Application::starting(function (\$artisan) { | ||
\$artisan->add(new CommandSchedulingTestCommand_{$this->id}); | ||
}); | ||
// Add command to scheduler so that the after() callback is trigger in our spawned process | ||
Illuminate\Foundation\Application::getInstance() | ||
->booted(function (\$app) { | ||
\$app->resolving(Illuminate\Console\Scheduling\Schedule::class, function(\$schedule) { | ||
\$fs = new Illuminate\Filesystem\Filesystem; | ||
\$schedule->command("test:{$this->id}") | ||
->after(function() use (\$fs) { | ||
\$logfile = {$logfile}; | ||
\$fs->append(\$logfile, "after\\n"); | ||
}) | ||
->before(function() use (\$fs) { | ||
\$logfile = {$logfile}; | ||
\$fs->append(\$logfile, "before\\n"); | ||
}); | ||
}); | ||
}); | ||
\$status = \$kernel->handle( | ||
\$input = new Symfony\Component\Console\Input\ArgvInput, | ||
new Symfony\Component\Console\Output\ConsoleOutput | ||
); | ||
\$kernel->terminate(\$input, \$status); | ||
exit(\$status); | ||
PHP; | ||
|
||
$this->fs->put($path, $script); | ||
} | ||
} |