diff --git a/bridge/symfony-console/src/Command/Run.php b/bridge/symfony-console/src/Command/Run.php index 586bb475..2464406b 100644 --- a/bridge/symfony-console/src/Command/Run.php +++ b/bridge/symfony-console/src/Command/Run.php @@ -9,6 +9,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Testo\Output\Html\HtmlPlugin; use Testo\Output\Json\JsonPlugin; use Testo\Output\Teamcity\TeamcityPlugin; use Testo\Output\Terminal\TerminalPlugin; @@ -154,9 +155,10 @@ public function configure(): void $this->addOption( 'log-html', null, - InputOption::VALUE_REQUIRED, + InputOption::VALUE_OPTIONAL, 'Write a self-contained HTML report. A path ending in ".html" produces that single file; ' . 'anything else is a directory to fill with index.html and its assets. ' + . 'Without a value the report goes to ' . HtmlPlugin::DEFAULT_PATH . '. ' . 'The report opens over file:// with no server.', ); // $this->addOption( @@ -219,4 +221,21 @@ public function __invoke( ? Command::SUCCESS : Command::FAILURE; } + + /** + * Exists because the option definition cannot express "a bare `--log-html` falls back to the default + * path": a VALUE_OPTIONAL option reports null both when absent and when passed without a value, and a + * declared default would fire for the absent case too — so the two nulls are told apart against the + * raw parameters instead. Lives in `initialize()` and not in `__invoke()` with the other flags + * because {@see Base::execute()} snapshots the options for config hydration between the two — any + * later, and the resolved path would never reach the reporter. + */ + #[\Override] + protected function initialize(InputInterface $input, OutputInterface $output): void + { + parent::initialize($input, $output); + + $input->getOption('log-html') === null && $input->hasParameterOption('--log-html', true) + and $input->setOption('log-html', HtmlPlugin::DEFAULT_PATH); + } } diff --git a/bridge/symfony-console/tests/Acceptance/RunCommandTest.php b/bridge/symfony-console/tests/Acceptance/RunCommandTest.php new file mode 100644 index 00000000..1f6ca08b --- /dev/null +++ b/bridge/symfony-console/tests/Acceptance/RunCommandTest.php @@ -0,0 +1,161 @@ +run(['--log-html' => null]); + + Assert::same( + $tester->getStatusCode(), + Command::SUCCESS, + 'a valueless --log-html must be accepted, not rejected by the parser; output: ' . $tester->getDisplay(), + ); + Assert::same( + $tester->getInput()->getOption('log-html'), + HtmlPlugin::DEFAULT_PATH, + 'a bare --log-html must fall back to the default report location', + ); + Assert::true( + \is_file($this->sandbox->path('runtime/report/index.html')), + 'a bare --log-html must write the report to runtime/report/index.html; output: ' . $tester->getDisplay(), + ); + } + + public function logHtmlWithAnExplicitPathWritesThatSingleFile(): void + { + $tester = $this->run(['--log-html' => 'build/report.html']); + + Assert::same( + $tester->getStatusCode(), + Command::SUCCESS, + 'the stub suite must pass; output: ' . $tester->getDisplay(), + ); + Assert::true( + \is_file($this->sandbox->path('build/report.html')), + 'an explicit .html path must produce that single file, untouched by the bare-flag fallback; output: ' + . $tester->getDisplay(), + ); + Assert::false( + \is_dir($this->sandbox->path('runtime/report')), + 'the default location must stay untouched when a path is given', + ); + } + + public function withoutTheFlagNothingIsResolvedAndNoReportIsWritten(): void + { + $tester = $this->run(); + + Assert::same( + $tester->getStatusCode(), + Command::SUCCESS, + 'the stub suite must pass; output: ' . $tester->getDisplay(), + ); + Assert::null( + $tester->getInput()->getOption('log-html'), + 'an absent flag must stay absent — the fallback belongs to the bare flag only', + ); + Assert::false( + \is_dir($this->sandbox->path('runtime')), + 'a run without the flag must leave no report behind', + ); + } + + #[BeforeTest] + public function setUp(): void + { + $this->sandbox = Sandbox::create(); + $this->sandbox->writeFile('testo.php', self::configPointingAtTheStubSuite()); + } + + #[AfterTest] + public function tearDown(): void + { + $this->sandbox->destroy(); + } + + /** + * The smallest config a nested run needs: no sources, one suite pointing at this module's stub case, + * and a fresh inert reporter in place of the process-wide default (see the class docblock). + * The stub directory is embedded as an absolute path — the sandbox CWD is elsewhere. + */ + private static function configPointingAtTheStubSuite(): string + { + $stub = \var_export(\dirname(__DIR__) . '/Stub/Run', true); + + return <<with(HtmlPlugin::inert()), + ); + PHP; + } + + /** + * Run the `run` command against the sandbox config in non-interactive mode. + * + * Pass CLI flags as the standard CommandTester input array; a bare flag is a key with a null value, + * e.g. `['--log-html' => null]`. + * + * @param array $input + */ + private function run(array $input = []): CommandTester + { + $tester = new CommandTester(new Run()); + $tester->execute( + $input, + ['interactive' => false, 'capture_stderr_separately' => false], + ); + + return $tester; + } +} diff --git a/bridge/symfony-console/tests/Stub/Run/PassingCase.php b/bridge/symfony-console/tests/Stub/Run/PassingCase.php new file mode 100644 index 00000000..afafb38b --- /dev/null +++ b/bridge/symfony-console/tests/Stub/Run/PassingCase.php @@ -0,0 +1,21 @@ +` and - * `--log-report=` flags have something to activate without any change to `testo.php`. An instance + * `--log-report=` flags have something to activate without any change to `testo.php`; a bare + * `--log-html` activates it at {@see self::DEFAULT_PATH}. An instance * configured in code owns its slots and ignores the flags; only an instance with no destination of its * own reads them, which is how the inert default gets activated. Every destination a run collects — from * configured plugins and from flags — feeds a single {@see HtmlReportSink}: the document is built once diff --git a/skills/testo-run-tests/SKILL.md b/skills/testo-run-tests/SKILL.md index ab25570f..e7509e9c 100644 --- a/skills/testo-run-tests/SKILL.md +++ b/skills/testo-run-tests/SKILL.md @@ -103,7 +103,8 @@ over-narrow filter — widen it (a typo in `--filter`, a `--path` that matches n - Coverage (`--coverage`, `--coverage-clover=`, `--coverage-level=`, …) — see the `testo-coverage` skill. -- `--log-junit=`, `--log-html=` (a `.html` path → single file, anything else → directory), +- `--log-junit=`, `--log-html=` (a `.html` path → single file, anything else → directory; + bare `--log-html` → `runtime/report`), `--log-report=` (the full run as a versioned JSON document — the data behind the HTML), `--teamcity` — reports for CI and IDEs, not for agent parsing. - `--config=path/to/testo.php` when the config is not at the project root.