*/ function crapJson(string $arguments): array { Artisan::call("crap {$arguments} --json"); return json_decode(Artisan::output(), true); } function crapIgnoreFile(array $entries): string { File::ensureDirectoryExists(base_path(CRAP_SCRATCH)); File::put(base_path(CRAP_SCRATCH.'/ignore.json'), json_encode($entries)); return CRAP_SCRATCH.'/ignore.json'; } afterEach(function () { File::deleteDirectory(base_path(CRAP_SCRATCH)); }); it('passes when every method is below the threshold', function () { $path = crapFixture('Simple', <<<'PHP' artisan("crap --path={$path} --no-coverage") ->expectsOutputToContain('none above complexity 10') ->assertExitCode(CommandAlias::SUCCESS); }); it('fails and names the method when it exceeds the threshold', function () { $path = crapFixture('Branchy', <<<'PHP' artisan("crap --path={$path} --no-coverage --threshold=2") ->expectsOutputToContain('Fixture\Branchy::tangle') ->assertExitCode(CommandAlias::FAILURE); }); it('counts one for the method plus one per decision point', function () { $path = crapFixture('Counted', <<<'PHP' 0 && $flag) { return 'a'; } foreach ([1, 2] as $i) { $n += $i; } return match ($n) { 1 => 'x', 2 => 'y', default => 'z' }; } } PHP); // 1 base + if + && + foreach + 3 match arms = 7 $json = crapJson("--path={$path} --no-coverage --threshold=0"); expect($json['over_threshold'])->toHaveCount(1) ->and($json['over_threshold'][0]['complexity'])->toBe(7) ->and($json['over_threshold'][0]['method'])->toBe('Fixture\Counted::branches'); }); it('counts closures nested in a method toward that method', function () { $path = crapFixture('Nested', <<<'PHP' 0 ? 'pos' : 'neg'; }, $items); } } PHP); $json = crapJson("--path={$path} --no-coverage --threshold=0"); expect($json['over_threshold'])->toHaveCount(1) ->and($json['over_threshold'][0]['complexity'])->toBe(2); }); it('measures methods on enums, which the upstream visitor cannot', function () { $path = crapFixture('Kind', <<<'PHP' 'first', self::B => 'second', }; } } PHP); $json = crapJson("--path={$path} --no-coverage --threshold=0"); expect(array_column($json['over_threshold'], 'method'))->toContain('Fixture\Kind::label'); }); it('exempts methods listed in the ignore file and reports the reason', function () { $path = crapFixture('Ignored', <<<'PHP' 'deliberate']); $json = crapJson("--path={$path} --no-coverage --threshold=2 --ignore={$ignore}"); expect($json['over_threshold'])->toBeEmpty() ->and($json['verdict'])->toBe('pass') ->and($json['exempt'][0]['reason'])->toBe('deliberate'); }); it('flags an exemption that is no longer needed', function () { $path = crapFixture('Plain', <<<'PHP' 'stale entry']); $json = crapJson("--path={$path} --no-coverage --ignore={$ignore}"); expect(array_column($json['stale_exemptions'], 'method'))->toBe(['Fixture\Plain::simple']); }); it('refuses to guess when the coverage report is missing', function () { $path = crapFixture('Any', 'artisan("crap --path={$path} --report=build/does-not-exist.xml") ->expectsOutputToContain('--no-coverage') ->assertExitCode(CommandAlias::INVALID); }); it('joins crap and coverage from the report onto the matching method', function () { $path = crapFixture('Reported', <<<'PHP' Fixture\Reported tangle 12.5 2 50 XML); $json = crapJson("--path={$path} --threshold=1 --report={$report}"); // Whole floats come back as ints: json_encode writes 50.0 as 50. expect($json['over_threshold'][0]['crap'])->toBe(12.5) ->and($json['over_threshold'][0]['coverage'])->toEqual(50) ->and($json['coverage']['source'])->toBe($report); }); it('reports the scope it did and did not look at', function () { $path = crapFixture('Scoped', 'toBe('php') ->and($json['excluded'])->toContain('resources/js/ (no complexity pipeline)') ->and($json['mode'])->toBe('project'); });