failingTests(); $failingTest = $tests->shift(); return $this->runBisect($failingTest, $tests); } protected function runBisect(string $failingTest, Collection $tests): int { [$first, $second] = $tests->split(2); $firstResult = $this->runTests($failingTest, $first, 'First Half Tests'); $secondResult = $this->runTests($failingTest, $second, 'Second Half Tests'); $this->table(['First', 'Second'], [ [$firstResult ? '✅' : '❌', $secondResult ? '✅' : '❌'], ]); if ($firstResult && $secondResult) { $this->error('Both halves passed, try again'); return Command::FAILURE; } elseif (!$firstResult && !$secondResult) { $this->error('Both halves failed, try again'); return Command::FAILURE; } if ($this->checkTests($firstResult, $first) || $this->checkTests($secondResult, $second)) { return Command::SUCCESS; } $this->info('Running new bisect'); $this->table(['First', 'Second'], [ [$first->count(), $second->count()], ]); if (!$firstResult) { return $this->runBisect($failingTest, $first); } else { return $this->runBisect($failingTest, $second); } } protected function runTests(string $failingTest, Collection $tests, string $title): bool { $toRun = (clone $tests)->push($failingTest); $this->table([$title], $toRun->map(fn ($test) => [$test])->toArray()); $process = new Process([ $this->phpUnitPath(), '--random-order-seed', $this->seed(), '--stop-on-failure', '--filter', $toRun->map(fn ($test) => addslashes($test))->implode('|'), ], timeout: 300); $process->run(); foreach ($process as $type => $data) { echo $data; } $process->wait(); return $process->getExitCode() === self::SUCCESS; } /** * @param Collection $tests */ protected function checkTests(bool $result, Collection $tests): bool { if ($result || $tests->count() !== 1) { return false; } $this->info('Found the Troublesome Test'); $this->info($tests->first()); if ($classPath = $this->classPath($tests->first())) { $this->info($classPath); } return true; } protected function classPath(string $test): ?string { if (!class_exists($test)) { return null; } return (new ReflectionClass($test))->getFileName(); } protected function failingTests(): Collection { $process = new Process([ $this->phpUnitPath(), '--random-order-seed', $this->seed(), '--stop-on-failure', '--debug', ], timeout: 300); $process->start(); $lines = []; foreach ($process as $type => $data) { $lines[] = $data; echo $data; } $process->wait(); if ($process->getExitCode() === self::SUCCESS) { $this->error('All tests passed, try again'); exit(Command::FAILURE); } return $this->parseFailingTestClasses($lines); } /** * @param array $lines * @return Collection */ protected function parseFailingTestClasses(array $lines): Collection { return collect($lines) ->filter(fn ($line) => Str::startsWith($line, 'Test \'')) ->filter(fn ($line) => !Str::endsWith($line, 'started')) ->map(fn (string $test) => Str::of($test) ->replace('Test \'', '') ->replace('\' ended', '') ->trim() ->explode('::') ->first()) ->unique(); } protected function seed(): string { if (isset($this->seed)) { return $this->seed; } $seed = $this->argument('seed'); if (! is_string($seed)) { $this->error('Seed must be a string'); $this->exit(Command::FAILURE); } return $this->seed = $seed; } protected function phpUnitPath(): string { return __DIR__ . '/../../../vendor/bin/phpunit'; } }