CommandSchedulingTest.php 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. <?php
  2. namespace Illuminate\Tests\Integration\Console;
  3. use Illuminate\Console\Scheduling\Schedule;
  4. use Illuminate\Filesystem\Filesystem;
  5. use Illuminate\Support\Str;
  6. use Orchestra\Testbench\TestCase;
  7. class CommandSchedulingTest extends TestCase
  8. {
  9. /**
  10. * Each run of this test is assigned a random ID to ensure that separate runs
  11. * do not interfere with each other.
  12. *
  13. * @var string
  14. */
  15. protected $id;
  16. /**
  17. * The path to the file that execution logs will be written to.
  18. *
  19. * @var string
  20. */
  21. protected $logfile;
  22. /**
  23. * Just in case Testbench starts to ship an `artisan` script, we'll check and save a backup.
  24. *
  25. * @var string|null
  26. */
  27. protected $originalArtisan;
  28. /**
  29. * The Filesystem instance for writing stubs and logs.
  30. *
  31. * @var \Illuminate\Filesystem\Filesystem
  32. */
  33. protected $fs;
  34. protected function setUp(): void
  35. {
  36. parent::setUp();
  37. $this->fs = new Filesystem;
  38. $this->id = Str::random();
  39. $this->logfile = storage_path("logs/command_scheduling_test_{$this->id}.log");
  40. $this->writeArtisanScript();
  41. }
  42. protected function tearDown(): void
  43. {
  44. $this->fs->delete($this->logfile);
  45. $this->fs->delete(base_path('artisan'));
  46. if (! is_null($this->originalArtisan)) {
  47. $this->fs->put(base_path('artisan'), $this->originalArtisan);
  48. }
  49. parent::tearDown();
  50. }
  51. /**
  52. * @dataProvider executionProvider
  53. */
  54. public function testExecutionOrder($background)
  55. {
  56. $event = $this->app->make(Schedule::class)
  57. ->command("test:{$this->id}")
  58. ->onOneServer()
  59. ->after(function () {
  60. $this->fs->append($this->logfile, "after\n");
  61. })
  62. ->before(function () {
  63. $this->fs->append($this->logfile, "before\n");
  64. });
  65. if ($background) {
  66. $event->runInBackground();
  67. }
  68. // We'll trigger the scheduler three times to simulate multiple servers
  69. $this->artisan('schedule:run');
  70. $this->artisan('schedule:run');
  71. $this->artisan('schedule:run');
  72. if ($background) {
  73. // Since our command is running in a separate process, we need to wait
  74. // until it has finished executing before running our assertions.
  75. $this->waitForLogMessages('before', 'handled', 'after');
  76. }
  77. $this->assertLogged('before', 'handled', 'after');
  78. }
  79. public function executionProvider()
  80. {
  81. return [
  82. 'Foreground' => [false],
  83. 'Background' => [true],
  84. ];
  85. }
  86. protected function waitForLogMessages(...$messages)
  87. {
  88. $tries = 0;
  89. $sleep = 100000; // 100K microseconds = 0.1 second
  90. $limit = 50; // 0.1s * 50 = 5 second wait limit
  91. do {
  92. $log = $this->fs->get($this->logfile);
  93. if (Str::containsAll($log, $messages)) {
  94. return;
  95. }
  96. $tries++;
  97. usleep($sleep);
  98. } while ($tries < $limit);
  99. }
  100. protected function assertLogged(...$messages)
  101. {
  102. $log = trim($this->fs->get($this->logfile));
  103. $this->assertEquals(implode("\n", $messages), $log);
  104. }
  105. protected function writeArtisanScript()
  106. {
  107. $path = base_path('artisan');
  108. // Save existing artisan script if there is one
  109. if ($this->fs->exists($path)) {
  110. $this->originalArtisan = $this->fs->get($path);
  111. }
  112. $thisFile = __FILE__;
  113. $logfile = var_export($this->logfile, true);
  114. $script = <<<PHP
  115. #!/usr/bin/env php
  116. <?php
  117. // This is a custom artisan script made specifically for:
  118. //
  119. // {$thisFile}
  120. //
  121. // It should be automatically cleaned up when the tests have finished executing.
  122. // If you are seeing this file, an unexpected error must have occurred. Please
  123. // manually remove it.
  124. define('LARAVEL_START', microtime(true));
  125. require __DIR__.'/../../../autoload.php';
  126. \$app = require_once __DIR__.'/bootstrap/app.php';
  127. \$kernel = \$app->make(Illuminate\Contracts\Console\Kernel::class);
  128. // Here is our custom command for the test
  129. class CommandSchedulingTestCommand_{$this->id} extends Illuminate\Console\Command
  130. {
  131. protected \$signature = 'test:{$this->id}';
  132. public function handle()
  133. {
  134. \$logfile = {$logfile};
  135. (new Illuminate\Filesystem\Filesystem)->append(\$logfile, "handled\\n");
  136. }
  137. }
  138. // Register command with Kernel
  139. Illuminate\Console\Application::starting(function (\$artisan) {
  140. \$artisan->add(new CommandSchedulingTestCommand_{$this->id});
  141. });
  142. // Add command to scheduler so that the after() callback is trigger in our spawned process
  143. Illuminate\Foundation\Application::getInstance()
  144. ->booted(function (\$app) {
  145. \$app->resolving(Illuminate\Console\Scheduling\Schedule::class, function(\$schedule) {
  146. \$fs = new Illuminate\Filesystem\Filesystem;
  147. \$schedule->command("test:{$this->id}")
  148. ->after(function() use (\$fs) {
  149. \$logfile = {$logfile};
  150. \$fs->append(\$logfile, "after\\n");
  151. })
  152. ->before(function() use (\$fs) {
  153. \$logfile = {$logfile};
  154. \$fs->append(\$logfile, "before\\n");
  155. });
  156. });
  157. });
  158. \$status = \$kernel->handle(
  159. \$input = new Symfony\Component\Console\Input\ArgvInput,
  160. new Symfony\Component\Console\Output\ConsoleOutput
  161. );
  162. \$kernel->terminate(\$input, \$status);
  163. exit(\$status);
  164. PHP;
  165. $this->fs->put($path, $script);
  166. }
  167. }