[results-dir] * * The conformance CLI writes one `checks.json` per scenario into * `[results-dir]` (default `results`), relative to this file. Every check with * a SUCCESS or FAILURE status counts towards the score; the badge message is * "/ (%)" and is written to * `-conformance-.json`, so each revision gets its own * badge. The badge is labelled "checks" only: the README renders the badges in * a table whose row and column headers already name the revision and suite. */ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\SingleCommandApplication; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Finder\Finder; require_once dirname(__DIR__, 2).'/vendor/autoload.php'; (new SingleCommandApplication()) ->setName('conformance-score') ->setDescription('Generates a shields.io endpoint badge from the conformance results') ->addArgument('suite', InputArgument::REQUIRED, 'Which conformance suite was run: "server" or "client"') ->addArgument('spec-version', InputArgument::REQUIRED, 'The spec revision that was run, e.g. "2025-11-25"') ->addArgument('results-dir', InputArgument::OPTIONAL, 'Directory the conformance CLI wrote results into, relative to this file', 'results') ->setCode(static function (InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $suite = $input->getArgument('suite'); $specVersion = $input->getArgument('spec-version'); if (!in_array($suite, ['server', 'client'], true)) { $io->error(sprintf('Suite must be "server" or "client", got "%s".', $suite)); return Command::INVALID; } $resultsDir = __DIR__.'/'.$input->getArgument('results-dir'); if (!is_dir($resultsDir)) { $io->error(sprintf('Results directory "%s" does not exist; run the conformance suite with `--output-dir` first.', $resultsDir)); return Command::FAILURE; } $total = 0; $passed = 0; $failures = []; foreach (Finder::create()->files()->name('checks.json')->in($resultsDir) as $file) { $checks = json_decode($file->getContents(), true); if (!is_array($checks)) { $io->warning(sprintf('Skipping unreadable result file "%s".', $file->getRelativePathname())); continue; } foreach ($checks as $check) { switch ($check['status'] ?? null) { case 'FAILURE': $failures[] = $file->getRelativePath(); break; case 'SUCCESS': ++$passed; break; default: continue 2; } ++$total; } } $pct = $total > 0 ? (int) round($passed / $total * 100) : 0; $badge = [ 'schemaVersion' => 1, 'label' => 'checks', 'message' => $total > 0 ? sprintf('%d/%d (%d%%)', $passed, $total, $pct) : 'no data', 'color' => match (true) { 0 === $total => 'lightgrey', $pct >= 95 => 'brightgreen', $pct >= 80 => 'green', $pct >= 60 => 'yellow', default => 'orange', }, ]; $outputFile = __DIR__.'/'.$suite.'-conformance-'.$specVersion.'.json'; if (false === file_put_contents($outputFile, json_encode($badge, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)."\n")) { $io->error(sprintf('Could not write badge file "%s".', $outputFile)); return Command::FAILURE; } if ($failures && $io->isVerbose()) { $io->section('Failing scenarios'); $io->listing($failures); } $io->success(sprintf('%s %s: %s', $suite, $specVersion, $badge['message'])); return Command::SUCCESS; }) ->run();