Skip to content

Commit 04c8d49

Browse files
chr-hertelclaude
andcommitted
Convert conformance score script to a Symfony Console command
Uses SingleCommandApplication for argument parsing, --help, and proper exit codes. symfony/console is a dev dependency and the conformance CI jobs run a full composer install, so it is available where the script runs. `--verbose` now also lists the failing scenarios. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ad5cb80 commit 04c8d49

1 file changed

Lines changed: 83 additions & 55 deletions

File tree

tests/Conformance/bin/score.php

Lines changed: 83 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -20,67 +20,95 @@
2020
* status. The resulting badge message is "<passed>/<total> (<pct>%)".
2121
*/
2222

23-
if (4 !== $argc) {
24-
fwrite(\STDERR, "Usage: php score.php <results-dir> <label> <output-file>\n");
25-
exit(1);
26-
}
23+
use Symfony\Component\Console\Command\Command;
24+
use Symfony\Component\Console\Input\InputArgument;
25+
use Symfony\Component\Console\Input\InputInterface;
26+
use Symfony\Component\Console\Output\OutputInterface;
27+
use Symfony\Component\Console\SingleCommandApplication;
28+
use Symfony\Component\Console\Style\SymfonyStyle;
29+
30+
require_once dirname(__DIR__, 3).'/vendor/autoload.php';
31+
32+
(new SingleCommandApplication())
33+
->setName('conformance-score')
34+
->setDescription('Generates a shields.io endpoint badge from a conformance results directory')
35+
->addArgument('results-dir', InputArgument::REQUIRED, 'Directory produced by `conformance --output-dir`')
36+
->addArgument('label', InputArgument::REQUIRED, 'Badge label, e.g. "server conformance"')
37+
->addArgument('output-file', InputArgument::REQUIRED, 'Path the badge JSON is written to')
38+
->setCode(static function (InputInterface $input, OutputInterface $output): int {
39+
$io = new SymfonyStyle($input, $output);
40+
41+
$resultsDir = $input->getArgument('results-dir');
42+
$label = $input->getArgument('label');
43+
$outputFile = $input->getArgument('output-file');
44+
45+
if (!is_dir($resultsDir)) {
46+
$io->error(sprintf('Results directory "%s" does not exist.', $resultsDir));
47+
48+
return Command::FAILURE;
49+
}
50+
51+
$checkFiles = new RegexIterator(
52+
new RecursiveIteratorIterator(
53+
new RecursiveDirectoryIterator($resultsDir, FilesystemIterator::SKIP_DOTS),
54+
),
55+
'/checks\.json$/',
56+
);
57+
58+
$total = 0;
59+
$passed = 0;
60+
$failures = [];
61+
62+
foreach ($checkFiles as $file) {
63+
$checks = json_decode((string) file_get_contents($file), true);
2764

28-
[, $resultsDir, $label, $outputFile] = $argv;
65+
if (!is_array($checks)) {
66+
$io->warning(sprintf('Skipping unreadable result file "%s".', $file));
2967

30-
if (!is_dir($resultsDir)) {
31-
fwrite(\STDERR, sprintf("Results directory \"%s\" does not exist.\n", $resultsDir));
32-
exit(1);
33-
}
68+
continue;
69+
}
3470

35-
$checkFiles = new RegexIterator(
36-
new RecursiveIteratorIterator(
37-
new RecursiveDirectoryIterator($resultsDir, FilesystemIterator::SKIP_DOTS),
38-
),
39-
'/checks\.json$/',
40-
);
71+
++$total;
4172

42-
$total = 0;
43-
$passed = 0;
73+
foreach ($checks as $check) {
74+
if ('FAILURE' === ($check['status'] ?? null)) {
75+
$failures[] = basename($file->getPath());
4476

45-
foreach ($checkFiles as $file) {
46-
$checks = json_decode((string) file_get_contents($file), true);
77+
continue 2;
78+
}
79+
}
4780

48-
if (!is_array($checks)) {
49-
fwrite(\STDERR, sprintf("Skipping unreadable result file \"%s\".\n", $file));
50-
continue;
51-
}
81+
++$passed;
82+
}
5283

53-
++$total;
84+
$pct = $total > 0 ? (int) round($passed / $total * 100) : 0;
85+
86+
$badge = [
87+
'schemaVersion' => 1,
88+
'label' => $label,
89+
'message' => $total > 0 ? sprintf('%d/%d (%d%%)', $passed, $total, $pct) : 'no data',
90+
'color' => match (true) {
91+
0 === $total => 'lightgrey',
92+
$pct >= 95 => 'brightgreen',
93+
$pct >= 80 => 'green',
94+
$pct >= 60 => 'yellow',
95+
default => 'orange',
96+
},
97+
];
98+
99+
if (false === file_put_contents($outputFile, json_encode($badge, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)."\n")) {
100+
$io->error(sprintf('Could not write badge file "%s".', $outputFile));
101+
102+
return Command::FAILURE;
103+
}
54104

55-
foreach ($checks as $check) {
56-
if ('FAILURE' === ($check['status'] ?? null)) {
57-
continue 2;
105+
if ($failures && $io->isVerbose()) {
106+
$io->section('Failing scenarios');
107+
$io->listing($failures);
58108
}
59-
}
60-
61-
++$passed;
62-
}
63-
64-
$pct = $total > 0 ? (int) round($passed / $total * 100) : 0;
65-
66-
$color = match (true) {
67-
0 === $total => 'lightgrey',
68-
$pct >= 95 => 'brightgreen',
69-
$pct >= 80 => 'green',
70-
$pct >= 60 => 'yellow',
71-
default => 'orange',
72-
};
73-
74-
$badge = [
75-
'schemaVersion' => 1,
76-
'label' => $label,
77-
'message' => $total > 0 ? sprintf('%d/%d (%d%%)', $passed, $total, $pct) : 'no data',
78-
'color' => $color,
79-
];
80-
81-
if (false === file_put_contents($outputFile, json_encode($badge, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)."\n")) {
82-
fwrite(\STDERR, sprintf("Could not write badge file \"%s\".\n", $outputFile));
83-
exit(1);
84-
}
85-
86-
printf("%s: %s\n", $label, $badge['message']);
109+
110+
$io->success(sprintf('%s: %s', $label, $badge['message']));
111+
112+
return Command::SUCCESS;
113+
})
114+
->run();

0 commit comments

Comments
 (0)