From 4cbc266f87c8598bed1273db27b195a446f92ea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 06:32:21 -0300 Subject: [PATCH 01/35] [tests] Structure agent-driven PHPUnit JSON results --- CHANGELOG.md | 1 + README.md | 6 + composer.json | 2 +- docs/commands/tests.rst | 13 +- docs/running/specialized-commands.rst | 4 + src/Console/Command/TestsCommand.php | 125 +++++++++++++--- tests/Console/Command/TestsCommandTest.php | 138 ++++++++++++++++++ .../Console/Logger/OutputFormatLoggerTest.php | 53 +++++++ 8 files changed, 321 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 567468e7d0..f85680cb78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Keep `tests` JSON output parseable while surfacing structured PHPUnit result data for agent-driven runs, including a raw-output fallback when PHPUnit cannot emit machine-readable JSON (#248) - Register ``ergebnis/phpunit-agent-reporter`` in the packaged ``phpunit.xml`` so AI agents receive compact PHPUnit JSON summaries without changing consumer overrides manually (#327) - Let `wiki`, `docs`, `tests`, `metrics`, and `reports` skip gracefully for guide-only repositories while keeping wiki/report workflows and published preview links aligned with the artifacts that were actually generated (#325) diff --git a/README.md b/README.md index 1be24515c5..ce68568182 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,12 @@ automatically when the runtime is detected as agent-driven. For prints raw values so release workflows can keep capturing semantic versions and piping rendered release notes directly into GitHub releases. +`--pretty-json` intentionally remains valid JSON. DevTools does not inject ANSI +escape sequences into that mode today because preserving a parseable payload +takes precedence over terminal-only color. Where orchestrated tools can expose +structured subprocess results safely, DevTools prefers adding stable fields to +the JSON context rather than coloring otherwise strict JSON output. + Progress output is disabled by default on the commands that support transient rendering, and `--progress` re-enables it for human-readable terminal runs. When `--json` or `--pretty-json` is active on commands that orchestrate other diff --git a/composer.json b/composer.json index cecfff73f3..345b18fb7d 100644 --- a/composer.json +++ b/composer.json @@ -44,8 +44,8 @@ "container-interop/service-provider": "^0.4.1", "dg/bypass-finals": "^1.9", "ergebnis/agent-detector": "^1.1", - "ergebnis/phpunit-agent-reporter": "^0.3", "ergebnis/composer-normalize": "^2.51", + "ergebnis/phpunit-agent-reporter": "^0.3", "ergebnis/rector-rules": "^1.18", "fakerphp/faker": "^1.24", "fast-forward/phpdoc-bootstrap-template": "^2.0", diff --git a/docs/commands/tests.rst b/docs/commands/tests.rst index 6a25cf21e1..adf591cfee 100644 --- a/docs/commands/tests.rst +++ b/docs/commands/tests.rst @@ -71,7 +71,8 @@ Options ``--pretty-json`` Emit the same structured payload with indentation for terminal inspection. This also suppresses PHPUnit progress output automatically so the JSON - payload is not polluted by transient progress rendering. + payload is not polluted by transient progress rendering. The output remains + valid JSON and intentionally does not include ANSI color escapes. Examples -------- @@ -160,5 +161,15 @@ Behavior - progress output is disabled by default. - ``--json`` and ``--pretty-json`` keep progress output disabled so the structured payload stays clean, even when ``--progress`` is provided. +- in agent-driven runs outside the Composer test suite, the command also + switches to the same structured capture mode automatically so PHPUnit output + is buffered instead of interleaving with top-level DevTools log payloads. +- when structured capture is active and PHPUnit emits agent-reporter JSON, the + command exposes a ``phpunit`` object in the log context with ``tool``, + ``label``, ``exit_code``, and the reported ``result`` / ``summary`` / + ``details`` fields. +- when structured capture is active but PHPUnit does not emit parseable JSON, + the command preserves the raw subprocess text under + ``context.phpunit.raw_output`` instead of dropping it. - The command fails if minimum coverage is not met (when ``--min-coverage`` is set). - The packaged configuration registers the DevTools PHPUnit extension. diff --git a/docs/running/specialized-commands.rst b/docs/running/specialized-commands.rst index fc4d83b277..0e683807a4 100644 --- a/docs/running/specialized-commands.rst +++ b/docs/running/specialized-commands.rst @@ -63,6 +63,10 @@ Important details: - ``--progress`` re-enables PHPUnit progress output in text mode; - ``--json`` and ``--pretty-json`` still suppress PHPUnit progress output automatically; +- ``--pretty-json`` stays valid JSON and does not add ANSI color escapes; +- in agent-driven runs, the command also captures PHPUnit output in structured + mode automatically and exposes a ``phpunit`` summary object when the bundled + agent reporter is active; - the packaged configuration registers the DevTools PHPUnit extension. ``dependencies`` diff --git a/src/Console/Command/TestsCommand.php b/src/Console/Command/TestsCommand.php index 782cf9c280..dda5089369 100644 --- a/src/Console/Command/TestsCommand.php +++ b/src/Console/Command/TestsCommand.php @@ -19,10 +19,12 @@ namespace FastForward\DevTools\Console\Command; +use JsonException; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Console\Input\HasCacheOption; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Composer\Json\ComposerJsonInterface; +use FastForward\DevTools\Environment\RuntimeEnvironmentInterface; use FastForward\DevTools\Filesystem\FilesystemInterface; use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\PhpUnit\Bootstrap\BootstrapShimGenerator; @@ -45,6 +47,8 @@ use Symfony\Component\Console\Output\OutputInterface; use function is_numeric; +use function Safe\json_decode; +use function Safe\preg_match; /** * Facilitates the execution of the PHPUnit testing framework. @@ -57,6 +61,8 @@ final class TestsCommand extends Command use HasJsonOption; use LogsCommandResults; + private const string PROCESS_LABEL = 'Running PHPUnit Tests'; + /** * @var string identifies the local configuration file for PHPUnit processes */ @@ -71,6 +77,7 @@ final class TestsCommand extends Command * @param ProcessBuilderInterface $processBuilder the builder used to assemble the PHPUnit process * @param ProcessQueueInterface $processQueue the queue used to execute PHPUnit * @param ProjectCapabilitiesResolverInterface $projectCapabilitiesResolver the project capability resolver + * @param RuntimeEnvironmentInterface $runtimeEnvironment the runtime environment capability resolver * @param LoggerInterface $logger the output-aware logger */ public function __construct( @@ -82,6 +89,7 @@ public function __construct( private readonly ProcessBuilderInterface $processBuilder, private readonly ProcessQueueInterface $processQueue, private readonly ProjectCapabilitiesResolverInterface $projectCapabilitiesResolver, + private readonly RuntimeEnvironmentInterface $runtimeEnvironment, private readonly LoggerInterface $logger, ) { parent::__construct(); @@ -160,7 +168,8 @@ protected function configure(): void */ protected function execute(InputInterface $input, OutputInterface $output): int { - $jsonOutput = $this->isJsonOutput($input); + $jsonOutput = $this->isJsonOutput($input) + || ($this->runtimeEnvironment->isAgentPresent() && ! $this->runtimeEnvironment->isComposerTestRun()); $processOutput = $jsonOutput ? new BufferedOutput() : $output; $cacheEnabled = $this->isCacheEnabled($input); @@ -236,25 +245,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int process: $processBuilder ->withArgument($input->getArgument('path')) ->build([DevToolsPathResolver::getPreferredToolBinaryPath('phpunit')]), - label: 'Running PHPUnit Tests', + label: self::PROCESS_LABEL, ); $result = $this->processQueue->run($processOutput); + $processResultContext = $this->resolveProcessResultContext($processOutput, $result, $jsonOutput); if (self::SUCCESS !== $result || null === $minimumCoverage || null === $coverageReportPath) { if (self::SUCCESS === $result) { - return $this->success( - 'PHPUnit tests completed successfully.', - $input, - [ - 'output' => $processOutput, - ], - ); + return $this->success('PHPUnit tests completed successfully.', $input, $processResultContext); } - return $this->failure('PHPUnit tests failed.', $input, [ - 'output' => $processOutput, - ]); + return $this->failure('PHPUnit tests failed.', $input, $processResultContext); } [$validationResult, $message, $coverageContext] = $this->validateMinimumCoverage( @@ -263,16 +265,101 @@ protected function execute(InputInterface $input, OutputInterface $output): int ); if (self::SUCCESS === $validationResult) { - return $this->success($message, $input, [ + return $this->success($message, $input, [...$processResultContext, ...$coverageContext]); + } + + return $this->failure($message, $input, [...$processResultContext, ...$coverageContext]); + } + + /** + * Builds structured context for the executed PHPUnit process. + * + * @param OutputInterface $processOutput the output sink used while the process ran + * @param int $exitCode the exit code returned by the process queue + * @param bool $structuredOutput whether the command captured subprocess output for structured logging + * + * @return array|OutputInterface> + */ + private function resolveProcessResultContext( + OutputInterface $processOutput, + int $exitCode, + bool $structuredOutput + ): array { + if (! $structuredOutput) { + return [ 'output' => $processOutput, - ...$coverageContext, - ]); + ]; + } + + $context = [ + 'phpunit' => [ + 'tool' => 'phpunit', + 'label' => self::PROCESS_LABEL, + 'exit_code' => $exitCode, + ], + ]; + + if (! $processOutput instanceof BufferedOutput) { + return $context; + } + + $rawOutput = trim($processOutput->fetch()); + + if ('' === $rawOutput) { + return $context; } - return $this->failure($message, $input, [ - 'output' => $processOutput, - ...$coverageContext, - ]); + [$decoded, $supplementalOutput] = $this->decodeStructuredProcessOutput($rawOutput); + + if (! \is_array($decoded)) { + $context['phpunit']['raw_output'] = $supplementalOutput; + + return $context; + } + + $context['phpunit'] = [...$context['phpunit'], ...$decoded]; + + if (null !== $supplementalOutput) { + $context['phpunit']['raw_output'] = $supplementalOutput; + } + + return $context; + } + + /** + * Attempts to decode structured PHPUnit output while preserving any + * non-JSON prelude that was emitted before the final reporter payload. + * + * @param string $rawOutput the captured subprocess output + * + * @return array{array|null, string|null} decoded payload and preserved supplemental output + */ + private function decodeStructuredProcessOutput(string $rawOutput): array + { + try { + $decoded = json_decode($rawOutput, true); + + return [\is_array($decoded) ? $decoded : null, null]; + } catch (JsonException) { + } + + if (1 !== preg_match('/^(?P.*?)(?P\{\s*"result".*)$/s', $rawOutput, $matches)) { + return [null, $rawOutput]; + } + + try { + $decoded = json_decode($matches['payload'], true); + } catch (JsonException) { + return [null, $rawOutput]; + } + + if (! \is_array($decoded)) { + return [null, $rawOutput]; + } + + $prefix = trim($matches['prefix']); + + return [$decoded, '' === $prefix ? null : $prefix]; } /** diff --git a/tests/Console/Command/TestsCommandTest.php b/tests/Console/Command/TestsCommandTest.php index 9e163c6a62..db0e67e747 100644 --- a/tests/Console/Command/TestsCommandTest.php +++ b/tests/Console/Command/TestsCommandTest.php @@ -22,6 +22,7 @@ use FastForward\DevTools\Composer\Json\ComposerJsonInterface; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Console\Command\TestsCommand; +use FastForward\DevTools\Environment\RuntimeEnvironmentInterface; use FastForward\DevTools\Filesystem\FilesystemInterface; use FastForward\DevTools\PhpUnit\Bootstrap\BootstrapShimGenerator; use FastForward\DevTools\PhpUnit\Coverage\CoverageSummary; @@ -76,6 +77,8 @@ final class TestsCommandTest extends TestCase private ObjectProphecy $projectCapabilitiesResolver; + private ObjectProphecy $runtimeEnvironment; + private ObjectProphecy $logger; private ObjectProphecy $input; @@ -95,6 +98,7 @@ protected function setUp(): void $this->fileLocator = $this->prophesize(FileLocatorInterface::class); $this->processQueue = $this->prophesize(ProcessQueueInterface::class); $this->projectCapabilitiesResolver = $this->prophesize(ProjectCapabilitiesResolverInterface::class); + $this->runtimeEnvironment = $this->prophesize(RuntimeEnvironmentInterface::class); $this->logger = $this->prophesize(LoggerInterface::class); $this->input = $this->prophesize(InputInterface::class); $this->output = $this->prophesize(OutputInterface::class); @@ -108,6 +112,7 @@ protected function setUp(): void new ProcessBuilder(), $this->processQueue->reveal(), $this->projectCapabilitiesResolver->reveal(), + $this->runtimeEnvironment->reveal(), $this->logger->reveal(), ); @@ -124,6 +129,10 @@ protected function setUp(): void false, true, )); + $this->runtimeEnvironment->isAgentPresent() + ->willReturn(false); + $this->runtimeEnvironment->isComposerTestRun() + ->willReturn(true); $this->fileLocator->locate(TestsCommand::CONFIG)->willReturn(getcwd() . '/' . TestsCommand::CONFIG); $this->filesystem->getAbsolutePath('./vendor/autoload.php') ->willReturn(getcwd() . '/vendor/autoload.php'); @@ -251,6 +260,135 @@ public function executeWillDisablePhpUnitProgressWhenJsonIsRequested(): void self::assertSame(TestsCommand::SUCCESS, $this->invokeExecute()); } + /** + * @return void + */ + #[Test] + public function executeWillCaptureStructuredPhpUnitSummaryWhenAgentEnvironmentIsDetected(): void + { + $this->runtimeEnvironment->isAgentPresent() + ->willReturn(true); + $this->runtimeEnvironment->isComposerTestRun() + ->willReturn(false); + + $this->processQueue->add( + Argument::that(static fn(Process $process): bool => str_contains( + $process->getCommandLine(), + '--no-progress', + ) && ! str_contains($process->getCommandLine(), '--colors=always')), + false, + false, + 'Running PHPUnit Tests' + )->shouldBeCalled(); + $this->processQueue->run(Argument::type(OutputInterface::class)) + ->will(static function (array $arguments): int { + $arguments[0]->write( + "{\n \"result\": \"success\",\n \"summary\": {\n \"assertions\": 5,\n \"failures\": 0,\n \"tests\": 2,\n \"warnings\": 0\n }\n}\n" + ); + + return TestsCommand::SUCCESS; + })->shouldBeCalled(); + $this->logger->info('Running PHPUnit tests...', Argument::that( + static fn(array $context): bool => $context['input'] instanceof InputInterface + ))->shouldBeCalled(); + $this->logger->log( + 'info', + 'PHPUnit tests completed successfully.', + Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface + && isset($context['phpunit']) + && 'phpunit' === $context['phpunit']['tool'] + && 'Running PHPUnit Tests' === $context['phpunit']['label'] + && TestsCommand::SUCCESS === $context['phpunit']['exit_code'] + && 'success' === $context['phpunit']['result'] + && 5 === $context['phpunit']['summary']['assertions'] + && ! isset($context['output'])), + )->shouldBeCalled(); + + self::assertSame(TestsCommand::SUCCESS, $this->invokeExecute()); + } + + /** + * @return void + */ + #[Test] + public function executeWillCaptureStructuredPhpUnitSummaryAfterCoveragePreludeWhenAgentEnvironmentIsDetected(): void + { + $this->runtimeEnvironment->isAgentPresent() + ->willReturn(true); + $this->runtimeEnvironment->isComposerTestRun() + ->willReturn(false); + + $this->processQueue->add( + Argument::type(Process::class), + false, + false, + 'Running PHPUnit Tests' + )->shouldBeCalled(); + $this->processQueue->run(Argument::type(OutputInterface::class)) + ->will(static function (array $arguments): int { + $arguments[0]->write( + "Generating code coverage report in PHP format ... done [00:00.002]\n\n{\n \"result\": \"success\",\n \"summary\": {\n \"assertions\": 5,\n \"failures\": 0,\n \"tests\": 2,\n \"warnings\": 0\n }\n}\n" + ); + + return TestsCommand::SUCCESS; + })->shouldBeCalled(); + $this->logger->info('Running PHPUnit tests...', Argument::that( + static fn(array $context): bool => $context['input'] instanceof InputInterface + ))->shouldBeCalled(); + $this->logger->log( + 'info', + 'PHPUnit tests completed successfully.', + Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface + && isset($context['phpunit']) + && 'success' === $context['phpunit']['result'] + && 5 === $context['phpunit']['summary']['assertions'] + && 'Generating code coverage report in PHP format ... done [00:00.002]' === $context['phpunit']['raw_output']), + )->shouldBeCalled(); + + self::assertSame(TestsCommand::SUCCESS, $this->invokeExecute()); + } + + /** + * @return void + */ + #[Test] + public function executeWillKeepRawPhpUnitOutputWhenStructuredSummaryCannotBeDecoded(): void + { + $this->input->getOption('json') + ->willReturn(true); + $this->input->getOption('pretty-json') + ->willReturn(false); + + $this->processQueue->add( + Argument::type(Process::class), + false, + false, + 'Running PHPUnit Tests' + )->shouldBeCalled(); + $this->logger->info('Running PHPUnit tests...', Argument::that( + static fn(array $context): bool => $context['input'] instanceof InputInterface + ))->shouldBeCalled(); + $this->processQueue->run(Argument::type(OutputInterface::class)) + ->will(static function (array $arguments): int { + $arguments[0]->write("PHPUnit 12.5.24 by Sebastian Bergmann.\n\nOK (2 tests, 5 assertions)\n"); + + return TestsCommand::SUCCESS; + })->shouldBeCalled(); + $this->logger->log( + 'info', + 'PHPUnit tests completed successfully.', + Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface + && isset($context['phpunit']) + && 'phpunit' === $context['phpunit']['tool'] + && 'Running PHPUnit Tests' === $context['phpunit']['label'] + && TestsCommand::SUCCESS === $context['phpunit']['exit_code'] + && "PHPUnit 12.5.24 by Sebastian Bergmann.\n\nOK (2 tests, 5 assertions)" === $context['phpunit']['raw_output'] + && ! isset($context['output'])), + )->shouldBeCalled(); + + self::assertSame(TestsCommand::SUCCESS, $this->invokeExecute()); + } + /** * @return void */ diff --git a/tests/Console/Logger/OutputFormatLoggerTest.php b/tests/Console/Logger/OutputFormatLoggerTest.php index a5bf3de286..d79dbd0035 100644 --- a/tests/Console/Logger/OutputFormatLoggerTest.php +++ b/tests/Console/Logger/OutputFormatLoggerTest.php @@ -41,6 +41,7 @@ use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; +use function Safe\json_decode; use function Safe\putenv; #[CoversClass(OutputFormatLogger::class)] @@ -185,6 +186,32 @@ public function logWillWriteStructuredJsonWhenJsonOutputIsRequested(): void ]); } + /** + * @return void + */ + #[Test] + public function logWillEmitParseableJsonWhenJsonOutputIsRequested(): void + { + $logger = new OutputFormatLogger( + new ArgvInput(['dev-tools', '--json']), + $this->output->reveal(), + $this->clock->reveal(), + new Detector(), + new CompositeContextProcessor([new CommandInputProcessor(), new CommandOutputProcessor()]), + $this->createGithubActionOutput(), + ); + + $this->output->writeln(Argument::that(static function (string $payload): bool { + $decoded = json_decode($payload, true, 512, \JSON_THROW_ON_ERROR); + + return 'info' === $decoded['level'] && 'Build {status}' === $decoded['message']; + }))->shouldBeCalledOnce(); + + $logger->info('Build {status}', [ + 'status' => 'ready', + ]); + } + /** * @return void */ @@ -212,6 +239,32 @@ public function logWillWritePrettyPrintedJsonWhenPrettyJsonOutputIsRequested(): ]); } + /** + * @return void + */ + #[Test] + public function logWillEmitParseableJsonWhenPrettyJsonOutputIsRequested(): void + { + $logger = new OutputFormatLogger( + new ArgvInput(['dev-tools', '--pretty-json']), + $this->output->reveal(), + $this->clock->reveal(), + new Detector(), + new CompositeContextProcessor([new CommandInputProcessor(), new CommandOutputProcessor()]), + $this->createGithubActionOutput(), + ); + + $this->output->writeln(Argument::that(static function (string $payload): bool { + $decoded = json_decode($payload, true, 512, \JSON_THROW_ON_ERROR); + + return 'info' === $decoded['level'] && 'Build {status}' === $decoded['message']; + }))->shouldBeCalledOnce(); + + $logger->info('Build {status}', [ + 'status' => 'ready', + ]); + } + /** * @return void */ From 198dfbefc99abc0537044fc3df9fc586d3150e0f Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 09:33:39 +0000 Subject: [PATCH 02/35] Update wiki submodule pointer for PR #329 --- .github/wiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/wiki b/.github/wiki index 19c36c7374..f556757bbc 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit 19c36c73743fdc0720bad2c1a5dc9eca4a97bf88 +Subproject commit f556757bbc35e4ae7ca5144a717ae9bc4e551334 From 860f47d77a729429912d357f990557b3e08a4a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 15:49:29 -0300 Subject: [PATCH 03/35] [tests] Wrap structured PHPUnit output in command JSON --- CHANGELOG.md | 2 +- README.md | 15 +- docs/commands/tests.rst | 19 ++- docs/running/specialized-commands.rst | 6 +- src/Console/Command/TestsCommand.php | 139 +++++++++++++++---- tests/Console/Command/TestsCommandTest.php | 151 +++++++++++++++++---- 6 files changed, 264 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f85680cb78..8dfc98a235 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Keep `tests` JSON output parseable while surfacing structured PHPUnit result data for agent-driven runs, including a raw-output fallback when PHPUnit cannot emit machine-readable JSON (#248) +- Keep `tests` JSON output parseable by exposing the PHPUnit agent-reporter payload under `output` in structured runs, including a raw-output fallback when PHPUnit writes extra text before the final JSON (#248) - Register ``ergebnis/phpunit-agent-reporter`` in the packaged ``phpunit.xml`` so AI agents receive compact PHPUnit JSON summaries without changing consumer overrides manually (#327) - Let `wiki`, `docs`, `tests`, `metrics`, and `reports` skip gracefully for guide-only repositories while keeping wiki/report workflows and published preview links aligned with the artifacts that were actually generated (#325) diff --git a/README.md b/README.md index ce68568182..649b7af582 100644 --- a/README.md +++ b/README.md @@ -220,11 +220,16 @@ rendering, and `--progress` re-enables it for human-readable terminal runs. When `--json` or `--pretty-json` is active on commands that orchestrate other tools, DevTools keeps progress suppressed, forwards JSON flags where the underlying tool supports structured output, and otherwise falls back to -quieter subprocess modes so the captured payload stays machine-readable. In -GitHub Actions, queued subprocess output is grouped into collapsible sections, -and logged failures emit native workflow error annotations, including file and -line metadata when commands provide it. The packaged tests, reports, wiki, and -changelog workflows also append concise Markdown outcomes to +quieter subprocess modes so the captured payload stays machine-readable. The +`tests` command now captures the bundled PHPUnit agent-reporter payload in +structured runs and stores it in `context.output`, preserving `result`, +`summary`, optional `details`, and a `raw_output` fallback when coverage or +other PHPUnit text is emitted before the final reporter JSON. In GitHub +Actions, queued subprocess output is +grouped into collapsible sections, and logged failures emit native workflow +error annotations, including file and line metadata when commands provide it. +The packaged tests, reports, wiki, and changelog workflows also append concise +Markdown outcomes to `GITHUB_STEP_SUMMARY` so maintainers can scan versions, URLs, preview refs, verification status, and release results without expanding full logs. This repository also keeps a bounded retry workflow that reruns failed jobs once diff --git a/docs/commands/tests.rst b/docs/commands/tests.rst index adf591cfee..004bc71826 100644 --- a/docs/commands/tests.rst +++ b/docs/commands/tests.rst @@ -162,14 +162,19 @@ Behavior - ``--json`` and ``--pretty-json`` keep progress output disabled so the structured payload stays clean, even when ``--progress`` is provided. - in agent-driven runs outside the Composer test suite, the command also - switches to the same structured capture mode automatically so PHPUnit output - is buffered instead of interleaving with top-level DevTools log payloads. + switches to the same structured capture mode automatically. - when structured capture is active and PHPUnit emits agent-reporter JSON, the - command exposes a ``phpunit`` object in the log context with ``tool``, - ``label``, ``exit_code``, and the reported ``result`` / ``summary`` / - ``details`` fields. + command stores that payload inside ``output`` while keeping the standard + DevTools JSON envelope. ``--json`` and ``--pretty-json`` therefore expose + the same structured result, with formatting as the only difference. - when structured capture is active but PHPUnit does not emit parseable JSON, - the command preserves the raw subprocess text under - ``context.phpunit.raw_output`` instead of dropping it. + the command preserves the raw subprocess text inside ``output.raw_output`` + instead of dropping it. +- when coverage generation or other PHPUnit text appears before the final + reporter payload, the command preserves that prelude in + ``output.raw_output`` while keeping the main JSON result parseable. +- when ``--min-coverage`` is used in structured mode, the command appends a + ``coverage`` object under ``output`` and flips ``output.result`` to + ``failure`` if the threshold is not met. - The command fails if minimum coverage is not met (when ``--min-coverage`` is set). - The packaged configuration registers the DevTools PHPUnit extension. diff --git a/docs/running/specialized-commands.rst b/docs/running/specialized-commands.rst index 0e683807a4..ebd5fa52fb 100644 --- a/docs/running/specialized-commands.rst +++ b/docs/running/specialized-commands.rst @@ -65,8 +65,10 @@ Important details: automatically; - ``--pretty-json`` stays valid JSON and does not add ANSI color escapes; - in agent-driven runs, the command also captures PHPUnit output in structured - mode automatically and exposes a ``phpunit`` summary object when the bundled - agent reporter is active; + mode automatically and stores the bundled PHPUnit agent-reporter payload in + ``output``. ``--json`` and ``--pretty-json`` therefore keep the same + structured shape, with ``raw_output`` preserved under ``output`` when + PHPUnit writes extra text before the final JSON; - the packaged configuration registers the DevTools PHPUnit extension. ``dependencies`` diff --git a/src/Console/Command/TestsCommand.php b/src/Console/Command/TestsCommand.php index dda5089369..bad6c85a75 100644 --- a/src/Console/Command/TestsCommand.php +++ b/src/Console/Command/TestsCommand.php @@ -45,6 +45,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Process\Process; use function is_numeric; use function Safe\json_decode; @@ -61,6 +62,10 @@ final class TestsCommand extends Command use HasJsonOption; use LogsCommandResults; + private const string AGENT_ENVIRONMENT_VARIABLE = 'AI_AGENT'; + + private const string AGENT_ENVIRONMENT_VALUE = 'fast-forward/dev-tools'; + private const string PROCESS_LABEL = 'Running PHPUnit Tests'; /** @@ -168,9 +173,12 @@ protected function configure(): void */ protected function execute(InputInterface $input, OutputInterface $output): int { - $jsonOutput = $this->isJsonOutput($input) + $explicitJsonOutput = (bool) $input->getOption('json'); + $prettyJsonOutput = $this->isPrettyJsonOutput($input); + $structuredOutput = $prettyJsonOutput + || $explicitJsonOutput || ($this->runtimeEnvironment->isAgentPresent() && ! $this->runtimeEnvironment->isComposerTestRun()); - $processOutput = $jsonOutput ? new BufferedOutput() : $output; + $processOutput = $structuredOutput ? new BufferedOutput() : $output; $cacheEnabled = $this->isCacheEnabled($input); $this->getLogger() @@ -215,11 +223,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int ->withArgument('--display-incomplete') ->withArgument('--display-skipped'); - if (! $input->getOption('progress') || $jsonOutput) { + if (! $input->getOption('progress') || $structuredOutput) { $processBuilder = $processBuilder->withArgument('--no-progress'); } - if (! $jsonOutput) { + if (! $structuredOutput) { $processBuilder = $processBuilder->withArgument('--colors=always'); } @@ -241,15 +249,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int $processBuilder = $processBuilder->withArgument('--filter', $input->getOption('filter')); } - $this->processQueue->add( - process: $processBuilder - ->withArgument($input->getArgument('path')) - ->build([DevToolsPathResolver::getPreferredToolBinaryPath('phpunit')]), - label: self::PROCESS_LABEL, - ); + $process = $processBuilder + ->withArgument($input->getArgument('path')) + ->build([DevToolsPathResolver::getPreferredToolBinaryPath('phpunit')]); + + if ($structuredOutput) { + $this->forceAgentReporter($process); + } + + $this->processQueue->add(process: $process, label: self::PROCESS_LABEL); $result = $this->processQueue->run($processOutput); - $processResultContext = $this->resolveProcessResultContext($processOutput, $result, $jsonOutput); + $processResultContext = $this->resolveProcessResultContext($processOutput, $result, $structuredOutput); if (self::SUCCESS !== $result || null === $minimumCoverage || null === $coverageReportPath) { if (self::SUCCESS === $result) { @@ -264,6 +275,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int $minimumCoverage, ); + if ($structuredOutput) { + $processResultContext = $this->withStructuredCoverageValidationContext( + $processResultContext, + $coverageContext, + $minimumCoverage, + $validationResult, + $message, + ); + } + if (self::SUCCESS === $validationResult) { return $this->success($message, $input, [...$processResultContext, ...$coverageContext]); } @@ -283,46 +304,110 @@ protected function execute(InputInterface $input, OutputInterface $output): int private function resolveProcessResultContext( OutputInterface $processOutput, int $exitCode, - bool $structuredOutput + bool $structuredOutput, ): array { - if (! $structuredOutput) { + if ($structuredOutput) { return [ - 'output' => $processOutput, + 'output' => $this->resolveStructuredProcessResultPayload($processOutput, $exitCode), ]; } - $context = [ - 'phpunit' => [ - 'tool' => 'phpunit', - 'label' => self::PROCESS_LABEL, - 'exit_code' => $exitCode, - ], + return [ + 'output' => $processOutput, + ]; + } + + /** + * Forces the PHPUnit subprocess to expose the agent reporter payload. + * + * @param Process $process the configured PHPUnit process + * + * @return void + */ + private function forceAgentReporter(Process $process): void + { + $env = $process->getEnv(); + + if (\array_key_exists(self::AGENT_ENVIRONMENT_VARIABLE, $env)) { + return; + } + + $env[self::AGENT_ENVIRONMENT_VARIABLE] = self::AGENT_ENVIRONMENT_VALUE; + $process->setEnv($env); + } + + /** + * Builds the structured payload that will be emitted for agent-oriented runs. + * + * @param OutputInterface $processOutput the output sink used while the process ran + * @param int $exitCode the exit code returned by the process queue + * + * @return array the structured process payload + */ + private function resolveStructuredProcessResultPayload(OutputInterface $processOutput, int $exitCode): array + { + $payload = [ + 'result' => self::SUCCESS === $exitCode ? 'success' : 'failure', ]; if (! $processOutput instanceof BufferedOutput) { - return $context; + return $payload; } $rawOutput = trim($processOutput->fetch()); if ('' === $rawOutput) { - return $context; + return $payload; } [$decoded, $supplementalOutput] = $this->decodeStructuredProcessOutput($rawOutput); - if (! \is_array($decoded)) { - $context['phpunit']['raw_output'] = $supplementalOutput; + if (\is_array($decoded)) { + $payload = $decoded; + } + if (null !== $supplementalOutput) { + $payload['raw_output'] = $supplementalOutput; + } + + return $payload; + } + + /** + * Appends minimum-coverage validation data to the structured PHPUnit output payload. + * + * @param array $context the command result context + * @param array $coverageContext structured coverage metrics + * @param float $minimumCoverage the required coverage percentage + * @param int $validationResult the post-PHPUnit validation status + * @param string $message the validation message + * + * @return array the enriched structured command context + */ + private function withStructuredCoverageValidationContext( + array $context, + array $coverageContext, + float $minimumCoverage, + int $validationResult, + string $message, + ): array { + if (! isset($context['output']) || ! \is_array($context['output'])) { return $context; } - $context['phpunit'] = [...$context['phpunit'], ...$decoded]; + $payload = $context['output']; + $payload['coverage'] = [ + ...$coverageContext, + 'minimum' => $minimumCoverage, + ]; - if (null !== $supplementalOutput) { - $context['phpunit']['raw_output'] = $supplementalOutput; + if (self::SUCCESS !== $validationResult) { + $payload['message'] = $message; + $payload['result'] = 'failure'; } + $context['output'] = $payload; + return $context; } diff --git a/tests/Console/Command/TestsCommandTest.php b/tests/Console/Command/TestsCommandTest.php index db0e67e747..1efa453962 100644 --- a/tests/Console/Command/TestsCommandTest.php +++ b/tests/Console/Command/TestsCommandTest.php @@ -240,22 +240,32 @@ public function executeWillDisablePhpUnitProgressWhenJsonIsRequested(): void Argument::that(static fn(Process $process): bool => str_contains( $process->getCommandLine(), '--no-progress', - ) && ! str_contains($process->getCommandLine(), '--colors=always')), + ) && ! str_contains($process->getCommandLine(), '--colors=always') + && 'fast-forward/dev-tools' === $process->getEnv()['AI_AGENT']), false, false, 'Running PHPUnit Tests' )->shouldBeCalled(); $this->processQueue->run(Argument::type(OutputInterface::class)) - ->willReturn(TestsCommand::SUCCESS)->shouldBeCalled(); + ->will(static function (array $arguments): int { + $arguments[0]->write( + "{\n \"result\": \"success\",\n \"summary\": {\n \"assertions\": 5,\n \"failures\": 0,\n \"tests\": 2,\n \"warnings\": 0\n }\n}\n" + ); + + return TestsCommand::SUCCESS; + })->shouldBeCalled(); $this->logger->info('Running PHPUnit tests...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface - )) - ->shouldBeCalled(); + ))->shouldBeCalled(); $this->logger->log( 'info', 'PHPUnit tests completed successfully.', - Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface), + Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface + && isset($context['output']) + && 'success' === $context['output']['result'] + && 5 === $context['output']['summary']['assertions']), )->shouldBeCalled(); + $this->output->writeln(Argument::cetera())->shouldNotBeCalled(); self::assertSame(TestsCommand::SUCCESS, $this->invokeExecute()); } @@ -275,7 +285,52 @@ public function executeWillCaptureStructuredPhpUnitSummaryWhenAgentEnvironmentIs Argument::that(static fn(Process $process): bool => str_contains( $process->getCommandLine(), '--no-progress', - ) && ! str_contains($process->getCommandLine(), '--colors=always')), + ) && ! str_contains($process->getCommandLine(), '--colors=always') + && 'fast-forward/dev-tools' === $process->getEnv()['AI_AGENT']), + false, + false, + 'Running PHPUnit Tests' + )->shouldBeCalled(); + $this->processQueue->run(Argument::type(OutputInterface::class)) + ->will(static function (array $arguments): int { + $arguments[0]->write( + "{\n \"result\": \"success\",\n \"summary\": {\n \"assertions\": 5,\n \"failures\": 0,\n \"tests\": 2,\n \"warnings\": 0\n }\n}\n" + ); + + return TestsCommand::SUCCESS; + })->shouldBeCalled(); + $this->logger->info('Running PHPUnit tests...', Argument::that( + static fn(array $context): bool => $context['input'] instanceof InputInterface + ))->shouldBeCalled(); + $this->logger->log( + 'info', + 'PHPUnit tests completed successfully.', + Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface + && isset($context['output']) + && 'success' === $context['output']['result'] + && 5 === $context['output']['summary']['assertions']), + )->shouldBeCalled(); + $this->output->writeln(Argument::cetera())->shouldNotBeCalled(); + + self::assertSame(TestsCommand::SUCCESS, $this->invokeExecute()); + } + + /** + * @return void + */ + #[Test] + public function executeWillKeepPrettyJsonInsideTheStandardCommandLogOutput(): void + { + $this->input->getOption('json') + ->willReturn(false); + $this->input->getOption('pretty-json') + ->willReturn(true); + + $this->processQueue->add( + Argument::that(static fn(Process $process): bool => str_contains( + $process->getCommandLine(), + '--no-progress', + ) && 'fast-forward/dev-tools' === $process->getEnv()['AI_AGENT']), false, false, 'Running PHPUnit Tests' @@ -295,14 +350,11 @@ public function executeWillCaptureStructuredPhpUnitSummaryWhenAgentEnvironmentIs 'info', 'PHPUnit tests completed successfully.', Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface - && isset($context['phpunit']) - && 'phpunit' === $context['phpunit']['tool'] - && 'Running PHPUnit Tests' === $context['phpunit']['label'] - && TestsCommand::SUCCESS === $context['phpunit']['exit_code'] - && 'success' === $context['phpunit']['result'] - && 5 === $context['phpunit']['summary']['assertions'] - && ! isset($context['output'])), + && isset($context['output']) + && 'success' === $context['output']['result'] + && 5 === $context['output']['summary']['assertions']), )->shouldBeCalled(); + $this->output->writeln(Argument::cetera())->shouldNotBeCalled(); self::assertSame(TestsCommand::SUCCESS, $this->invokeExecute()); } @@ -339,11 +391,12 @@ public function executeWillCaptureStructuredPhpUnitSummaryAfterCoveragePreludeWh 'info', 'PHPUnit tests completed successfully.', Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface - && isset($context['phpunit']) - && 'success' === $context['phpunit']['result'] - && 5 === $context['phpunit']['summary']['assertions'] - && 'Generating code coverage report in PHP format ... done [00:00.002]' === $context['phpunit']['raw_output']), + && isset($context['output']) + && 'success' === $context['output']['result'] + && 5 === $context['output']['summary']['assertions'] + && 'Generating code coverage report in PHP format ... done [00:00.002]' === $context['output']['raw_output']), )->shouldBeCalled(); + $this->output->writeln(Argument::cetera())->shouldNotBeCalled(); self::assertSame(TestsCommand::SUCCESS, $this->invokeExecute()); } @@ -365,26 +418,24 @@ public function executeWillKeepRawPhpUnitOutputWhenStructuredSummaryCannotBeDeco false, 'Running PHPUnit Tests' )->shouldBeCalled(); - $this->logger->info('Running PHPUnit tests...', Argument::that( - static fn(array $context): bool => $context['input'] instanceof InputInterface - ))->shouldBeCalled(); $this->processQueue->run(Argument::type(OutputInterface::class)) ->will(static function (array $arguments): int { $arguments[0]->write("PHPUnit 12.5.24 by Sebastian Bergmann.\n\nOK (2 tests, 5 assertions)\n"); return TestsCommand::SUCCESS; })->shouldBeCalled(); + $this->logger->info('Running PHPUnit tests...', Argument::that( + static fn(array $context): bool => $context['input'] instanceof InputInterface + ))->shouldBeCalled(); $this->logger->log( 'info', 'PHPUnit tests completed successfully.', Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface - && isset($context['phpunit']) - && 'phpunit' === $context['phpunit']['tool'] - && 'Running PHPUnit Tests' === $context['phpunit']['label'] - && TestsCommand::SUCCESS === $context['phpunit']['exit_code'] - && "PHPUnit 12.5.24 by Sebastian Bergmann.\n\nOK (2 tests, 5 assertions)" === $context['phpunit']['raw_output'] - && ! isset($context['output'])), + && isset($context['output']) + && 'success' === $context['output']['result'] + && "PHPUnit 12.5.24 by Sebastian Bergmann.\n\nOK (2 tests, 5 assertions)" === $context['output']['raw_output']), )->shouldBeCalled(); + $this->output->writeln(Argument::cetera())->shouldNotBeCalled(); self::assertSame(TestsCommand::SUCCESS, $this->invokeExecute()); } @@ -523,6 +574,54 @@ public function executeWithCoverageBelowMinimumWillReturnFailure(): void self::assertSame(TestsCommand::FAILURE, $this->invokeExecute()); } + /** + * @return void + */ + #[Test] + public function executeWillEmitStructuredCoverageFailurePayloadWhenMinimumCoverageIsNotMet(): void + { + $coverageReportPath = getcwd() . '/.dev-tools/cache/phpunit/coverage.php'; + + $this->runtimeEnvironment->isAgentPresent() + ->willReturn(true); + $this->runtimeEnvironment->isComposerTestRun() + ->willReturn(false); + $this->input->getOption('min-coverage') + ->willReturn('80'); + $this->coverageSummaryLoader->load($coverageReportPath) + ->willReturn(new CoverageSummary(75, 100)); + $this->processQueue->add( + Argument::type(Process::class), + false, + false, + 'Running PHPUnit Tests' + )->shouldBeCalled(); + $this->processQueue->run(Argument::type(OutputInterface::class)) + ->will(static function (array $arguments): int { + $arguments[0]->write( + "{\n \"result\": \"success\",\n \"summary\": {\n \"assertions\": 5,\n \"failures\": 0,\n \"tests\": 2,\n \"warnings\": 0\n }\n}\n" + ); + + return TestsCommand::SUCCESS; + })->shouldBeCalled(); + $this->logger->info('Running PHPUnit tests...', Argument::that( + static fn(array $context): bool => $context['input'] instanceof InputInterface + ))->shouldBeCalled(); + $this->logger->log(Argument::cetera())->shouldNotBeCalled(); + $this->logger->error( + 'Minimum line coverage of 80.00% was not met. Current coverage: 75.00% (75/100 lines).', + Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface + && isset($context['output']) + && 'failure' === $context['output']['result'] + && 80.0 === (float) $context['output']['coverage']['minimum'] + && 75.0 === (float) $context['output']['coverage']['line_coverage'] + && 'Minimum line coverage of 80.00% was not met. Current coverage: 75.00% (75/100 lines).' === $context['output']['message']), + )->shouldBeCalled(); + $this->output->writeln(Argument::cetera())->shouldNotBeCalled(); + + self::assertSame(TestsCommand::FAILURE, $this->invokeExecute()); + } + /** * @return void */ From cbd844358dcf7aee69c042702d3ef2bb3e7db38a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 16:05:24 -0300 Subject: [PATCH 04/35] [tests] Suppress extra JSON progress records --- docs/commands/tests.rst | 2 ++ src/Console/Command/TestsCommand.php | 10 +++++---- tests/Console/Command/TestsCommandTest.php | 24 ++++++---------------- 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/docs/commands/tests.rst b/docs/commands/tests.rst index 004bc71826..23eda8128a 100644 --- a/docs/commands/tests.rst +++ b/docs/commands/tests.rst @@ -167,6 +167,8 @@ Behavior command stores that payload inside ``output`` while keeping the standard DevTools JSON envelope. ``--json`` and ``--pretty-json`` therefore expose the same structured result, with formatting as the only difference. +- in structured mode, the command suppresses intermediary ``Running...`` log + records so the output stream contains a single final JSON document. - when structured capture is active but PHPUnit does not emit parseable JSON, the command preserves the raw subprocess text inside ``output.raw_output`` instead of dropping it. diff --git a/src/Console/Command/TestsCommand.php b/src/Console/Command/TestsCommand.php index bad6c85a75..be3a09646b 100644 --- a/src/Console/Command/TestsCommand.php +++ b/src/Console/Command/TestsCommand.php @@ -181,10 +181,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $processOutput = $structuredOutput ? new BufferedOutput() : $output; $cacheEnabled = $this->isCacheEnabled($input); - $this->getLogger() - ->info('Running PHPUnit tests...', [ - 'input' => $input, - ]); + if (! $structuredOutput) { + $this->getLogger() + ->info('Running PHPUnit tests...', [ + 'input' => $input, + ]); + } try { $minimumCoverage = $this->resolveMinimumCoverage($input); diff --git a/tests/Console/Command/TestsCommandTest.php b/tests/Console/Command/TestsCommandTest.php index 1efa453962..0e5183da52 100644 --- a/tests/Console/Command/TestsCommandTest.php +++ b/tests/Console/Command/TestsCommandTest.php @@ -254,9 +254,7 @@ public function executeWillDisablePhpUnitProgressWhenJsonIsRequested(): void return TestsCommand::SUCCESS; })->shouldBeCalled(); - $this->logger->info('Running PHPUnit tests...', Argument::that( - static fn(array $context): bool => $context['input'] instanceof InputInterface - ))->shouldBeCalled(); + $this->logger->info(Argument::cetera())->shouldNotBeCalled(); $this->logger->log( 'info', 'PHPUnit tests completed successfully.', @@ -299,9 +297,7 @@ public function executeWillCaptureStructuredPhpUnitSummaryWhenAgentEnvironmentIs return TestsCommand::SUCCESS; })->shouldBeCalled(); - $this->logger->info('Running PHPUnit tests...', Argument::that( - static fn(array $context): bool => $context['input'] instanceof InputInterface - ))->shouldBeCalled(); + $this->logger->info(Argument::cetera())->shouldNotBeCalled(); $this->logger->log( 'info', 'PHPUnit tests completed successfully.', @@ -343,9 +339,7 @@ public function executeWillKeepPrettyJsonInsideTheStandardCommandLogOutput(): vo return TestsCommand::SUCCESS; })->shouldBeCalled(); - $this->logger->info('Running PHPUnit tests...', Argument::that( - static fn(array $context): bool => $context['input'] instanceof InputInterface - ))->shouldBeCalled(); + $this->logger->info(Argument::cetera())->shouldNotBeCalled(); $this->logger->log( 'info', 'PHPUnit tests completed successfully.', @@ -384,9 +378,7 @@ public function executeWillCaptureStructuredPhpUnitSummaryAfterCoveragePreludeWh return TestsCommand::SUCCESS; })->shouldBeCalled(); - $this->logger->info('Running PHPUnit tests...', Argument::that( - static fn(array $context): bool => $context['input'] instanceof InputInterface - ))->shouldBeCalled(); + $this->logger->info(Argument::cetera())->shouldNotBeCalled(); $this->logger->log( 'info', 'PHPUnit tests completed successfully.', @@ -424,9 +416,7 @@ public function executeWillKeepRawPhpUnitOutputWhenStructuredSummaryCannotBeDeco return TestsCommand::SUCCESS; })->shouldBeCalled(); - $this->logger->info('Running PHPUnit tests...', Argument::that( - static fn(array $context): bool => $context['input'] instanceof InputInterface - ))->shouldBeCalled(); + $this->logger->info(Argument::cetera())->shouldNotBeCalled(); $this->logger->log( 'info', 'PHPUnit tests completed successfully.', @@ -604,9 +594,7 @@ public function executeWillEmitStructuredCoverageFailurePayloadWhenMinimumCovera return TestsCommand::SUCCESS; })->shouldBeCalled(); - $this->logger->info('Running PHPUnit tests...', Argument::that( - static fn(array $context): bool => $context['input'] instanceof InputInterface - ))->shouldBeCalled(); + $this->logger->info(Argument::cetera())->shouldNotBeCalled(); $this->logger->log(Argument::cetera())->shouldNotBeCalled(); $this->logger->error( 'Minimum line coverage of 80.00% was not met. Current coverage: 75.00% (75/100 lines).', From 5c7a2c12350844cec6b55f2712e31d9cb6ad616f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 16:17:43 -0300 Subject: [PATCH 05/35] [console] Decode nested structured command output --- src/Console/Command/TestsCommand.php | 5 +- .../Processor/CommandOutputProcessor.php | 152 +++++++++++++++++- tests/Console/Command/TestsCommandTest.php | 38 +++++ .../Console/Logger/OutputFormatLoggerTest.php | 43 +++++ .../Processor/CommandOutputProcessorTest.php | 53 ++++++ 5 files changed, 287 insertions(+), 4 deletions(-) diff --git a/src/Console/Command/TestsCommand.php b/src/Console/Command/TestsCommand.php index be3a09646b..66bb0b72ae 100644 --- a/src/Console/Command/TestsCommand.php +++ b/src/Console/Command/TestsCommand.php @@ -365,7 +365,10 @@ private function resolveStructuredProcessResultPayload(OutputInterface $processO [$decoded, $supplementalOutput] = $this->decodeStructuredProcessOutput($rawOutput); if (\is_array($decoded)) { - $payload = $decoded; + $payload = [ + ...$decoded, + 'result' => $payload['result'], + ]; } if (null !== $supplementalOutput) { diff --git a/src/Console/Logger/Processor/CommandOutputProcessor.php b/src/Console/Logger/Processor/CommandOutputProcessor.php index 42fbda0334..0948fdd79d 100644 --- a/src/Console/Logger/Processor/CommandOutputProcessor.php +++ b/src/Console/Logger/Processor/CommandOutputProcessor.php @@ -19,12 +19,18 @@ namespace FastForward\DevTools\Console\Logger\Processor; +use JsonException; use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; +use function Safe\json_decode; + /** * Converts buffered command output objects into serializable context entries. + * + * JSON payloads are decoded eagerly so parent command envelopes can expose + * nested structured output without re-encoding it as an escaped string. */ final class CommandOutputProcessor implements ContextProcessorInterface { @@ -63,14 +69,154 @@ public function process(array $context): array /** * @param OutputInterface $output * - * @return ?string + * @return mixed */ - private function extractBufferedOutput(OutputInterface $output): ?string + private function extractBufferedOutput(OutputInterface $output): mixed { if (! $output instanceof BufferedOutput) { return null; } - return $output->fetch(); + $content = $output->fetch(); + + return $this->decodeStructuredOutput($content); + } + + /** + * Decodes a buffered output string when it contains JSON content. + * + * @param string $content the buffered output contents + * + * @return mixed the decoded JSON payload or the original string + */ + private function decodeStructuredOutput(string $content): mixed + { + $trimmedContent = trim($content); + + if ('' === $trimmedContent) { + return $content; + } + + try { + return json_decode($trimmedContent, true); + } catch (JsonException) { + } + + $decodedDocuments = $this->decodeJsonDocumentStream($trimmedContent); + + if (null !== $decodedDocuments) { + return $decodedDocuments; + } + + return $content; + } + + /** + * Decodes a stream that contains multiple JSON documents separated by whitespace. + * + * @param string $content the buffered output contents + * + * @return ?list the decoded JSON documents when the stream is valid + */ + private function decodeJsonDocumentStream(string $content): ?array + { + $decodedDocuments = []; + $offset = 0; + $length = \strlen($content); + + while ($offset < $length) { + while ($offset < $length && ctype_space($content[$offset])) { + ++$offset; + } + + if ($offset >= $length) { + break; + } + + $document = $this->consumeJsonDocument($content, $offset); + + if (null === $document) { + return null; + } + + try { + $decodedDocuments[] = json_decode($document, true); + } catch (JsonException) { + return null; + } + } + + return \count($decodedDocuments) > 1 ? $decodedDocuments : null; + } + + /** + * Consumes a single top-level JSON document from a multi-document stream. + * + * @param string $content the buffered output contents + * @param int $offset the current stream offset, advanced past the document on success + * + * @return ?string the extracted JSON document + */ + private function consumeJsonDocument(string $content, int &$offset): ?string + { + $length = \strlen($content); + $start = $offset; + $openingToken = $content[$offset]; + + if ('{' !== $openingToken && '[' !== $openingToken) { + return null; + } + + $depth = 0; + $inString = false; + $escaping = false; + + for (; $offset < $length; ++$offset) { + $character = $content[$offset]; + + if ($inString) { + if ($escaping) { + $escaping = false; + + continue; + } + + if ('\\' === $character) { + $escaping = true; + + continue; + } + + if ('"' === $character) { + $inString = false; + } + + continue; + } + + if ('"' === $character) { + $inString = true; + + continue; + } + + if ('{' === $character || '[' === $character) { + ++$depth; + + continue; + } + + if ('}' === $character || ']' === $character) { + --$depth; + + if (0 === $depth) { + ++$offset; + + return substr($content, $start, $offset - $start); + } + } + } + + return null; } } diff --git a/tests/Console/Command/TestsCommandTest.php b/tests/Console/Command/TestsCommandTest.php index 0e5183da52..e4de221b74 100644 --- a/tests/Console/Command/TestsCommandTest.php +++ b/tests/Console/Command/TestsCommandTest.php @@ -393,6 +393,44 @@ public function executeWillCaptureStructuredPhpUnitSummaryAfterCoveragePreludeWh self::assertSame(TestsCommand::SUCCESS, $this->invokeExecute()); } + /** + * @return void + */ + #[Test] + public function executeWillKeepTheExitCodeDerivedFailureResultAuthoritative(): void + { + $this->input->getOption('json') + ->willReturn(true); + $this->input->getOption('pretty-json') + ->willReturn(false); + + $this->processQueue->add( + Argument::type(Process::class), + false, + false, + 'Running PHPUnit Tests' + )->shouldBeCalled(); + $this->processQueue->run(Argument::type(OutputInterface::class)) + ->will(static function (array $arguments): int { + $arguments[0]->write( + "{\n \"result\": \"success\",\n \"summary\": {\n \"assertions\": 5,\n \"failures\": 1,\n \"tests\": 2,\n \"warnings\": 0\n }\n}\n" + ); + + return TestsCommand::FAILURE; + })->shouldBeCalled(); + $this->logger->info(Argument::cetera())->shouldNotBeCalled(); + $this->logger->error( + 'PHPUnit tests failed.', + Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface + && isset($context['output']) + && 'failure' === $context['output']['result'] + && 1 === $context['output']['summary']['failures']), + )->shouldBeCalled(); + $this->output->writeln(Argument::cetera())->shouldNotBeCalled(); + + self::assertSame(TestsCommand::FAILURE, $this->invokeExecute()); + } + /** * @return void */ diff --git a/tests/Console/Logger/OutputFormatLoggerTest.php b/tests/Console/Logger/OutputFormatLoggerTest.php index d79dbd0035..eb81dcf6f0 100644 --- a/tests/Console/Logger/OutputFormatLoggerTest.php +++ b/tests/Console/Logger/OutputFormatLoggerTest.php @@ -38,6 +38,7 @@ use Psr\Clock\ClockInterface; use Stringable; use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -265,6 +266,48 @@ public function logWillEmitParseableJsonWhenPrettyJsonOutputIsRequested(): void ]); } + /** + * @return void + */ + #[Test] + public function logWillEmbedDecodedStructuredCommandOutputInsteadOfEscapedJsonStrings(): void + { + $logger = new OutputFormatLogger( + new ArgvInput(['dev-tools', '--pretty-json']), + $this->output->reveal(), + $this->clock->reveal(), + new Detector(), + new CompositeContextProcessor([new CommandInputProcessor(), new CommandOutputProcessor()]), + $this->createGithubActionOutput(), + ); + $commandOutput = new BufferedOutput(); + $commandOutput->write( + "{\"message\":\"docs\"}\n{\"message\":\"tests\",\"context\":{\"output\":{\"result\":\"success\"}}}\n" + ); + + $this->output->writeln(Argument::that(static function (string $payload): bool { + $decoded = json_decode($payload, true, 512, \JSON_THROW_ON_ERROR); + + return [ + [ + 'message' => 'docs', + ], + [ + 'message' => 'tests', + 'context' => [ + 'output' => [ + 'result' => 'success', + ], + ], + ], + ] === $decoded['context']['output']; + }))->shouldBeCalledOnce(); + + $logger->info('Reports ready.', [ + 'output' => $commandOutput, + ]); + } + /** * @return void */ diff --git a/tests/Console/Logger/Processor/CommandOutputProcessorTest.php b/tests/Console/Logger/Processor/CommandOutputProcessorTest.php index d75540c1ef..8ceb420538 100644 --- a/tests/Console/Logger/Processor/CommandOutputProcessorTest.php +++ b/tests/Console/Logger/Processor/CommandOutputProcessorTest.php @@ -49,6 +49,59 @@ public function processWillReplaceBufferedOutputWithItsContents(): void self::assertSame("done\n", $context['output']); } + /** + * @return void + */ + #[Test] + public function processWillDecodeSingleJsonBufferedOutput(): void + { + $processor = new CommandOutputProcessor(); + $output = new BufferedOutput(); + $output->write("{\n \"result\": \"success\",\n \"summary\": {\n \"tests\": 2\n }\n}\n"); + + $context = $processor->process([ + 'output' => $output, + ]); + + self::assertSame([ + 'result' => 'success', + 'summary' => [ + 'tests' => 2, + ], + ], $context['output']); + } + + /** + * @return void + */ + #[Test] + public function processWillDecodeMultipleJsonBufferedOutputsIntoAList(): void + { + $processor = new CommandOutputProcessor(); + $output = new BufferedOutput(); + $output->write( + "{\"message\":\"docs\"}\n{\"message\":\"tests\",\"context\":{\"output\":{\"result\":\"success\"}}}\n" + ); + + $context = $processor->process([ + 'output' => $output, + ]); + + self::assertSame([ + [ + 'message' => 'docs', + ], + [ + 'message' => 'tests', + 'context' => [ + 'output' => [ + 'result' => 'success', + ], + ], + ], + ], $context['output']); + } + /** * @return void */ From 36e4a77f5309efb273da83d02c2c0f6db7c3eca8 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 19:19:54 +0000 Subject: [PATCH 06/35] Update wiki submodule pointer for PR #329 --- .github/wiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/wiki b/.github/wiki index f556757bbc..8995313710 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit f556757bbc35e4ae7ca5144a717ae9bc4e551334 +Subproject commit 8995313710eb82dbe1073e7b5851973b63f82b22 From 37b5ba1f1b01f7cdfac94a52fc98609c0e037430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 16:39:18 -0300 Subject: [PATCH 07/35] [console] Suppress structured progress preambles --- src/Console/Command/CodeStyleCommand.php | 2 +- src/Console/Command/DependenciesCommand.php | 6 +-- src/Console/Command/DocsCommand.php | 4 +- src/Console/Command/MetricsCommand.php | 4 +- src/Console/Command/PhpDocCommand.php | 12 ++--- src/Console/Command/RefactorCommand.php | 4 +- src/Console/Command/ReportsCommand.php | 4 +- src/Console/Command/StandardsCommand.php | 4 +- src/Console/Command/SyncCommand.php | 4 +- .../Command/Traits/LogsCommandResults.php | 22 +++++++++ src/Console/Command/WikiCommand.php | 6 +-- .../Console/Command/CodeStyleCommandTest.php | 12 +++-- .../Command/DependenciesCommandTest.php | 26 +++++++++++ tests/Console/Command/DocsCommandTest.php | 32 +++++++++++++ tests/Console/Command/MetricsCommandTest.php | 8 ++++ tests/Console/Command/PhpDocCommandTest.php | 12 ++++- tests/Console/Command/RefactorCommandTest.php | 8 ++++ tests/Console/Command/ReportsCommandTest.php | 6 +-- .../Console/Command/StandardsCommandTest.php | 31 +++++++++++++ tests/Console/Command/SyncCommandTest.php | 6 +-- tests/Console/Command/WikiCommandTest.php | 45 +++++++++++++++++++ 21 files changed, 210 insertions(+), 48 deletions(-) diff --git a/src/Console/Command/CodeStyleCommand.php b/src/Console/Command/CodeStyleCommand.php index 9a59f933af..5eb59ecec3 100644 --- a/src/Console/Command/CodeStyleCommand.php +++ b/src/Console/Command/CodeStyleCommand.php @@ -121,7 +121,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $fix = (bool) $input->getOption('fix'); $progress = ! $jsonOutput && (bool) $input->getOption('progress'); - $this->logger->info('Running code style checks and fixes...'); + $this->intermediateInfo('Running code style checks and fixes...', $input); $composerUpdate = $this->processBuilder ->withArgument('--lock') diff --git a/src/Console/Command/DependenciesCommand.php b/src/Console/Command/DependenciesCommand.php index 11babe2b91..8f8a7c81a0 100644 --- a/src/Console/Command/DependenciesCommand.php +++ b/src/Console/Command/DependenciesCommand.php @@ -150,11 +150,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int ); } - if (! $jsonOutput) { - $this->logger->info('Running dependency analysis...', [ - 'input' => $input, - ]); - } + $this->intermediateInfo('Running dependency analysis...', $input); $this->processQueue->add( process: $this->getComposerDependencyAnalyserCommand($input), diff --git a/src/Console/Command/DocsCommand.php b/src/Console/Command/DocsCommand.php index a8118afb98..5f80e61409 100644 --- a/src/Console/Command/DocsCommand.php +++ b/src/Console/Command/DocsCommand.php @@ -155,9 +155,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $template = DevToolsPathResolver::getPreferredVendorPath(self::DEFAULT_TEMPLATE); } - $this->logger->info('Generating API documentation...', [ - 'input' => $input, - ]); + $this->intermediateInfo('Generating API documentation...', $input); if ( ! $projectCapabilities->hasGuideDirectory() diff --git a/src/Console/Command/MetricsCommand.php b/src/Console/Command/MetricsCommand.php index 972b47358b..a346def444 100644 --- a/src/Console/Command/MetricsCommand.php +++ b/src/Console/Command/MetricsCommand.php @@ -135,9 +135,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $exclude = (string) $input->getOption('exclude'); $junit = $input->getOption('junit'); - $this->logger->info('Running code metrics analysis...', [ - 'input' => $input, - ]); + $this->intermediateInfo('Running code metrics analysis...', $input); $processBuilder = $this->processBuilder ->withArgument('--ansi') diff --git a/src/Console/Command/PhpDocCommand.php b/src/Console/Command/PhpDocCommand.php index 52d5627799..ae93f35bdd 100644 --- a/src/Console/Command/PhpDocCommand.php +++ b/src/Console/Command/PhpDocCommand.php @@ -152,11 +152,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int $progress = ! $jsonOutput && (bool) $input->getOption('progress'); $cacheEnabled = $this->isCacheEnabled($input); - $this->logger->info('Checking and fixing PHPDocs...', [ - 'input' => $input, - ]); + $this->intermediateInfo('Checking and fixing PHPDocs...', $input); - $this->ensureDocHeaderExists(); + $this->ensureDocHeaderExists($input); $processBuilder = $this->processBuilder ->withArgument('--ansi') @@ -231,9 +229,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int * The method MUST query the local filesystem. If the file is missing, it SHOULD copy * the tool template into the root folder. * + * @param InputInterface $input the originating command input + * * @return void */ - private function ensureDocHeaderExists(): void + private function ensureDocHeaderExists(InputInterface $input): void { $support = $this->composer->getSupport(); @@ -265,6 +265,6 @@ private function ensureDocHeaderExists(): void return; } - $this->logger->info('Created .docheader from repository template.'); + $this->intermediateInfo('Created .docheader from repository template.', $input); } } diff --git a/src/Console/Command/RefactorCommand.php b/src/Console/Command/RefactorCommand.php index f2a1d51bbc..6eca3fb806 100644 --- a/src/Console/Command/RefactorCommand.php +++ b/src/Console/Command/RefactorCommand.php @@ -120,9 +120,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $fix = (bool) $input->getOption('fix'); $progress = ! $jsonOutput && (bool) $input->getOption('progress'); - $this->logger->info('Running Rector for code refactoring...', [ - 'input' => $input, - ]); + $this->intermediateInfo('Running Rector for code refactoring...', $input); $processBuilder = $this->processBuilder ->withArgument('--ansi') diff --git a/src/Console/Command/ReportsCommand.php b/src/Console/Command/ReportsCommand.php index 39daed3c75..5a4e1edbc3 100644 --- a/src/Console/Command/ReportsCommand.php +++ b/src/Console/Command/ReportsCommand.php @@ -129,9 +129,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $coveragePath = (string) $input->getOption('coverage'); $metricsPath = (string) $input->getOption('metrics'); - $this->logger->info('Generating frontpage for Fast Forward documentation...', [ - 'input' => $input, - ]); + $this->intermediateInfo('Generating frontpage for Fast Forward documentation...', $input); $docsBuilder = $this->processBuilder ->withArgument('--target', $target); diff --git a/src/Console/Command/StandardsCommand.php b/src/Console/Command/StandardsCommand.php index bef10eedc3..62237a0a40 100644 --- a/src/Console/Command/StandardsCommand.php +++ b/src/Console/Command/StandardsCommand.php @@ -109,9 +109,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $commands = []; $fix = (bool) $input->getOption('fix'); - $this->logger->info('Running code standards checks...', [ - 'input' => $input, - ]); + $this->intermediateInfo('Running code standards checks...', $input); foreach (['refactor', 'phpdoc', 'code-style', 'reports'] as $command) { $commands[] = $command; diff --git a/src/Console/Command/SyncCommand.php b/src/Console/Command/SyncCommand.php index 85fc82af78..fca5f1c16a 100644 --- a/src/Console/Command/SyncCommand.php +++ b/src/Console/Command/SyncCommand.php @@ -112,9 +112,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int ]; $allowDetached = ! $dryRun && ! $check && ! $interactive; - $this->logger->info('Starting dev-tools synchronization...', [ - 'input' => $input, - ]); + $this->intermediateInfo('Starting dev-tools synchronization...', $input); $this->queueDevToolsCommand(['update-composer-json', ...$modeArguments], false, $jsonOutput, $prettyJsonOutput); $this->queueDevToolsCommand(['funding', ...$modeArguments], false, $jsonOutput, $prettyJsonOutput); diff --git a/src/Console/Command/Traits/LogsCommandResults.php b/src/Console/Command/Traits/LogsCommandResults.php index 4907e9cce1..4b99172bed 100644 --- a/src/Console/Command/Traits/LogsCommandResults.php +++ b/src/Console/Command/Traits/LogsCommandResults.php @@ -34,6 +34,28 @@ trait LogsCommandResults { use HasCommandLogger; + /** + * Logs a non-terminal informational message unless structured JSON output is active. + * + * @param string $message the progress message + * @param InputInterface $input the originating command input + * @param array $context optional extra log context + * + * @return void + */ + private function intermediateInfo(string $message, InputInterface $input, array $context = []): void + { + if (method_exists($this, 'isJsonOutput') && $this->isJsonOutput($input)) { + return; + } + + $this->getLogger() + ->info($message, [ + 'input' => $input, + ...$context, + ]); + } + /** * Logs an informational command message at notice level. * diff --git a/src/Console/Command/WikiCommand.php b/src/Console/Command/WikiCommand.php index 5fdf979292..323c71efbc 100644 --- a/src/Console/Command/WikiCommand.php +++ b/src/Console/Command/WikiCommand.php @@ -140,11 +140,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return $this->initializeWikiSubmodule($input, $target, $processOutput); } - if (! $jsonOutput) { - $this->logger->info('Generating wiki documentation...', [ - 'input' => $input, - ]); - } + $this->intermediateInfo('Generating wiki documentation...', $input); $projectCapabilities = $this->projectCapabilitiesResolver->resolve(wikiTarget: $target); diff --git a/tests/Console/Command/CodeStyleCommandTest.php b/tests/Console/Command/CodeStyleCommandTest.php index 161365bdba..493530343b 100644 --- a/tests/Console/Command/CodeStyleCommandTest.php +++ b/tests/Console/Command/CodeStyleCommandTest.php @@ -122,7 +122,9 @@ public function executeWillReturnSuccessWhenProcessQueueSucceeds(): void $this->processQueue->run(Argument::type('object')) ->willReturn(CodeStyleCommand::SUCCESS) ->shouldBeCalled(); - $this->logger->info('Running code style checks and fixes...') + $this->logger->info('Running code style checks and fixes...', Argument::that( + fn(array $context): bool => $this->input->reveal() === $context['input'] + )) ->shouldBeCalled(); $this->logger->log( 'info', @@ -147,7 +149,9 @@ public function executeWillReturnFailureWhenProcessQueueFails(): void $this->processQueue->run(Argument::type('object')) ->willReturn(CodeStyleCommand::FAILURE) ->shouldBeCalled(); - $this->logger->info('Running code style checks and fixes...') + $this->logger->info('Running code style checks and fixes...', Argument::that( + fn(array $context): bool => $this->input->reveal() === $context['input'] + )) ->shouldBeCalled(); $this->logger->error( 'Code style checks failed.', @@ -180,8 +184,8 @@ public function executeWillCaptureBufferedOutputWhenJsonIsRequested(): void $this->processQueue->run(Argument::type('object')) ->willReturn(CodeStyleCommand::SUCCESS) ->shouldBeCalled(); - $this->logger->info('Running code style checks and fixes...') - ->shouldBeCalled(); + $this->logger->info(Argument::cetera()) + ->shouldNotBeCalled(); $this->logger->log( 'info', 'Code style checks completed successfully.', diff --git a/tests/Console/Command/DependenciesCommandTest.php b/tests/Console/Command/DependenciesCommandTest.php index f8701a2d96..cc8e2351e7 100644 --- a/tests/Console/Command/DependenciesCommandTest.php +++ b/tests/Console/Command/DependenciesCommandTest.php @@ -174,6 +174,32 @@ public function executeWillIgnoreJackFailuresWhenMaxOutdatedIsDisabled(): void self::assertSame(DependenciesCommand::SUCCESS, $this->executeCommand()); } + /** + * @return void + */ + #[Test] + public function executeWillSuppressProgressLogWhenJsonIsRequested(): void + { + $this->input->getOption('json') + ->willReturn(true); + $this->input->getOption('pretty-json') + ->willReturn(false); + $this->processQueue->add(Argument::type(Process::class), false, Argument::cetera())->shouldBeCalledTimes(4); + $this->processQueue->run(Argument::type(OutputInterface::class)) + ->willReturn(DependenciesCommand::SUCCESS) + ->shouldBeCalledOnce(); + $this->logger->info(Argument::cetera()) + ->shouldNotBeCalled(); + $this->logger->log( + 'info', + 'Dependency analysis completed successfully.', + Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface + && $context['output'] instanceof OutputInterface), + )->shouldBeCalledOnce(); + + self::assertSame(DependenciesCommand::SUCCESS, $this->executeCommand()); + } + /** * @return void */ diff --git a/tests/Console/Command/DocsCommandTest.php b/tests/Console/Command/DocsCommandTest.php index 4684f6e031..8a886067fc 100644 --- a/tests/Console/Command/DocsCommandTest.php +++ b/tests/Console/Command/DocsCommandTest.php @@ -286,6 +286,38 @@ public function executeWillReturnSuccessWhenProcessQueueSucceeds(): void self::assertSame(DocsCommand::SUCCESS, $this->executeCommand()); } + /** + * @return void + */ + #[Test] + public function executeWillSuppressProgressLogWhenJsonIsRequested(): void + { + $this->input->getOption('json') + ->willReturn(true); + $this->input->getOption('pretty-json') + ->willReturn(false); + $this->filesystem->dumpFile('phpdocumentor.xml', '', '/repo/.dev-tools/cache/phpdoc') + ->shouldBeCalled(); + $this->processBuilder->withArgument('--cache-folder', '/repo/.dev-tools/cache/phpdoc') + ->willReturn($this->processBuilder->reveal()) + ->shouldBeCalled(); + $this->processQueue->add($this->process->reveal(), Argument::cetera()) + ->shouldBeCalled(); + $this->processQueue->run(Argument::type(OutputInterface::class)) + ->willReturn(DocsCommand::SUCCESS) + ->shouldBeCalled(); + $this->logger->info(Argument::cetera()) + ->shouldNotBeCalled(); + $this->logger->log( + 'info', + 'API documentation generated successfully.', + Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface + && $context['output'] instanceof OutputInterface), + )->shouldBeCalled(); + + self::assertSame(DocsCommand::SUCCESS, $this->executeCommand()); + } + /** * @return void */ diff --git a/tests/Console/Command/MetricsCommandTest.php b/tests/Console/Command/MetricsCommandTest.php index dbd2d84255..c15196a83c 100644 --- a/tests/Console/Command/MetricsCommandTest.php +++ b/tests/Console/Command/MetricsCommandTest.php @@ -186,6 +186,14 @@ public function executeWillRunPhpMetricsInQuietModeWhenJsonIsRequested(): void $this->processQueue->run(Argument::type(OutputInterface::class)) ->willReturn(MetricsCommand::SUCCESS) ->shouldBeCalled(); + $this->logger->info(Argument::cetera()) + ->shouldNotBeCalled(); + $this->logger->log( + 'info', + 'Code metrics analysis completed successfully.', + Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface + && $context['output'] instanceof OutputInterface), + )->shouldBeCalled(); self::assertSame(MetricsCommand::SUCCESS, $this->executeCommand()); } diff --git a/tests/Console/Command/PhpDocCommandTest.php b/tests/Console/Command/PhpDocCommandTest.php index aaebc09de1..cb6c37ad34 100644 --- a/tests/Console/Command/PhpDocCommandTest.php +++ b/tests/Console/Command/PhpDocCommandTest.php @@ -201,7 +201,9 @@ public function executeWillCreateDocHeaderAndRunPhpDocProcesses(): void static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); - $this->logger->info('Created .docheader from repository template.') + $this->logger->info('Created .docheader from repository template.', Argument::that( + static fn(array $context): bool => $context['input'] instanceof InputInterface + )) ->shouldBeCalled(); $this->logger->log( 'info', @@ -289,6 +291,14 @@ public function executeWillRequestStructuredOutputAndDisableProgressWhenJsonIsRe $this->processQueue->run(Argument::type(OutputInterface::class)) ->willReturn(PhpDocCommand::SUCCESS) ->shouldBeCalled(); + $this->logger->info(Argument::cetera()) + ->shouldNotBeCalled(); + $this->logger->log( + 'info', + 'PHPDoc checks completed successfully.', + Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface + && $context['output'] instanceof OutputInterface), + )->shouldBeCalled(); self::assertSame(PhpDocCommand::SUCCESS, $this->invokeExecute()); } diff --git a/tests/Console/Command/RefactorCommandTest.php b/tests/Console/Command/RefactorCommandTest.php index a8ea0934b0..5c26678bc5 100644 --- a/tests/Console/Command/RefactorCommandTest.php +++ b/tests/Console/Command/RefactorCommandTest.php @@ -174,6 +174,14 @@ public function executeWillRequestJsonOutputAndDisableProgressWhenJsonIsRequeste $this->processQueue->run(Argument::type(OutputInterface::class)) ->willReturn(RefactorCommand::SUCCESS) ->shouldBeCalled(); + $this->logger->info(Argument::cetera()) + ->shouldNotBeCalled(); + $this->logger->log( + 'info', + 'Code refactoring checks completed successfully.', + Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface + && $context['output'] instanceof OutputInterface), + )->shouldBeCalled(); self::assertSame(RefactorCommand::SUCCESS, $this->executeCommand()); } diff --git a/tests/Console/Command/ReportsCommandTest.php b/tests/Console/Command/ReportsCommandTest.php index 6f09cbedf3..e051bae96f 100644 --- a/tests/Console/Command/ReportsCommandTest.php +++ b/tests/Console/Command/ReportsCommandTest.php @@ -152,10 +152,8 @@ public function executeWillCaptureBufferedOutputWhenJsonIsRequested(): void $this->processQueue->run(Argument::type('object')) ->willReturn(ReportsCommand::FAILURE) ->shouldBeCalledOnce(); - $this->logger->info('Generating frontpage for Fast Forward documentation...', Argument::that( - static fn(array $context): bool => $context['input'] instanceof InputInterface - )) - ->shouldBeCalled(); + $this->logger->info(Argument::cetera()) + ->shouldNotBeCalled(); $this->logger->error( 'Documentation reports generation failed.', Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface diff --git a/tests/Console/Command/StandardsCommandTest.php b/tests/Console/Command/StandardsCommandTest.php index 9ebab4d62b..a7f9c36a02 100644 --- a/tests/Console/Command/StandardsCommandTest.php +++ b/tests/Console/Command/StandardsCommandTest.php @@ -177,6 +177,37 @@ public function executeWithCacheWillForwardCacheOnlyToCacheAwareNestedCommands() self::assertSame(StandardsCommand::SUCCESS, $this->invokeExecute()); } + /** + * @return void + */ + #[Test] + public function executeWillSuppressProgressLogWhenJsonIsRequested(): void + { + $this->input->getOption('json') + ->willReturn(true); + $this->input->getOption('pretty-json') + ->willReturn(false); + $this->processBuilder->withArgument('--json') + ->willReturn($this->processBuilder->reveal()) + ->shouldBeCalledTimes(4); + $this->processQueue->add(Argument::type(Process::class), Argument::cetera()) + ->shouldBeCalledTimes(4); + $this->processQueue->run(Argument::type(OutputInterface::class)) + ->willReturn(StandardsCommand::SUCCESS) + ->shouldBeCalledOnce(); + $this->logger->info(Argument::cetera()) + ->shouldNotBeCalled(); + $this->logger->log( + 'info', + 'Code standards checks completed successfully.', + Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface + && $context['output'] instanceof OutputInterface + && ['refactor', 'phpdoc', 'code-style', 'reports'] === $context['commands']), + )->shouldBeCalled(); + + self::assertSame(StandardsCommand::SUCCESS, $this->invokeExecute()); + } + /** * @return void */ diff --git a/tests/Console/Command/SyncCommandTest.php b/tests/Console/Command/SyncCommandTest.php index a7f695b537..49d1f303e4 100644 --- a/tests/Console/Command/SyncCommandTest.php +++ b/tests/Console/Command/SyncCommandTest.php @@ -164,10 +164,8 @@ public function executeWillPropagateJsonOutputFormatToSubCommands(): void $this->processQueue->run(Argument::type('object')) ->willReturn(SyncCommand::SUCCESS) ->shouldBeCalledOnce(); - $this->logger->info('Starting dev-tools synchronization...', Argument::that( - static fn(array $context): bool => $context['input'] instanceof InputInterface - )) - ->shouldBeCalled(); + $this->logger->info(Argument::cetera()) + ->shouldNotBeCalled(); $this->logger->log( 'info', 'Dev-tools synchronization completed successfully.', diff --git a/tests/Console/Command/WikiCommandTest.php b/tests/Console/Command/WikiCommandTest.php index 69c287cfb9..dd17257a5b 100644 --- a/tests/Console/Command/WikiCommandTest.php +++ b/tests/Console/Command/WikiCommandTest.php @@ -179,6 +179,51 @@ public function executeWillReturnSuccessWhenProcessQueueSucceeds(): void self::assertSame(WikiCommand::SUCCESS, $this->executeCommand()); } + /** + * @return void + */ + #[Test] + public function executeWillSuppressProgressLogWhenJsonIsRequested(): void + { + $this->input->getOption('json') + ->willReturn(true); + $this->input->getOption('pretty-json') + ->willReturn(false); + $this->processBuilder->withArgument( + '--template', + DevToolsPathResolver::getPreferredVendorPath('vendor/saggre/phpdocumentor-markdown/themes/markdown') + ) + ->willReturn($this->processBuilder->reveal()) + ->shouldBeCalled(); + $this->processBuilder->withArgument( + '--cache-folder', + ManagedWorkspace::getCacheDirectory(ManagedWorkspace::PHPDOC) + ) + ->willReturn($this->processBuilder->reveal()) + ->shouldBeCalled(); + $this->filesystem->getAbsolutePath('src/') + ->willReturn(getcwd() . '/src') + ->shouldBeCalled(); + $this->processBuilder->withArgument('--directory', getcwd() . '/src') + ->willReturn($this->processBuilder->reveal()) + ->shouldBeCalled(); + $this->processQueue->add($this->process->reveal(), Argument::cetera()) + ->shouldBeCalled(); + $this->processQueue->run(Argument::type(OutputInterface::class)) + ->willReturn(WikiCommand::SUCCESS) + ->shouldBeCalled(); + $this->logger->info(Argument::cetera()) + ->shouldNotBeCalled(); + $this->logger->log( + 'info', + 'Wiki documentation generated successfully.', + Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface + && $context['output'] instanceof OutputInterface), + )->shouldBeCalled(); + + self::assertSame(WikiCommand::SUCCESS, $this->executeCommand()); + } + /** * @return void */ From 5703f3cdbd979d1b44c6804559005944f8eec1f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 17:53:56 -0300 Subject: [PATCH 08/35] [console] Normalize Rector changed file lists --- .../Processor/CommandOutputProcessor.php | 52 ++++++++++++++++- .../Processor/CommandOutputProcessorTest.php | 56 +++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/src/Console/Logger/Processor/CommandOutputProcessor.php b/src/Console/Logger/Processor/CommandOutputProcessor.php index 0948fdd79d..feea4ce291 100644 --- a/src/Console/Logger/Processor/CommandOutputProcessor.php +++ b/src/Console/Logger/Processor/CommandOutputProcessor.php @@ -98,7 +98,7 @@ private function decodeStructuredOutput(string $content): mixed } try { - return json_decode($trimmedContent, true); + return $this->normalizeStructuredPayload(json_decode($trimmedContent, true)); } catch (JsonException) { } @@ -140,7 +140,7 @@ private function decodeJsonDocumentStream(string $content): ?array } try { - $decodedDocuments[] = json_decode($document, true); + $decodedDocuments[] = $this->normalizeStructuredPayload(json_decode($document, true)); } catch (JsonException) { return null; } @@ -219,4 +219,52 @@ private function consumeJsonDocument(string $content, int &$offset): ?string return null; } + + /** + * Normalizes decoded structured payloads produced by wrapped tooling. + * + * @param mixed $payload the decoded payload + * + * @return mixed the normalized payload + */ + private function normalizeStructuredPayload(mixed $payload): mixed + { + if (! \is_array($payload)) { + return $payload; + } + + if (! isset($payload['totals']) || ! \is_array($payload['totals'])) { + return $payload; + } + + $changedFilesTotal = $payload['totals']['changed_files'] ?? null; + + if (! \is_int($changedFilesTotal)) { + return $payload; + } + + if (0 === $changedFilesTotal) { + $payload['changed_files'] = []; + + return $payload; + } + + if (! isset($payload['file_diffs']) || ! \is_array($payload['file_diffs'])) { + return $payload; + } + + $changedFiles = []; + + foreach ($payload['file_diffs'] as $fileDiff) { + if (! \is_array($fileDiff) || ! isset($fileDiff['file']) || ! \is_string($fileDiff['file'])) { + continue; + } + + $changedFiles[$fileDiff['file']] = $fileDiff['file']; + } + + $payload['changed_files'] = array_values($changedFiles); + + return $payload; + } } diff --git a/tests/Console/Logger/Processor/CommandOutputProcessorTest.php b/tests/Console/Logger/Processor/CommandOutputProcessorTest.php index 8ceb420538..af8dbc9800 100644 --- a/tests/Console/Logger/Processor/CommandOutputProcessorTest.php +++ b/tests/Console/Logger/Processor/CommandOutputProcessorTest.php @@ -71,6 +71,62 @@ public function processWillDecodeSingleJsonBufferedOutput(): void ], $context['output']); } + /** + * @return void + */ + #[Test] + public function processWillNormalizeRectorChangedFilesWhenNoFilesActuallyChanged(): void + { + $processor = new CommandOutputProcessor(); + $output = new BufferedOutput(); + $output->write( + "{\"totals\":{\"changed_files\":0,\"errors\":0},\"changed_files\":[\"src/Foo.php\",\"src/Bar.php\"]}\n" + ); + + $context = $processor->process([ + 'output' => $output, + ]); + + self::assertSame([ + 'totals' => [ + 'changed_files' => 0, + 'errors' => 0, + ], + 'changed_files' => [], + ], $context['output']); + } + + /** + * @return void + */ + #[Test] + public function processWillNormalizeRectorChangedFilesUsingOnlyDiffEntries(): void + { + $processor = new CommandOutputProcessor(); + $output = new BufferedOutput(); + $output->write( + "{\"totals\":{\"changed_files\":1,\"errors\":0},\"changed_files\":[\"src/Foo.php\",\"src/Bar.php\"],\"file_diffs\":[{\"file\":\"src/Bar.php\",\"diff\":\"@@\"}]}\n" + ); + + $context = $processor->process([ + 'output' => $output, + ]); + + self::assertSame([ + 'totals' => [ + 'changed_files' => 1, + 'errors' => 0, + ], + 'changed_files' => ['src/Bar.php'], + 'file_diffs' => [ + [ + 'file' => 'src/Bar.php', + 'diff' => '@@', + ], + ], + ], $context['output']); + } + /** * @return void */ From ba1afb2b5e20dedf72204746fb03f1571aadbaff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 18:00:00 -0300 Subject: [PATCH 09/35] [bootstrap] Normalize CLI autoload bootstrap --- bin/dev-tools.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/bin/dev-tools.php b/bin/dev-tools.php index d3eba1dae7..62e8fd254b 100644 --- a/bin/dev-tools.php +++ b/bin/dev-tools.php @@ -21,9 +21,16 @@ use FastForward\DevTools\Console\DevTools; -$projectVendorAutoload = \dirname(__DIR__, 4) . '/vendor/autoload.php'; -$pluginVendorAutoload = \dirname(__DIR__) . '/vendor/autoload.php'; +$autoloadCandidates = [\dirname(__DIR__) . '/vendor/autoload.php', \dirname(__DIR__, 4) . '/vendor/autoload.php']; -require_once file_exists($projectVendorAutoload) ? $projectVendorAutoload : $pluginVendorAutoload; +foreach ($autoloadCandidates as $autoloadCandidate) { + if (is_file($autoloadCandidate)) { + require_once $autoloadCandidate; -DevTools::create()->run(); + exit(DevTools::create()->run()); + } +} + +fprintf(\STDERR, "Could not locate Composer autoload.php for fast-forward/dev-tools.\n"); + +exit(1); From 4a228a21bd136d6d02b42497bb44b8e140122d2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 18:04:12 -0300 Subject: [PATCH 10/35] [console] Ignore warning preambles before structured JSON --- .../Processor/CommandOutputProcessor.php | 67 +++++++++++++++++++ .../Processor/CommandOutputProcessorTest.php | 32 +++++++++ 2 files changed, 99 insertions(+) diff --git a/src/Console/Logger/Processor/CommandOutputProcessor.php b/src/Console/Logger/Processor/CommandOutputProcessor.php index feea4ce291..505a586e5a 100644 --- a/src/Console/Logger/Processor/CommandOutputProcessor.php +++ b/src/Console/Logger/Processor/CommandOutputProcessor.php @@ -108,9 +108,55 @@ private function decodeStructuredOutput(string $content): mixed return $decodedDocuments; } + $decodedStructuredOutput = $this->decodeStructuredOutputAfterTextPreamble($content); + + if (null !== $decodedStructuredOutput) { + return $decodedStructuredOutput; + } + return $content; } + /** + * Decodes structured output that is preceded by plain-text warnings or banners. + * + * Some tooling emits advisory text before a valid JSON payload even when a + * machine-readable format is requested. When the suffix starting at the + * first valid JSON token is fully decodable, the textual preamble SHALL be + * ignored so parent command envelopes remain parseable. + * + * @param string $content the buffered output contents + * + * @return mixed the decoded JSON payload when a valid structured suffix exists + */ + private function decodeStructuredOutputAfterTextPreamble(string $content): mixed + { + $offset = 0; + + while (null !== ($offset = $this->findNextJsonDocumentOffset($content, $offset))) { + $structuredSuffix = trim(substr($content, $offset)); + + if ('' === $structuredSuffix) { + return null; + } + + try { + return $this->normalizeStructuredPayload(json_decode($structuredSuffix, true)); + } catch (JsonException) { + } + + $decodedDocuments = $this->decodeJsonDocumentStream($structuredSuffix); + + if (null !== $decodedDocuments) { + return $decodedDocuments; + } + + ++$offset; + } + + return null; + } + /** * Decodes a stream that contains multiple JSON documents separated by whitespace. * @@ -220,6 +266,27 @@ private function consumeJsonDocument(string $content, int &$offset): ?string return null; } + /** + * Finds the offset of the next possible JSON document opening token. + * + * @param string $content the buffered output contents + * @param int $offset the offset from which scanning SHALL start + * + * @return ?int the offset of the next "{" or "[" token + */ + private function findNextJsonDocumentOffset(string $content, int $offset): ?int + { + $length = \strlen($content); + + for (; $offset < $length; ++$offset) { + if ('{' === $content[$offset] || '[' === $content[$offset]) { + return $offset; + } + } + + return null; + } + /** * Normalizes decoded structured payloads produced by wrapped tooling. * diff --git a/tests/Console/Logger/Processor/CommandOutputProcessorTest.php b/tests/Console/Logger/Processor/CommandOutputProcessorTest.php index af8dbc9800..9f88b38dfc 100644 --- a/tests/Console/Logger/Processor/CommandOutputProcessorTest.php +++ b/tests/Console/Logger/Processor/CommandOutputProcessorTest.php @@ -158,6 +158,38 @@ public function processWillDecodeMultipleJsonBufferedOutputsIntoAList(): void ], $context['output']); } + /** + * @return void + */ + #[Test] + public function processWillDiscardPlainTextPreambleBeforeStructuredJsonOutput(): void + { + $processor = new CommandOutputProcessor(); + $output = new BufferedOutput(); + $output->write( + "Warning: advisory text before JSON.\n" + . "{\"about\":\"PHP CS Fixer\"}\n" + . "{\"totals\":{\"changed_files\":0,\"errors\":0},\"changed_files\":[\"src/Foo.php\"]}\n" + ); + + $context = $processor->process([ + 'output' => $output, + ]); + + self::assertSame([ + [ + 'about' => 'PHP CS Fixer', + ], + [ + 'totals' => [ + 'changed_files' => 0, + 'errors' => 0, + ], + 'changed_files' => [], + ], + ], $context['output']); + } + /** * @return void */ From a40c792eb6863fb97a2da00905a9002bde8c356d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 18:14:59 -0300 Subject: [PATCH 11/35] [console] Centralize implicit JSON output detection --- src/Console/Command/TestsCommand.php | 6 +- src/Console/Input/HasJsonOption.php | 91 +++++++++++++- tests/Console/Input/HasJsonOptionTest.php | 139 ++++++++++++++++++++++ 3 files changed, 230 insertions(+), 6 deletions(-) create mode 100644 tests/Console/Input/HasJsonOptionTest.php diff --git a/src/Console/Command/TestsCommand.php b/src/Console/Command/TestsCommand.php index 66bb0b72ae..6c067709f6 100644 --- a/src/Console/Command/TestsCommand.php +++ b/src/Console/Command/TestsCommand.php @@ -173,11 +173,7 @@ protected function configure(): void */ protected function execute(InputInterface $input, OutputInterface $output): int { - $explicitJsonOutput = (bool) $input->getOption('json'); - $prettyJsonOutput = $this->isPrettyJsonOutput($input); - $structuredOutput = $prettyJsonOutput - || $explicitJsonOutput - || ($this->runtimeEnvironment->isAgentPresent() && ! $this->runtimeEnvironment->isComposerTestRun()); + $structuredOutput = $this->isJsonOutput($input); $processOutput = $structuredOutput ? new BufferedOutput() : $output; $cacheEnabled = $this->isCacheEnabled($input); diff --git a/src/Console/Input/HasJsonOption.php b/src/Console/Input/HasJsonOption.php index 40ef3fafd6..37b1c5eae4 100644 --- a/src/Console/Input/HasJsonOption.php +++ b/src/Console/Input/HasJsonOption.php @@ -19,6 +19,8 @@ namespace FastForward\DevTools\Console\Input; +use Ergebnis\AgentDetector\Detector; +use FastForward\DevTools\Environment\RuntimeEnvironmentInterface; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -56,7 +58,11 @@ protected function isJsonOutput(InputInterface $input): bool return true; } - return (bool) $input->getOption('json'); + if ((bool) $input->getOption('json')) { + return true; + } + + return $this->isImplicitJsonOutputEnabled(); } /** @@ -68,4 +74,87 @@ protected function isPrettyJsonOutput(InputInterface $input): bool { return (bool) $input->getOption('pretty-json'); } + + /** + * Determines whether structured JSON output SHOULD be enabled implicitly. + * + * Commands MAY opt into runtime-environment-aware behavior by exposing a + * `$runtimeEnvironment` property. Commands that do not expose it SHALL fall + * back to lightweight agent detection based on process environment + * variables, except while the PHPUnit test runtime is active. + */ + private function isImplicitJsonOutputEnabled(): bool + { + $runtimeEnvironment = $this->resolveRuntimeEnvironment(); + + if ($runtimeEnvironment instanceof RuntimeEnvironmentInterface) { + return $runtimeEnvironment->isAgentPresent() && ! $runtimeEnvironment->isComposerTestRun(); + } + + if ($this->isPhpUnitRuntime() || $this->isComposerTestRunEnvironmentEnabled()) { + return false; + } + + return (new Detector())->isAgentPresent($this->resolveEnvironmentVariables()); + } + + /** + * @return ?RuntimeEnvironmentInterface + */ + private function resolveRuntimeEnvironment(): ?RuntimeEnvironmentInterface + { + if (! property_exists($this, 'runtimeEnvironment')) { + return null; + } + + if (! $this->runtimeEnvironment instanceof RuntimeEnvironmentInterface) { + return null; + } + + return $this->runtimeEnvironment; + } + + /** + * Returns whether the current process is executing inside PHPUnit. + */ + private function isPhpUnitRuntime(): bool + { + return \defined('PHPUNIT_COMPOSER_INSTALL'); + } + + /** + * Returns whether the Composer test runtime flag is enabled. + */ + private function isComposerTestRunEnvironmentEnabled(): bool + { + $value = $_SERVER['COMPOSER_TESTS_ARE_RUNNING'] ?? getenv('COMPOSER_TESTS_ARE_RUNNING'); + + if (false === $value || null === $value) { + return false; + } + + return \in_array(strtolower((string) $value), ['1', 'true', 'yes', 'on'], true); + } + + /** + * Returns environment variables suitable for lightweight agent detection. + * + * @return array + */ + private function resolveEnvironmentVariables(): array + { + $environmentVariables = []; + + foreach ([$_SERVER, $_ENV] as $environment) { + foreach ($environment as $name => $value) { + if (! \is_string($name) || ! \is_string($value)) { + continue; + } + + $environmentVariables[$name] ??= $value; + } + } + + return $environmentVariables; + } } diff --git a/tests/Console/Input/HasJsonOptionTest.php b/tests/Console/Input/HasJsonOptionTest.php new file mode 100644 index 0000000000..72641e2b9d --- /dev/null +++ b/tests/Console/Input/HasJsonOptionTest.php @@ -0,0 +1,139 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/ + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward/dev-tools/issues + * @see https://php-fast-forward.github.io/dev-tools/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Tests\Console\Input; + +use FastForward\DevTools\Console\Input\HasJsonOption; +use FastForward\DevTools\Environment\RuntimeEnvironmentInterface; +use PHPUnit\Framework\Attributes\CoversTrait; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; +use Symfony\Component\Console\Input\InputInterface; + +use function Safe\putenv; + +#[CoversTrait(HasJsonOption::class)] +final class HasJsonOptionTest extends TestCase +{ + use ProphecyTrait; + + /** + * @var array + */ + private array $server; + + /** + * @var array + */ + private array $environment; + + private string|false $composerTestsAreRunning; + + /** + * @return void + */ + protected function setUp(): void + { + $this->server = $_SERVER; + $this->environment = $_ENV; + $this->composerTestsAreRunning = getenv('COMPOSER_TESTS_ARE_RUNNING'); + + $_SERVER = []; + $_ENV = []; + putenv('COMPOSER_TESTS_ARE_RUNNING'); + } + + /** + * @return void + */ + protected function tearDown(): void + { + $_SERVER = $this->server; + $_ENV = $this->environment; + + if (false === $this->composerTestsAreRunning) { + putenv('COMPOSER_TESTS_ARE_RUNNING'); + + return; + } + + putenv('COMPOSER_TESTS_ARE_RUNNING=' . $this->composerTestsAreRunning); + } + + /** + * @return void + */ + #[Test] + public function isJsonOutputWillUseRuntimeEnvironmentWhenAvailable(): void + { + $runtimeEnvironment = $this->prophesize(RuntimeEnvironmentInterface::class); + $runtimeEnvironment->isAgentPresent() + ->willReturn(true); + $runtimeEnvironment->isComposerTestRun() + ->willReturn(false); + + $input = $this->prophesize(InputInterface::class); + $input->getOption('pretty-json') + ->willReturn(false); + $input->getOption('json') + ->willReturn(false); + + $command = new class ($runtimeEnvironment->reveal()) { + use HasJsonOption; + + public function __construct( + private readonly RuntimeEnvironmentInterface $runtimeEnvironment, + ) {} + + public function isStructured(InputInterface $input): bool + { + return $this->isJsonOutput($input); + } + }; + + self::assertTrue($command->isStructured($input->reveal())); + } + + /** + * @return void + */ + #[Test] + public function isJsonOutputWillIgnoreFallbackAgentDetectionDuringPhpUnitRuns(): void + { + $_SERVER['CODEX_CI'] = '1'; + + $input = $this->prophesize(InputInterface::class); + $input->getOption('pretty-json') + ->willReturn(false); + $input->getOption('json') + ->willReturn(false); + + $command = new class { + use HasJsonOption; + + public function isStructured(InputInterface $input): bool + { + return $this->isJsonOutput($input); + } + }; + + self::assertFalse($command->isStructured($input->reveal())); + } +} From 28aba82952ecee99e66b457e4df635446c8dff7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 18:34:21 -0300 Subject: [PATCH 12/35] [container] Centralize trait-only service resolution --- src/Console/Command/TestsCommand.php | 6 - .../Command/Traits/HasCommandLogger.php | 23 +- src/Console/DevTools.php | 17 +- src/Console/Input/HasJsonOption.php | 57 +---- src/Container/ContainerFactory.php | 99 +++++++ .../DevToolsServiceProvider.php | 242 ++++++++++++++++++ src/Environment/RuntimeEnvironment.php | 6 +- .../RuntimeEnvironmentInterface.php | 2 +- .../DevToolsServiceProvider.php | 224 +--------------- .../DevToolsCommandProviderTest.php | 110 +++++--- tests/Console/Command/TestsCommandTest.php | 15 +- tests/Console/DevToolsTest.php | 10 +- tests/Console/Input/HasJsonOptionTest.php | 18 ++ tests/Container/ContainerFactoryTest.php | 92 +++++++ tests/Environment/RuntimeEnvironmentTest.php | 9 + .../DevToolsServiceProviderTest.php | 2 +- 16 files changed, 585 insertions(+), 347 deletions(-) create mode 100644 src/Container/ContainerFactory.php create mode 100644 src/Container/ServiceProvider/DevToolsServiceProvider.php create mode 100644 tests/Container/ContainerFactoryTest.php diff --git a/src/Console/Command/TestsCommand.php b/src/Console/Command/TestsCommand.php index 6c067709f6..467a866745 100644 --- a/src/Console/Command/TestsCommand.php +++ b/src/Console/Command/TestsCommand.php @@ -24,7 +24,6 @@ use FastForward\DevTools\Console\Input\HasCacheOption; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Composer\Json\ComposerJsonInterface; -use FastForward\DevTools\Environment\RuntimeEnvironmentInterface; use FastForward\DevTools\Filesystem\FilesystemInterface; use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\PhpUnit\Bootstrap\BootstrapShimGenerator; @@ -34,7 +33,6 @@ use FastForward\DevTools\Path\ManagedWorkspace; use FastForward\DevTools\Project\ProjectCapabilitiesResolverInterface; use InvalidArgumentException; -use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use RuntimeException; use Symfony\Component\Config\FileLocatorInterface; @@ -82,8 +80,6 @@ final class TestsCommand extends Command * @param ProcessBuilderInterface $processBuilder the builder used to assemble the PHPUnit process * @param ProcessQueueInterface $processQueue the queue used to execute PHPUnit * @param ProjectCapabilitiesResolverInterface $projectCapabilitiesResolver the project capability resolver - * @param RuntimeEnvironmentInterface $runtimeEnvironment the runtime environment capability resolver - * @param LoggerInterface $logger the output-aware logger */ public function __construct( private readonly CoverageSummaryLoaderInterface $coverageSummaryLoader, @@ -94,8 +90,6 @@ public function __construct( private readonly ProcessBuilderInterface $processBuilder, private readonly ProcessQueueInterface $processQueue, private readonly ProjectCapabilitiesResolverInterface $projectCapabilitiesResolver, - private readonly RuntimeEnvironmentInterface $runtimeEnvironment, - private readonly LoggerInterface $logger, ) { parent::__construct(); } diff --git a/src/Console/Command/Traits/HasCommandLogger.php b/src/Console/Command/Traits/HasCommandLogger.php index e4e21c8bbd..058f189576 100644 --- a/src/Console/Command/Traits/HasCommandLogger.php +++ b/src/Console/Command/Traits/HasCommandLogger.php @@ -19,15 +19,16 @@ namespace FastForward\DevTools\Console\Command\Traits; +use FastForward\DevTools\Container\ContainerFactory; use LogicException; use Psr\Log\LoggerInterface; /** * Resolves the logger expected by command result helper traits. * - * The consuming command is expected to expose an initialized `$logger` - * property so reusable traits can log without coupling themselves to a - * specific constructor signature. + * The consuming command MAY expose an initialized `$logger` property. When it + * does not, the trait SHALL resolve the shared logger from the DevTools + * container so reusable traits can stay decoupled from constructor wiring. */ trait HasCommandLogger { @@ -38,22 +39,28 @@ trait HasCommandLogger */ public function getLogger(): LoggerInterface { - if (! property_exists($this, 'logger') || null === $this->logger) { + if (property_exists($this, 'logger') && $this->logger instanceof LoggerInterface) { + return $this->logger; + } + + if (property_exists($this, 'logger') && null !== $this->logger) { throw new LogicException(\sprintf( - 'Commands using %s MUST expose an initialized $logger property with an instance of %s.', + 'Commands using %s MUST expose a %s instance on the $logger property.', LogsCommandResults::class, LoggerInterface::class, )); } - if (! $this->logger instanceof LoggerInterface) { + $logger = ContainerFactory::get(LoggerInterface::class); + + if (! $logger instanceof LoggerInterface) { throw new LogicException(\sprintf( - 'Commands using %s MUST expose a %s instance on the $logger property.', + 'Commands using %s MUST resolve a %s instance from the shared container.', LogsCommandResults::class, LoggerInterface::class, )); } - return $this->logger; + return $logger; } } diff --git a/src/Console/DevTools.php b/src/Console/DevTools.php index 42b0912fc5..70621bce3c 100644 --- a/src/Console/DevTools.php +++ b/src/Console/DevTools.php @@ -20,6 +20,7 @@ namespace FastForward\DevTools\Console; use FastForward\DevTools\Console\Command\SelfUpdateCommand; +use FastForward\DevTools\Container\ContainerFactory; use FastForward\DevTools\Environment\EnvironmentInterface; use FastForward\DevTools\Environment\RuntimeEnvironmentInterface; use FastForward\DevTools\Path\ManagedWorkspace; @@ -27,8 +28,6 @@ use FastForward\DevTools\SelfUpdate\SelfUpdateScopeResolverInterface; use FastForward\DevTools\SelfUpdate\VersionCheckNotifierInterface; use FastForward\DevTools\SelfUpdate\WorkingDirectorySwitcherInterface; -use FastForward\DevTools\ServiceProvider\DevToolsServiceProvider; -use DI\Container; use Override; use Psr\Container\ContainerInterface; use Symfony\Component\Console\Application; @@ -65,11 +64,6 @@ final class DevTools extends Application */ private const array RAW_OUTPUT_COMMANDS = ['changelog:next-version', 'changelog:show']; - /** - * @var ContainerInterface holds the static container instance for global access within the DevTools context - */ - private static ?ContainerInterface $container = null; - /** * Initializes the DevTools global context and dependency graph. * @@ -177,7 +171,7 @@ public function doRun(InputInterface $input, OutputInterface $output): int */ public static function create(): self { - return self::getContainer()->get(self::class); + return ContainerFactory::get(self::class); } /** @@ -185,12 +179,7 @@ public static function create(): self */ public static function getContainer(): ContainerInterface { - if (! self::$container instanceof ContainerInterface) { - $serviceProvider = new DevToolsServiceProvider(); - self::$container = new Container($serviceProvider->getFactories()); - } - - return self::$container; + return ContainerFactory::create(); } /** diff --git a/src/Console/Input/HasJsonOption.php b/src/Console/Input/HasJsonOption.php index 37b1c5eae4..3af859380c 100644 --- a/src/Console/Input/HasJsonOption.php +++ b/src/Console/Input/HasJsonOption.php @@ -19,7 +19,7 @@ namespace FastForward\DevTools\Console\Input; -use Ergebnis\AgentDetector\Detector; +use FastForward\DevTools\Container\ContainerFactory; use FastForward\DevTools\Environment\RuntimeEnvironmentInterface; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -80,22 +80,21 @@ protected function isPrettyJsonOutput(InputInterface $input): bool * * Commands MAY opt into runtime-environment-aware behavior by exposing a * `$runtimeEnvironment` property. Commands that do not expose it SHALL fall - * back to lightweight agent detection based on process environment - * variables, except while the PHPUnit test runtime is active. + * back to the shared runtime-environment service from the DevTools container. */ private function isImplicitJsonOutputEnabled(): bool { $runtimeEnvironment = $this->resolveRuntimeEnvironment(); - if ($runtimeEnvironment instanceof RuntimeEnvironmentInterface) { - return $runtimeEnvironment->isAgentPresent() && ! $runtimeEnvironment->isComposerTestRun(); + if (! $runtimeEnvironment instanceof RuntimeEnvironmentInterface) { + $runtimeEnvironment = ContainerFactory::get(RuntimeEnvironmentInterface::class); } - if ($this->isPhpUnitRuntime() || $this->isComposerTestRunEnvironmentEnabled()) { + if (! $runtimeEnvironment instanceof RuntimeEnvironmentInterface) { return false; } - return (new Detector())->isAgentPresent($this->resolveEnvironmentVariables()); + return $runtimeEnvironment->isAgentPresent() && ! $runtimeEnvironment->isComposerTestRun(); } /** @@ -113,48 +112,4 @@ private function resolveRuntimeEnvironment(): ?RuntimeEnvironmentInterface return $this->runtimeEnvironment; } - - /** - * Returns whether the current process is executing inside PHPUnit. - */ - private function isPhpUnitRuntime(): bool - { - return \defined('PHPUNIT_COMPOSER_INSTALL'); - } - - /** - * Returns whether the Composer test runtime flag is enabled. - */ - private function isComposerTestRunEnvironmentEnabled(): bool - { - $value = $_SERVER['COMPOSER_TESTS_ARE_RUNNING'] ?? getenv('COMPOSER_TESTS_ARE_RUNNING'); - - if (false === $value || null === $value) { - return false; - } - - return \in_array(strtolower((string) $value), ['1', 'true', 'yes', 'on'], true); - } - - /** - * Returns environment variables suitable for lightweight agent detection. - * - * @return array - */ - private function resolveEnvironmentVariables(): array - { - $environmentVariables = []; - - foreach ([$_SERVER, $_ENV] as $environment) { - foreach ($environment as $name => $value) { - if (! \is_string($name) || ! \is_string($value)) { - continue; - } - - $environmentVariables[$name] ??= $value; - } - } - - return $environmentVariables; - } } diff --git a/src/Container/ContainerFactory.php b/src/Container/ContainerFactory.php new file mode 100644 index 0000000000..eec78a5a7b --- /dev/null +++ b/src/Container/ContainerFactory.php @@ -0,0 +1,99 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/ + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward/dev-tools/issues + * @see https://php-fast-forward.github.io/dev-tools/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Container; + +use DI\Container; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use Psr\Container\ContainerInterface; + +/** + * Builds and caches the shared DevTools dependency injection container. + * + * The factory centralizes container bootstrapping so command traits and other + * internal helpers can resolve services without duplicating bootstrap logic or + * depending on the console application entrypoint. + */ +final class ContainerFactory +{ + private static ?Container $container = null; + + /** + * Creates or returns the shared DevTools container instance. + * + * @return ContainerInterface the shared container instance + */ + public static function create(): ContainerInterface + { + if (! self::$container instanceof Container) { + $serviceProvider = new DevToolsServiceProvider(); + self::$container = new Container($serviceProvider->getFactories()); + } + + return self::$container; + } + + /** + * Resolves a service from the shared DevTools container. + * + * @template T + * + * @param string|class-string $id the service identifier + * + * @return mixed|T the resolved service + */ + public static function get(string $id): mixed + { + return self::create()->get($id); + } + + /** + * Returns whether the shared DevTools container can resolve a service. + * + * @param string $id the service identifier + */ + public static function has(string $id): bool + { + return self::create()->has($id); + } + + /** + * Overrides a shared service entry for the current process. + * + * @internal this method exists so tests can replace container entries with doubles + * + * @param string $id the service identifier + * @param mixed $value the replacement service entry + */ + public static function set(string $id, mixed $value): void + { + self::create(); + self::$container?->set($id, $value); + } + + /** + * Resets the cached shared container instance. + * + * @internal this method exists so tests can isolate container state between test cases + */ + public static function reset(): void + { + self::$container = null; + } +} diff --git a/src/Container/ServiceProvider/DevToolsServiceProvider.php b/src/Container/ServiceProvider/DevToolsServiceProvider.php new file mode 100644 index 0000000000..4fc468c1f8 --- /dev/null +++ b/src/Container/ServiceProvider/DevToolsServiceProvider.php @@ -0,0 +1,242 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/ + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward/dev-tools/issues + * @see https://php-fast-forward.github.io/dev-tools/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Container\ServiceProvider; + +use Composer\Plugin\Capability\CommandProvider; +use FastForward\DevTools\Changelog\Checker\UnreleasedEntryChecker; +use FastForward\DevTools\Changelog\Checker\UnreleasedEntryCheckerInterface; +use FastForward\DevTools\Changelog\Manager\ChangelogManager; +use FastForward\DevTools\Changelog\Manager\ChangelogManagerInterface; +use FastForward\DevTools\Changelog\Parser\ChangelogParser; +use FastForward\DevTools\Changelog\Parser\ChangelogParserInterface; +use FastForward\DevTools\Changelog\Renderer\MarkdownRenderer; +use FastForward\DevTools\Changelog\Renderer\MarkdownRendererInterface; +use FastForward\DevTools\Composer\Capability\DevToolsCommandProvider; +use FastForward\DevTools\Composer\Json\ComposerJson; +use FastForward\DevTools\Composer\Json\ComposerJsonInterface; +use FastForward\DevTools\Console\CommandLoader\DevToolsCommandLoader; +use FastForward\DevTools\Console\Formatter\LogLevelOutputFormatter; +use FastForward\DevTools\Console\Logger\OutputFormatLogger; +use FastForward\DevTools\Console\Logger\Processor\CommandInputProcessor; +use FastForward\DevTools\Console\Logger\Processor\CommandOutputProcessor; +use FastForward\DevTools\Console\Logger\Processor\CompositeContextProcessor; +use FastForward\DevTools\Console\Logger\Processor\ContextProcessorInterface; +use FastForward\DevTools\Console\Output\GithubActionOutput; +use FastForward\DevTools\Console\Output\OutputCapabilityDetector; +use FastForward\DevTools\Console\Output\OutputCapabilityDetectorInterface; +use FastForward\DevTools\Environment\Environment; +use FastForward\DevTools\Environment\EnvironmentInterface; +use FastForward\DevTools\Environment\RuntimeEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironmentInterface; +use FastForward\DevTools\Filesystem\Filesystem; +use FastForward\DevTools\Filesystem\FilesystemInterface; +use FastForward\DevTools\Filesystem\FinderFactory; +use FastForward\DevTools\Filesystem\FinderFactoryInterface; +use FastForward\DevTools\Git\GitClient; +use FastForward\DevTools\Git\GitClientInterface; +use FastForward\DevTools\GitAttributes\CandidateProvider; +use FastForward\DevTools\GitAttributes\CandidateProviderInterface; +use FastForward\DevTools\GitAttributes\ExistenceChecker; +use FastForward\DevTools\GitAttributes\ExistenceCheckerInterface; +use FastForward\DevTools\GitAttributes\ExportIgnoreFilter; +use FastForward\DevTools\GitAttributes\ExportIgnoreFilterInterface; +use FastForward\DevTools\GitAttributes\Merger as GitAttributesMerger; +use FastForward\DevTools\GitAttributes\MergerInterface as GitAttributesMergerInterface; +use FastForward\DevTools\GitAttributes\Reader as GitAttributesReader; +use FastForward\DevTools\GitAttributes\ReaderInterface as GitAttributesReaderInterface; +use FastForward\DevTools\GitAttributes\Writer as GitAttributesWriter; +use FastForward\DevTools\GitAttributes\WriterInterface as GitAttributesWriterInterface; +use FastForward\DevTools\GitIgnore\Merger; +use FastForward\DevTools\GitIgnore\MergerInterface; +use FastForward\DevTools\GitIgnore\Reader; +use FastForward\DevTools\GitIgnore\ReaderInterface; +use FastForward\DevTools\GitIgnore\Writer; +use FastForward\DevTools\GitIgnore\WriterInterface; +use FastForward\DevTools\License\Generator; +use FastForward\DevTools\License\GeneratorInterface; +use FastForward\DevTools\License\Resolver; +use FastForward\DevTools\License\ResolverInterface; +use FastForward\DevTools\Path\DevToolsPathResolver; +use FastForward\DevTools\Path\WorkingProjectPathResolver; +use FastForward\DevTools\Php\Extension; +use FastForward\DevTools\Php\ExtensionInterface; +use FastForward\DevTools\PhpUnit\Coverage\CoverageSummaryLoader; +use FastForward\DevTools\PhpUnit\Coverage\CoverageSummaryLoaderInterface; +use FastForward\DevTools\Process\ColorPreservingProcessEnvironmentConfigurator; +use FastForward\DevTools\Process\CompositeProcessEnvironmentConfigurator; +use FastForward\DevTools\Process\ProcessBuilder; +use FastForward\DevTools\Process\ProcessBuilderInterface; +use FastForward\DevTools\Process\ProcessEnvironmentConfiguratorInterface; +use FastForward\DevTools\Process\ProcessQueue; +use FastForward\DevTools\Process\ProcessQueueInterface; +use FastForward\DevTools\Process\XdebugDisablingProcessEnvironmentConfigurator; +use FastForward\DevTools\Project\ProjectCapabilitiesResolver; +use FastForward\DevTools\Project\ProjectCapabilitiesResolverInterface; +use FastForward\DevTools\Psr\Clock\SystemClock; +use FastForward\DevTools\Resource\DifferInterface; +use FastForward\DevTools\Resource\UnifiedDiffer; +use FastForward\DevTools\SelfUpdate\ComposerSelfUpdateRunner; +use FastForward\DevTools\SelfUpdate\ComposerSelfUpdateScopeResolver; +use FastForward\DevTools\SelfUpdate\ComposerVersionChecker; +use FastForward\DevTools\SelfUpdate\SelfUpdateRunnerInterface; +use FastForward\DevTools\SelfUpdate\SelfUpdateScopeResolverInterface; +use FastForward\DevTools\SelfUpdate\VersionCheckerInterface; +use FastForward\DevTools\SelfUpdate\VersionCheckNotifier; +use FastForward\DevTools\SelfUpdate\VersionCheckNotifierInterface; +use FastForward\DevTools\SelfUpdate\WorkingDirectorySwitcher; +use FastForward\DevTools\SelfUpdate\WorkingDirectorySwitcherInterface; +use Interop\Container\ServiceProviderInterface; +use Psr\Clock\ClockInterface; +use Psr\Log\LoggerInterface; +use SebastianBergmann\Diff\Output\DiffOutputBuilderInterface; +use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\Config\FileLocatorInterface; +use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Twig\Loader\FilesystemLoader; +use Twig\Loader\LoaderInterface; + +use function DI\create; +use function DI\factory; +use function DI\get; + +/** + * Registers the services exposed by the DevTools container. + */ +class DevToolsServiceProvider implements ServiceProviderInterface +{ + /** + * @return array + */ + public function getFactories(): array + { + return [ + // Process + EnvironmentInterface::class => get(Environment::class), + RuntimeEnvironmentInterface::class => get(RuntimeEnvironment::class), + ExtensionInterface::class => get(Extension::class), + OutputCapabilityDetectorInterface::class => get(OutputCapabilityDetector::class), + ProcessBuilderInterface::class => get(ProcessBuilder::class), + ProcessEnvironmentConfiguratorInterface::class => create(CompositeProcessEnvironmentConfigurator::class) + ->constructor([ + get(ColorPreservingProcessEnvironmentConfigurator::class), + get(XdebugDisablingProcessEnvironmentConfigurator::class), + ]), + ProcessQueueInterface::class => get(ProcessQueue::class), + + // Self-update + SelfUpdateRunnerInterface::class => get(ComposerSelfUpdateRunner::class), + SelfUpdateScopeResolverInterface::class => get(ComposerSelfUpdateScopeResolver::class), + VersionCheckerInterface::class => get(ComposerVersionChecker::class), + VersionCheckNotifierInterface::class => get(VersionCheckNotifier::class), + WorkingDirectorySwitcherInterface::class => get(WorkingDirectorySwitcher::class), + + // Filesystem + FinderFactoryInterface::class => get(FinderFactory::class), + FilesystemInterface::class => get(Filesystem::class), + + // Composer + ComposerJsonInterface::class => get(ComposerJson::class), + + // Project + ProjectCapabilitiesResolverInterface::class => get(ProjectCapabilitiesResolver::class), + + // Changelog + ChangelogManagerInterface::class => get(ChangelogManager::class), + ChangelogParserInterface::class => get(ChangelogParser::class), + MarkdownRendererInterface::class => get(MarkdownRenderer::class), + UnreleasedEntryCheckerInterface::class => get(UnreleasedEntryChecker::class), + + // Git + GitClientInterface::class => get(GitClient::class), + + // Symfony Components + FileLocatorInterface::class => factory( + static fn(): FileLocator => new FileLocator([ + WorkingProjectPathResolver::getProjectPath(), + DevToolsPathResolver::getPackagePath(), + ]) + ), + + // PSR + LoggerInterface::class => get(OutputFormatLogger::class), + ClockInterface::class => get(SystemClock::class), + + // Console + InputInterface::class => get(ArgvInput::class), + OutputInterface::class => get(ConsoleOutputInterface::class), + CommandLoaderInterface::class => get(DevToolsCommandLoader::class), + CommandProvider::class => get(DevToolsCommandProvider::class), + ConsoleOutputInterface::class => create(ConsoleOutput::class) + ->method('setVerbosity', ConsoleOutputInterface::VERBOSITY_VERBOSE) + ->method('setFormatter', get(LogLevelOutputFormatter::class)), + GithubActionOutput::class => create(GithubActionOutput::class)->constructor( + get(ConsoleOutputInterface::class), + get(RuntimeEnvironmentInterface::class) + ), + ContextProcessorInterface::class => create(CompositeContextProcessor::class)->constructor([ + get(CommandInputProcessor::class), + get(CommandOutputProcessor::class), + ]), + + // Coverage + CoverageSummaryLoaderInterface::class => get(CoverageSummaryLoader::class), + + // Resource + DiffOutputBuilderInterface::class => get(UnifiedDiffOutputBuilder::class), + DifferInterface::class => get(UnifiedDiffer::class), + + // GitIgnore + MergerInterface::class => get(Merger::class), + ReaderInterface::class => get(Reader::class), + WriterInterface::class => get(Writer::class), + + // GitAttributes + CandidateProviderInterface::class => get(CandidateProvider::class), + ExistenceCheckerInterface::class => get(ExistenceChecker::class), + ExportIgnoreFilterInterface::class => get(ExportIgnoreFilter::class), + GitAttributesMergerInterface::class => get(GitAttributesMerger::class), + GitAttributesReaderInterface::class => get(GitAttributesReader::class), + GitAttributesWriterInterface::class => get(GitAttributesWriter::class), + + // License + GeneratorInterface::class => get(Generator::class), + ResolverInterface::class => get(Resolver::class), + + // Twig + LoaderInterface::class => create(FilesystemLoader::class)->constructor( + DevToolsPathResolver::getResourcesPath() + ), + ]; + } + + /** + * @return array + */ + public function getExtensions(): array + { + return []; + } +} diff --git a/src/Environment/RuntimeEnvironment.php b/src/Environment/RuntimeEnvironment.php index e1d2376fcb..8462751e35 100644 --- a/src/Environment/RuntimeEnvironment.php +++ b/src/Environment/RuntimeEnvironment.php @@ -66,10 +66,14 @@ public function isCi(): bool } /** - * Returns whether the Composer test suite runtime flag is enabled. + * Returns whether the current process runs inside the Composer or PHPUnit test runtime. */ public function isComposerTestRun(): bool { + if (\defined('PHPUNIT_COMPOSER_INSTALL')) { + return true; + } + return $this->isEnabled('COMPOSER_TESTS_ARE_RUNNING'); } diff --git a/src/Environment/RuntimeEnvironmentInterface.php b/src/Environment/RuntimeEnvironmentInterface.php index 61001ec762..1ea315e930 100644 --- a/src/Environment/RuntimeEnvironmentInterface.php +++ b/src/Environment/RuntimeEnvironmentInterface.php @@ -44,7 +44,7 @@ public function isGithubActions(): bool; public function isCi(): bool; /** - * Returns whether the Composer test suite runtime flag is enabled. + * Returns whether the current process runs inside the Composer or PHPUnit test runtime. */ public function isComposerTestRun(): bool; diff --git a/src/ServiceProvider/DevToolsServiceProvider.php b/src/ServiceProvider/DevToolsServiceProvider.php index 7b47081c80..ca7232fa6f 100644 --- a/src/ServiceProvider/DevToolsServiceProvider.php +++ b/src/ServiceProvider/DevToolsServiceProvider.php @@ -19,227 +19,9 @@ namespace FastForward\DevTools\ServiceProvider; -use Composer\Plugin\Capability\CommandProvider; -use FastForward\DevTools\Changelog\Manager\ChangelogManager; -use FastForward\DevTools\Changelog\Manager\ChangelogManagerInterface; -use FastForward\DevTools\Changelog\Parser\ChangelogParser; -use FastForward\DevTools\Changelog\Parser\ChangelogParserInterface; -use FastForward\DevTools\Composer\Capability\DevToolsCommandProvider; -use FastForward\DevTools\Composer\Json\ComposerJson; -use FastForward\DevTools\Composer\Json\ComposerJsonInterface; -use FastForward\DevTools\Git\GitClient; -use FastForward\DevTools\Git\GitClientInterface; -use FastForward\DevTools\Changelog\Renderer\MarkdownRenderer; -use FastForward\DevTools\Changelog\Renderer\MarkdownRendererInterface; -use FastForward\DevTools\Changelog\Checker\UnreleasedEntryChecker; -use FastForward\DevTools\Changelog\Checker\UnreleasedEntryCheckerInterface; -use FastForward\DevTools\Console\CommandLoader\DevToolsCommandLoader; -use FastForward\DevTools\Console\Formatter\LogLevelOutputFormatter; -use FastForward\DevTools\Console\Logger\OutputFormatLogger; -use FastForward\DevTools\Console\Logger\Processor\CommandInputProcessor; -use FastForward\DevTools\Console\Logger\Processor\CommandOutputProcessor; -use FastForward\DevTools\Console\Logger\Processor\CompositeContextProcessor; -use FastForward\DevTools\Console\Logger\Processor\ContextProcessorInterface; -use FastForward\DevTools\Console\Output\GithubActionOutput; -use FastForward\DevTools\Console\Output\OutputCapabilityDetector; -use FastForward\DevTools\Console\Output\OutputCapabilityDetectorInterface; -use FastForward\DevTools\Environment\Environment; -use FastForward\DevTools\Environment\EnvironmentInterface; -use FastForward\DevTools\Environment\RuntimeEnvironment; -use FastForward\DevTools\Environment\RuntimeEnvironmentInterface; -use FastForward\DevTools\Filesystem\FinderFactory; -use FastForward\DevTools\Filesystem\FinderFactoryInterface; -use FastForward\DevTools\Filesystem\Filesystem; -use FastForward\DevTools\Filesystem\FilesystemInterface; -use FastForward\DevTools\GitAttributes\CandidateProvider; -use FastForward\DevTools\GitAttributes\CandidateProviderInterface; -use FastForward\DevTools\GitAttributes\ExistenceChecker; -use FastForward\DevTools\GitAttributes\ExistenceCheckerInterface; -use FastForward\DevTools\GitAttributes\ExportIgnoreFilter; -use FastForward\DevTools\GitAttributes\ExportIgnoreFilterInterface; -use FastForward\DevTools\GitAttributes\Merger as GitAttributesMerger; -use FastForward\DevTools\GitAttributes\MergerInterface as GitAttributesMergerInterface; -use FastForward\DevTools\GitAttributes\Reader as GitAttributesReader; -use FastForward\DevTools\GitAttributes\ReaderInterface as GitAttributesReaderInterface; -use FastForward\DevTools\GitAttributes\Writer as GitAttributesWriter; -use FastForward\DevTools\GitAttributes\WriterInterface as GitAttributesWriterInterface; -use FastForward\DevTools\GitIgnore\Merger; -use FastForward\DevTools\GitIgnore\MergerInterface; -use FastForward\DevTools\GitIgnore\Reader; -use FastForward\DevTools\GitIgnore\ReaderInterface; -use FastForward\DevTools\GitIgnore\Writer; -use FastForward\DevTools\GitIgnore\WriterInterface; -use FastForward\DevTools\License\Generator; -use FastForward\DevTools\License\GeneratorInterface; -use FastForward\DevTools\License\Resolver; -use FastForward\DevTools\License\ResolverInterface; -use FastForward\DevTools\Php\Extension; -use FastForward\DevTools\Php\ExtensionInterface; -use FastForward\DevTools\PhpUnit\Coverage\CoverageSummaryLoader; -use FastForward\DevTools\PhpUnit\Coverage\CoverageSummaryLoaderInterface; -use FastForward\DevTools\Process\ColorPreservingProcessEnvironmentConfigurator; -use FastForward\DevTools\Process\CompositeProcessEnvironmentConfigurator; -use FastForward\DevTools\Process\ProcessBuilder; -use FastForward\DevTools\Process\ProcessBuilderInterface; -use FastForward\DevTools\Process\ProcessEnvironmentConfiguratorInterface; -use FastForward\DevTools\Process\ProcessQueue; -use FastForward\DevTools\Process\ProcessQueueInterface; -use FastForward\DevTools\Process\XdebugDisablingProcessEnvironmentConfigurator; -use FastForward\DevTools\Project\ProjectCapabilitiesResolver; -use FastForward\DevTools\Project\ProjectCapabilitiesResolverInterface; -use FastForward\DevTools\SelfUpdate\ComposerSelfUpdateRunner; -use FastForward\DevTools\SelfUpdate\ComposerSelfUpdateScopeResolver; -use FastForward\DevTools\SelfUpdate\ComposerVersionChecker; -use FastForward\DevTools\SelfUpdate\SelfUpdateRunnerInterface; -use FastForward\DevTools\SelfUpdate\SelfUpdateScopeResolverInterface; -use FastForward\DevTools\SelfUpdate\VersionCheckerInterface; -use FastForward\DevTools\SelfUpdate\VersionCheckNotifier; -use FastForward\DevTools\SelfUpdate\VersionCheckNotifierInterface; -use FastForward\DevTools\SelfUpdate\WorkingDirectorySwitcher; -use FastForward\DevTools\SelfUpdate\WorkingDirectorySwitcherInterface; -use FastForward\DevTools\Path\DevToolsPathResolver; -use FastForward\DevTools\Path\WorkingProjectPathResolver; -use FastForward\DevTools\Psr\Clock\SystemClock; -use FastForward\DevTools\Resource\DifferInterface; -use FastForward\DevTools\Resource\UnifiedDiffer; -use Interop\Container\ServiceProviderInterface; -use Psr\Clock\ClockInterface; -use Psr\Log\LoggerInterface; -use SebastianBergmann\Diff\Output\DiffOutputBuilderInterface; -use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder; -use Symfony\Component\Config\FileLocator; -use Symfony\Component\Config\FileLocatorInterface; -use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; -use Symfony\Component\Console\Input\ArgvInput; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\ConsoleOutput; -use Symfony\Component\Console\Output\ConsoleOutputInterface; -use Symfony\Component\Console\Output\OutputInterface; -use Twig\Loader\FilesystemLoader; -use Twig\Loader\LoaderInterface; - -use function DI\create; -use function DI\factory; -use function DI\get; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider as ContainerDevToolsServiceProvider; /** - * DevToolsServiceProvider registers the services provided by this package. - * - * This class implements the ServiceProviderInterface from the PHP-Interop container package, - * allowing it to be used with any compatible dependency injection container. + * @deprecated use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider instead */ -final class DevToolsServiceProvider implements ServiceProviderInterface -{ - /** - * @return array - */ - public function getFactories(): array - { - return [ - // Process - EnvironmentInterface::class => get(Environment::class), - RuntimeEnvironmentInterface::class => get(RuntimeEnvironment::class), - ExtensionInterface::class => get(Extension::class), - OutputCapabilityDetectorInterface::class => get(OutputCapabilityDetector::class), - ProcessBuilderInterface::class => get(ProcessBuilder::class), - ProcessEnvironmentConfiguratorInterface::class => create(CompositeProcessEnvironmentConfigurator::class) - ->constructor([ - get(ColorPreservingProcessEnvironmentConfigurator::class), - get(XdebugDisablingProcessEnvironmentConfigurator::class), - ]), - ProcessQueueInterface::class => get(ProcessQueue::class), - - // Self-update - SelfUpdateRunnerInterface::class => get(ComposerSelfUpdateRunner::class), - SelfUpdateScopeResolverInterface::class => get(ComposerSelfUpdateScopeResolver::class), - VersionCheckerInterface::class => get(ComposerVersionChecker::class), - VersionCheckNotifierInterface::class => get(VersionCheckNotifier::class), - WorkingDirectorySwitcherInterface::class => get(WorkingDirectorySwitcher::class), - - // Filesystem - FinderFactoryInterface::class => get(FinderFactory::class), - FilesystemInterface::class => get(Filesystem::class), - - // Composer - ComposerJsonInterface::class => get(ComposerJson::class), - - // Project - ProjectCapabilitiesResolverInterface::class => get(ProjectCapabilitiesResolver::class), - - // Changelog - ChangelogManagerInterface::class => get(ChangelogManager::class), - ChangelogParserInterface::class => get(ChangelogParser::class), - MarkdownRendererInterface::class => get(MarkdownRenderer::class), - UnreleasedEntryCheckerInterface::class => get(UnreleasedEntryChecker::class), - - // Git - GitClientInterface::class => get(GitClient::class), - - // Symfony Components - FileLocatorInterface::class => factory( - static fn(): FileLocator => new FileLocator([ - WorkingProjectPathResolver::getProjectPath(), - DevToolsPathResolver::getPackagePath(), - ]) - ), - - // PSR - LoggerInterface::class => get(OutputFormatLogger::class), - ClockInterface::class => get(SystemClock::class), - - // Console - InputInterface::class => get(ArgvInput::class), - OutputInterface::class => get(ConsoleOutputInterface::class), - CommandLoaderInterface::class => get(DevToolsCommandLoader::class), - CommandProvider::class => get(DevToolsCommandProvider::class), - ConsoleOutputInterface::class => create(ConsoleOutput::class) - ->method('setVerbosity', ConsoleOutputInterface::VERBOSITY_VERBOSE) - ->method('setFormatter', get(LogLevelOutputFormatter::class)), - GithubActionOutput::class => create(GithubActionOutput::class)->constructor( - get(ConsoleOutputInterface::class), - get(RuntimeEnvironmentInterface::class) - ), - ContextProcessorInterface::class => create(CompositeContextProcessor::class)->constructor([ - get(CommandInputProcessor::class), - get(CommandOutputProcessor::class), - ]), - - // Coverage - CoverageSummaryLoaderInterface::class => get(CoverageSummaryLoader::class), - - // Resource - DiffOutputBuilderInterface::class => get(UnifiedDiffOutputBuilder::class), - DifferInterface::class => get(UnifiedDiffer::class), - - // GitIgnore - MergerInterface::class => get(Merger::class), - ReaderInterface::class => get(Reader::class), - WriterInterface::class => get(Writer::class), - - // GitAttributes - CandidateProviderInterface::class => get(CandidateProvider::class), - ExistenceCheckerInterface::class => get(ExistenceChecker::class), - ExportIgnoreFilterInterface::class => get(ExportIgnoreFilter::class), - GitAttributesMergerInterface::class => get(GitAttributesMerger::class), - GitAttributesReaderInterface::class => get(GitAttributesReader::class), - GitAttributesWriterInterface::class => get(GitAttributesWriter::class), - - // License - GeneratorInterface::class => get(Generator::class), - ResolverInterface::class => get(Resolver::class), - - // Twig - LoaderInterface::class => create(FilesystemLoader::class)->constructor( - DevToolsPathResolver::getResourcesPath() - ), - ]; - } - - /** - * @return array - */ - public function getExtensions(): array - { - return []; - } -} +final class DevToolsServiceProvider extends ContainerDevToolsServiceProvider {} diff --git a/tests/Composer/Capability/DevToolsCommandProviderTest.php b/tests/Composer/Capability/DevToolsCommandProviderTest.php index 3d4e21d7d7..613c0cbd2f 100644 --- a/tests/Composer/Capability/DevToolsCommandProviderTest.php +++ b/tests/Composer/Capability/DevToolsCommandProviderTest.php @@ -24,25 +24,24 @@ use FastForward\DevTools\Composer\DevToolsPluginInterface; use FastForward\DevTools\Console\Command\FixtureWithoutAsCommand; use FastForward\DevTools\Console\DevTools; +use FastForward\DevTools\Container\ContainerFactory; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; -use Psr\Container\ContainerInterface; -use ReflectionProperty; +use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; #[CoversClass(DevToolsCommandProvider::class)] +#[UsesClass(ContainerFactory::class)] #[UsesClass(DevTools::class)] #[UsesClass(ProxyCommand::class)] final class DevToolsCommandProviderTest extends TestCase { use ProphecyTrait; - private ObjectProphecy $container; - - private ObjectProphecy $devTools; + private ObjectProphecy $commandLoader; private ObjectProphecy $plugin; @@ -53,16 +52,20 @@ final class DevToolsCommandProviderTest extends TestCase */ protected function setUp(): void { - $this->container = $this->prophesize(ContainerInterface::class); - $this->devTools = $this->prophesize(DevTools::class); + ContainerFactory::reset(); + $this->commandLoader = $this->prophesize(CommandLoaderInterface::class); $this->plugin = $this->prophesize(DevToolsPluginInterface::class); - $this->container->get(DevTools::class) - ->willReturn($this->devTools->reveal()) - ->shouldBeCalledOnce(); - - $this->devTools->all() - ->willReturn([])->shouldBeCalledOnce(); + $this->commandLoader->getNames() + ->willReturn([]); + $this->commandLoader->has('help') + ->willReturn(false); + $this->commandLoader->has('list') + ->willReturn(false); + $this->commandLoader->has('complete') + ->willReturn(false); + $this->commandLoader->has('_complete') + ->willReturn(false); $this->plugin->isRegisteredCommand(null) ->willReturn(false); @@ -89,8 +92,15 @@ protected function setUp(): void 'plugin' => $this->plugin->reveal(), ]); - $property = new ReflectionProperty(DevTools::class, 'container'); - $property->setValue(null, $this->container->reveal()); + ContainerFactory::set(CommandLoaderInterface::class, $this->commandLoader->reveal()); + } + + /** + * @return void + */ + protected function tearDown(): void + { + ContainerFactory::reset(); } /** @@ -117,10 +127,13 @@ public function getCommandsWillReturnComposerProxyCommandsForRegisteredSymfonyCo $symfonyCommand->setHelp(''); $symfonyCommand->setHidden(false); - $this->devTools->all() - ->willReturn([ - 'agents' => $symfonyCommand, - ]) + $this->commandLoader->getNames() + ->willReturn(['agents']); + $this->commandLoader->has('agents') + ->willReturn(true) + ->shouldBeCalledOnce(); + $this->commandLoader->get('agents') + ->willReturn($symfonyCommand) ->shouldBeCalledOnce(); $commands = array_values($this->commandProvider->getCommands()); @@ -144,12 +157,18 @@ public function getCommandsWillIgnoreAliasEntriesFromApplicationAllRegistry(): v $symfonyCommand->setHelp(''); $symfonyCommand->setHidden(false); - $this->devTools->all() - ->willReturn([ - 'reports:tests' => $symfonyCommand, - 'tests' => $symfonyCommand, - ]) + $this->commandLoader->getNames() + ->willReturn(['reports:tests', 'tests']); + $this->commandLoader->has('reports:tests') + ->willReturn(true) ->shouldBeCalledOnce(); + $this->commandLoader->has('tests') + ->shouldNotBeCalled(); + $this->commandLoader->get('reports:tests') + ->willReturn($symfonyCommand) + ->shouldBeCalledOnce(); + $this->commandLoader->get('tests') + ->shouldNotBeCalled(); $commands = array_values($this->commandProvider->getCommands()); $proxyCommand = $commands[0]; @@ -172,12 +191,18 @@ public function getCommandsWillPreserveSafeAliasesThroughComposerPlugin(): void $symfonyCommand->setHelp(''); $symfonyCommand->setHidden(false); - $this->devTools->all() - ->willReturn([ - 'dev-tools:standards' => $symfonyCommand, - 'standards' => $symfonyCommand, - ]) + $this->commandLoader->getNames() + ->willReturn(['dev-tools:standards', 'standards']); + $this->commandLoader->has('dev-tools:standards') + ->willReturn(true) + ->shouldBeCalledOnce(); + $this->commandLoader->has('standards') + ->shouldNotBeCalled(); + $this->commandLoader->get('dev-tools:standards') + ->willReturn($symfonyCommand) ->shouldBeCalledOnce(); + $this->commandLoader->get('standards') + ->shouldNotBeCalled(); $proxyCommand = array_values($this->commandProvider->getCommands())[0]; @@ -198,12 +223,18 @@ public function getCommandsWillNotExposeSelfUpdateAliasToComposer(): void $symfonyCommand->setHelp(''); $symfonyCommand->setHidden(false); - $this->devTools->all() - ->willReturn([ - 'dev-tools:self-update' => $symfonyCommand, - 'self-update' => $symfonyCommand, - ]) + $this->commandLoader->getNames() + ->willReturn(['dev-tools:self-update', 'self-update']); + $this->commandLoader->has('dev-tools:self-update') + ->willReturn(true) ->shouldBeCalledOnce(); + $this->commandLoader->has('self-update') + ->shouldNotBeCalled(); + $this->commandLoader->get('dev-tools:self-update') + ->willReturn($symfonyCommand) + ->shouldBeCalledOnce(); + $this->commandLoader->get('self-update') + ->shouldNotBeCalled(); $proxyCommand = array_values($this->commandProvider->getCommands())[0]; @@ -224,10 +255,13 @@ public function getCommandsWillNotExposeCommandsOwnedByComposer(): void $symfonyCommand->setHelp(''); $symfonyCommand->setHidden(false); - $this->devTools->all() - ->willReturn([ - 'install' => $symfonyCommand, - ]) + $this->commandLoader->getNames() + ->willReturn(['install']); + $this->commandLoader->has('install') + ->willReturn(true) + ->shouldBeCalledOnce(); + $this->commandLoader->get('install') + ->willReturn($symfonyCommand) ->shouldBeCalledOnce(); self::assertSame([], $this->commandProvider->getCommands()); diff --git a/tests/Console/Command/TestsCommandTest.php b/tests/Console/Command/TestsCommandTest.php index e4de221b74..c12e808e0f 100644 --- a/tests/Console/Command/TestsCommandTest.php +++ b/tests/Console/Command/TestsCommandTest.php @@ -22,6 +22,7 @@ use FastForward\DevTools\Composer\Json\ComposerJsonInterface; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Console\Command\TestsCommand; +use FastForward\DevTools\Container\ContainerFactory; use FastForward\DevTools\Environment\RuntimeEnvironmentInterface; use FastForward\DevTools\Filesystem\FilesystemInterface; use FastForward\DevTools\PhpUnit\Bootstrap\BootstrapShimGenerator; @@ -55,6 +56,7 @@ #[CoversClass(TestsCommand::class)] #[UsesClass(BootstrapShimGenerator::class)] #[UsesClass(CoverageSummary::class)] +#[UsesClass(ContainerFactory::class)] #[UsesClass(DevToolsPathResolver::class)] #[UsesClass(ProcessBuilder::class)] #[UsesClass(ManagedWorkspace::class)] @@ -92,6 +94,7 @@ final class TestsCommandTest extends TestCase */ protected function setUp(): void { + ContainerFactory::reset(); $this->coverageSummaryLoader = $this->prophesize(CoverageSummaryLoaderInterface::class); $this->composerJson = $this->prophesize(ComposerJsonInterface::class); $this->filesystem = $this->prophesize(FilesystemInterface::class); @@ -112,8 +115,6 @@ protected function setUp(): void new ProcessBuilder(), $this->processQueue->reveal(), $this->projectCapabilitiesResolver->reveal(), - $this->runtimeEnvironment->reveal(), - $this->logger->reveal(), ); $this->composerJson->getAutoload('psr-4') @@ -133,6 +134,8 @@ protected function setUp(): void ->willReturn(false); $this->runtimeEnvironment->isComposerTestRun() ->willReturn(true); + ContainerFactory::set(RuntimeEnvironmentInterface::class, $this->runtimeEnvironment->reveal()); + ContainerFactory::set(LoggerInterface::class, $this->logger->reveal()); $this->fileLocator->locate(TestsCommand::CONFIG)->willReturn(getcwd() . '/' . TestsCommand::CONFIG); $this->filesystem->getAbsolutePath('./vendor/autoload.php') ->willReturn(getcwd() . '/vendor/autoload.php'); @@ -159,6 +162,14 @@ protected function setUp(): void ->willReturn(false); } + /** + * @return void + */ + protected function tearDown(): void + { + ContainerFactory::reset(); + } + /** * @return void */ diff --git a/tests/Console/DevToolsTest.php b/tests/Console/DevToolsTest.php index 240276a54b..b71bcbed57 100644 --- a/tests/Console/DevToolsTest.php +++ b/tests/Console/DevToolsTest.php @@ -24,6 +24,8 @@ use FastForward\DevTools\Console\DevTools; use FastForward\DevTools\Console\Formatter\LogLevelOutputFormatter; use FastForward\DevTools\Console\Output\GithubActionOutput; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; use FastForward\DevTools\Environment\EnvironmentInterface; use FastForward\DevTools\Environment\RuntimeEnvironment; use FastForward\DevTools\Environment\RuntimeEnvironmentInterface; @@ -46,7 +48,6 @@ use FastForward\DevTools\SelfUpdate\VersionCheckNotifierInterface; use FastForward\DevTools\SelfUpdate\WorkingDirectorySwitcher; use FastForward\DevTools\SelfUpdate\WorkingDirectorySwitcherInterface; -use FastForward\DevTools\ServiceProvider\DevToolsServiceProvider; use Override; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; @@ -57,7 +58,6 @@ use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use ReflectionMethod; -use ReflectionProperty; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\CompleteCommand; use Symfony\Component\Console\Command\DumpCompletionCommand; @@ -83,6 +83,7 @@ #[UsesClass(ClassReflection::class)] #[UsesClass(LogLevelOutputFormatter::class)] #[UsesClass(GithubActionOutput::class)] +#[UsesClass(ContainerFactory::class)] #[UsesClass(RuntimeEnvironment::class)] #[UsesClass(ColorPreservingProcessEnvironmentConfigurator::class)] #[UsesClass(CompositeProcessEnvironmentConfigurator::class)] @@ -143,6 +144,7 @@ final class DevToolsTest extends TestCase #[Override] protected function setUp(): void { + ContainerFactory::reset(); $this->commandLoader = $this->prophesize(CommandLoaderInterface::class); $this->commandLoader->getNames() ->willReturn([]); @@ -166,6 +168,7 @@ protected function setUp(): void #[Override] protected function tearDown(): void { + ContainerFactory::reset(); if (false === $this->originalWorkspaceDirectoryEnv) { putenv(ManagedWorkspace::ENV_WORKSPACE_DIR); @@ -505,8 +508,7 @@ public function __construct() #[Test] public function createWillReturnInstanceOfDevTools(): void { - $reflectionProperty = new ReflectionProperty(DevTools::class, 'container'); - $reflectionProperty->setValue(null, null); + ContainerFactory::reset(); $devTools = DevTools::create(); diff --git a/tests/Console/Input/HasJsonOptionTest.php b/tests/Console/Input/HasJsonOptionTest.php index 72641e2b9d..c8c82cec99 100644 --- a/tests/Console/Input/HasJsonOptionTest.php +++ b/tests/Console/Input/HasJsonOptionTest.php @@ -20,9 +20,11 @@ namespace FastForward\DevTools\Tests\Console\Input; use FastForward\DevTools\Console\Input\HasJsonOption; +use FastForward\DevTools\Container\ContainerFactory; use FastForward\DevTools\Environment\RuntimeEnvironmentInterface; use PHPUnit\Framework\Attributes\CoversTrait; use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\Console\Input\InputInterface; @@ -30,6 +32,7 @@ use function Safe\putenv; #[CoversTrait(HasJsonOption::class)] +#[UsesClass(ContainerFactory::class)] final class HasJsonOptionTest extends TestCase { use ProphecyTrait; @@ -51,6 +54,7 @@ final class HasJsonOptionTest extends TestCase */ protected function setUp(): void { + ContainerFactory::reset(); $this->server = $_SERVER; $this->environment = $_ENV; $this->composerTestsAreRunning = getenv('COMPOSER_TESTS_ARE_RUNNING'); @@ -65,6 +69,7 @@ protected function setUp(): void */ protected function tearDown(): void { + ContainerFactory::reset(); $_SERVER = $this->server; $_ENV = $this->environment; @@ -98,10 +103,18 @@ public function isJsonOutputWillUseRuntimeEnvironmentWhenAvailable(): void $command = new class ($runtimeEnvironment->reveal()) { use HasJsonOption; + /** + * @param RuntimeEnvironmentInterface $runtimeEnvironment + */ public function __construct( private readonly RuntimeEnvironmentInterface $runtimeEnvironment, ) {} + /** + * @param InputInterface $input + * + * @return bool + */ public function isStructured(InputInterface $input): bool { return $this->isJsonOutput($input); @@ -128,6 +141,11 @@ public function isJsonOutputWillIgnoreFallbackAgentDetectionDuringPhpUnitRuns(): $command = new class { use HasJsonOption; + /** + * @param InputInterface $input + * + * @return bool + */ public function isStructured(InputInterface $input): bool { return $this->isJsonOutput($input); diff --git a/tests/Container/ContainerFactoryTest.php b/tests/Container/ContainerFactoryTest.php new file mode 100644 index 0000000000..dbe4c2f518 --- /dev/null +++ b/tests/Container/ContainerFactoryTest.php @@ -0,0 +1,92 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/ + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward/dev-tools/issues + * @see https://php-fast-forward.github.io/dev-tools/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Tests\Container; + +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesClass; +use PHPUnit\Framework\TestCase; +use stdClass; +use Symfony\Component\Config\FileLocatorInterface; + +#[CoversClass(ContainerFactory::class)] +#[UsesClass(DevToolsServiceProvider::class)] +final class ContainerFactoryTest extends TestCase +{ + /** + * @return void + */ + protected function setUp(): void + { + ContainerFactory::reset(); + } + + /** + * @return void + */ + protected function tearDown(): void + { + ContainerFactory::reset(); + } + + /** + * @return void + */ + #[Test] + public function createWillReturnTheSharedContainerInstance(): void + { + self::assertSame(ContainerFactory::create(), ContainerFactory::create()); + } + + /** + * @return void + */ + #[Test] + public function getWillResolveServicesFromTheSharedContainer(): void + { + self::assertInstanceOf(FileLocatorInterface::class, ContainerFactory::get(FileLocatorInterface::class)); + } + + /** + * @return void + */ + #[Test] + public function hasWillReturnWhetherTheSharedContainerCanResolveAService(): void + { + self::assertTrue(ContainerFactory::has(FileLocatorInterface::class)); + self::assertFalse(ContainerFactory::has('dev-tools.missing-service')); + } + + /** + * @return void + */ + #[Test] + public function setWillOverrideSharedContainerEntries(): void + { + $service = new stdClass(); + + ContainerFactory::set('dev-tools.custom-service', $service); + + self::assertTrue(ContainerFactory::has('dev-tools.custom-service')); + self::assertSame($service, ContainerFactory::get('dev-tools.custom-service')); + } +} diff --git a/tests/Environment/RuntimeEnvironmentTest.php b/tests/Environment/RuntimeEnvironmentTest.php index f297c11b8c..5fde743390 100644 --- a/tests/Environment/RuntimeEnvironmentTest.php +++ b/tests/Environment/RuntimeEnvironmentTest.php @@ -155,6 +155,15 @@ public function isComposerTestRunWillReturnWhetherComposerTestsFlagIsEnabled(): self::assertTrue($this->runtimeEnvironment->isComposerTestRun()); } + /** + * @return void + */ + #[Test] + public function isComposerTestRunWillReturnTrueDuringPhpUnitRuntime(): void + { + self::assertTrue($this->runtimeEnvironment->isComposerTestRun()); + } + /** * @return void */ diff --git a/tests/ServiceProvider/DevToolsServiceProviderTest.php b/tests/ServiceProvider/DevToolsServiceProviderTest.php index aa18e58850..392fe573b4 100644 --- a/tests/ServiceProvider/DevToolsServiceProviderTest.php +++ b/tests/ServiceProvider/DevToolsServiceProviderTest.php @@ -20,9 +20,9 @@ namespace FastForward\DevTools\Tests\ServiceProvider; use DI\Container; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Path\WorkingProjectPathResolver; -use FastForward\DevTools\ServiceProvider\DevToolsServiceProvider; use Interop\Container\ServiceProviderInterface; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; From 08d87f3f8e6affc65ff6961e1b12d1a3f66303ab Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 21:35:20 +0000 Subject: [PATCH 13/35] Update wiki submodule pointer for PR #329 --- .github/wiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/wiki b/.github/wiki index 8995313710..6ce765289e 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit 8995313710eb82dbe1073e7b5851973b63f82b22 +Subproject commit 6ce765289ee623e1d55567d6c548b4b10c4a1550 From 3acfefd1c4f0ff79d4810bea1ec713640b63c3b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 18:50:48 -0300 Subject: [PATCH 14/35] [console] Align structured output detection --- bin/dev-tools.php | 2 +- src/Console/Command/TestsCommand.php | 7 +- src/Console/Logger/OutputFormatLogger.php | 8 +-- tests/Console/Command/TestsCommandTest.php | 42 ++++++++++++ .../Console/Logger/OutputFormatLoggerTest.php | 65 ++++++++++--------- 5 files changed, 88 insertions(+), 36 deletions(-) diff --git a/bin/dev-tools.php b/bin/dev-tools.php index 62e8fd254b..2747b30f31 100644 --- a/bin/dev-tools.php +++ b/bin/dev-tools.php @@ -21,7 +21,7 @@ use FastForward\DevTools\Console\DevTools; -$autoloadCandidates = [\dirname(__DIR__) . '/vendor/autoload.php', \dirname(__DIR__, 4) . '/vendor/autoload.php']; +$autoloadCandidates = [\dirname(__DIR__, 4) . '/vendor/autoload.php', \dirname(__DIR__) . '/vendor/autoload.php']; foreach ($autoloadCandidates as $autoloadCandidate) { if (is_file($autoloadCandidate)) { diff --git a/src/Console/Command/TestsCommand.php b/src/Console/Command/TestsCommand.php index 467a866745..c363c0f1f5 100644 --- a/src/Console/Command/TestsCommand.php +++ b/src/Console/Command/TestsCommand.php @@ -320,10 +320,15 @@ private function forceAgentReporter(Process $process): void { $env = $process->getEnv(); - if (\array_key_exists(self::AGENT_ENVIRONMENT_VARIABLE, $env)) { + if (\array_key_exists(self::AGENT_ENVIRONMENT_VARIABLE, $env) || false !== getenv( + self::AGENT_ENVIRONMENT_VARIABLE + )) { return; } + // Intentionally reuse the reporter's existing agent detection path for + // structured DevTools output instead of maintaining a separate + // integration that would need to mirror the plugin behavior. $env[self::AGENT_ENVIRONMENT_VARIABLE] = self::AGENT_ENVIRONMENT_VALUE; $process->setEnv($env); } diff --git a/src/Console/Logger/OutputFormatLogger.php b/src/Console/Logger/OutputFormatLogger.php index f20790c594..de824a0b6d 100644 --- a/src/Console/Logger/OutputFormatLogger.php +++ b/src/Console/Logger/OutputFormatLogger.php @@ -21,7 +21,7 @@ use Stringable; use DateTimeInterface; -use Ergebnis\AgentDetector\Detector; +use FastForward\DevTools\Environment\RuntimeEnvironmentInterface; use FastForward\DevTools\Console\Logger\Processor\ContextProcessorInterface; use FastForward\DevTools\Console\Output\GithubActionOutput; use Psr\Clock\ClockInterface; @@ -58,7 +58,7 @@ * @param ArgvInput $input the CLI input instance used to inspect runtime options * @param ConsoleOutputInterface $output the console output instance used for writing log messages * @param ClockInterface $clock provides timestamps for rendered log entries - * @param Detector $agentDetector detects agent-oriented execution environments + * @param RuntimeEnvironmentInterface $runtimeEnvironment resolves runtime-specific output behavior * @param ContextProcessorInterface $contextProcessor expands command input and output metadata * @param GithubActionOutput $githubActionOutput emits GitHub Actions annotations when supported */ @@ -66,7 +66,7 @@ public function __construct( private ArgvInput $input, private ConsoleOutputInterface $output, private ClockInterface $clock, - private Detector $agentDetector, + private RuntimeEnvironmentInterface $runtimeEnvironment, private ContextProcessorInterface $contextProcessor, private GithubActionOutput $githubActionOutput, ) {} @@ -179,7 +179,7 @@ private function isJsonOutput(): bool return true; } - return $this->agentDetector->isAgentPresent($_SERVER); + return $this->runtimeEnvironment->isAgentPresent() && ! $this->runtimeEnvironment->isComposerTestRun(); } /** diff --git a/tests/Console/Command/TestsCommandTest.php b/tests/Console/Command/TestsCommandTest.php index c12e808e0f..549192e630 100644 --- a/tests/Console/Command/TestsCommandTest.php +++ b/tests/Console/Command/TestsCommandTest.php @@ -89,12 +89,15 @@ final class TestsCommandTest extends TestCase private TestsCommand $command; + private string|false $agentEnvironment; + /** * @return void */ protected function setUp(): void { ContainerFactory::reset(); + $this->agentEnvironment = getenv('AI_AGENT'); $this->coverageSummaryLoader = $this->prophesize(CoverageSummaryLoaderInterface::class); $this->composerJson = $this->prophesize(ComposerJsonInterface::class); $this->filesystem = $this->prophesize(FilesystemInterface::class); @@ -168,6 +171,14 @@ protected function setUp(): void protected function tearDown(): void { ContainerFactory::reset(); + + if (false === $this->agentEnvironment) { + putenv('AI_AGENT'); + + return; + } + + putenv('AI_AGENT=' . $this->agentEnvironment); } /** @@ -322,6 +333,37 @@ public function executeWillCaptureStructuredPhpUnitSummaryWhenAgentEnvironmentIs self::assertSame(TestsCommand::SUCCESS, $this->invokeExecute()); } + /** + * @return void + */ + #[Test] + public function executeWillPreserveAnInheritedAgentEnvironmentWhenForcingStructuredPhpUnitOutput(): void + { + putenv('AI_AGENT=existing-agent'); + + $this->input->getOption('json') + ->willReturn(true); + $this->input->getOption('pretty-json') + ->willReturn(false); + + $this->processQueue->add( + Argument::that(static fn(Process $process): bool => ! \array_key_exists('AI_AGENT', $process->getEnv())), + false, + false, + 'Running PHPUnit Tests' + )->shouldBeCalled(); + $this->processQueue->run(Argument::type(OutputInterface::class)) + ->willReturn(TestsCommand::SUCCESS)->shouldBeCalled(); + $this->logger->info(Argument::cetera())->shouldNotBeCalled(); + $this->logger->log( + 'info', + 'PHPUnit tests completed successfully.', + Argument::type('array'), + )->shouldBeCalled(); + + self::assertSame(TestsCommand::SUCCESS, $this->invokeExecute()); + } + /** * @return void */ diff --git a/tests/Console/Logger/OutputFormatLoggerTest.php b/tests/Console/Logger/OutputFormatLoggerTest.php index eb81dcf6f0..8e49a7ae48 100644 --- a/tests/Console/Logger/OutputFormatLoggerTest.php +++ b/tests/Console/Logger/OutputFormatLoggerTest.php @@ -21,7 +21,6 @@ use stdClass; use DateTimeImmutable; -use Ergebnis\AgentDetector\Detector; use FastForward\DevTools\Console\Logger\OutputFormatLogger; use FastForward\DevTools\Console\Logger\Processor\CommandInputProcessor; use FastForward\DevTools\Console\Logger\Processor\CommandOutputProcessor; @@ -43,7 +42,6 @@ use Symfony\Component\Console\Output\OutputInterface; use function Safe\json_decode; -use function Safe\putenv; #[CoversClass(OutputFormatLogger::class)] #[UsesClass(CommandInputProcessor::class)] @@ -65,29 +63,21 @@ final class OutputFormatLoggerTest extends TestCase */ private ObjectProphecy $environment; - /** - * @var array - */ - private array $server; - - private string|false $composerTestsAreRunningEnv; - /** * @return void */ protected function setUp(): void { - $this->server = $_SERVER; - $_SERVER = []; - $this->composerTestsAreRunningEnv = getenv('COMPOSER_TESTS_ARE_RUNNING'); - putenv('COMPOSER_TESTS_ARE_RUNNING=1'); - $this->output = $this->prophesize(ConsoleOutputInterface::class); $this->errorOutput = $this->prophesize(OutputInterface::class); $this->clock = $this->prophesize(ClockInterface::class); $this->environment = $this->prophesize(RuntimeEnvironmentInterface::class); $this->environment->isGithubActions() ->willReturn(false); + $this->environment->isAgentPresent() + ->willReturn(false); + $this->environment->isComposerTestRun() + ->willReturn(false); $this->output->getErrorOutput() ->willReturn($this->errorOutput->reveal()); @@ -105,7 +95,7 @@ public function logWillWriteInterpolatedInfoMessagesToStandardOutput(): void new ArgvInput(['dev-tools']), $this->output->reveal(), $this->clock->reveal(), - new Detector(), + $this->environment->reveal(), new CompositeContextProcessor([new CommandInputProcessor(), new CommandOutputProcessor()]), $this->createGithubActionOutput(), ); @@ -141,7 +131,7 @@ public function logWillWriteErrorMessagesToErrorOutput(): void new ArgvInput(['dev-tools']), $this->output->reveal(), $this->clock->reveal(), - new Detector(), + $this->environment->reveal(), new CompositeContextProcessor([new CommandInputProcessor(), new CommandOutputProcessor()]), $this->createGithubActionOutput(), ); @@ -170,7 +160,7 @@ public function logWillWriteStructuredJsonWhenJsonOutputIsRequested(): void new ArgvInput(['dev-tools', '--json']), $this->output->reveal(), $this->clock->reveal(), - new Detector(), + $this->environment->reveal(), new CompositeContextProcessor([new CommandInputProcessor(), new CommandOutputProcessor()]), $this->createGithubActionOutput(), ); @@ -197,7 +187,7 @@ public function logWillEmitParseableJsonWhenJsonOutputIsRequested(): void new ArgvInput(['dev-tools', '--json']), $this->output->reveal(), $this->clock->reveal(), - new Detector(), + $this->environment->reveal(), new CompositeContextProcessor([new CommandInputProcessor(), new CommandOutputProcessor()]), $this->createGithubActionOutput(), ); @@ -223,7 +213,7 @@ public function logWillWritePrettyPrintedJsonWhenPrettyJsonOutputIsRequested(): new ArgvInput(['dev-tools', '--pretty-json']), $this->output->reveal(), $this->clock->reveal(), - new Detector(), + $this->environment->reveal(), new CompositeContextProcessor([new CommandInputProcessor(), new CommandOutputProcessor()]), $this->createGithubActionOutput(), ); @@ -250,7 +240,7 @@ public function logWillEmitParseableJsonWhenPrettyJsonOutputIsRequested(): void new ArgvInput(['dev-tools', '--pretty-json']), $this->output->reveal(), $this->clock->reveal(), - new Detector(), + $this->environment->reveal(), new CompositeContextProcessor([new CommandInputProcessor(), new CommandOutputProcessor()]), $this->createGithubActionOutput(), ); @@ -276,7 +266,7 @@ public function logWillEmbedDecodedStructuredCommandOutputInsteadOfEscapedJsonSt new ArgvInput(['dev-tools', '--pretty-json']), $this->output->reveal(), $this->clock->reveal(), - new Detector(), + $this->environment->reveal(), new CompositeContextProcessor([new CommandInputProcessor(), new CommandOutputProcessor()]), $this->createGithubActionOutput(), ); @@ -314,13 +304,16 @@ public function logWillEmbedDecodedStructuredCommandOutputInsteadOfEscapedJsonSt #[Test] public function logWillWriteStructuredJsonWhenAgentEnvironmentIsDetected(): void { - $_SERVER['CODEX_THREAD_ID'] = 'thread-123'; + $this->environment->isAgentPresent() + ->willReturn(true); + $this->environment->isComposerTestRun() + ->willReturn(false); $logger = new OutputFormatLogger( new ArgvInput(['dev-tools']), $this->output->reveal(), $this->clock->reveal(), - new Detector(), + $this->environment->reveal(), new CompositeContextProcessor([new CommandInputProcessor(), new CommandOutputProcessor()]), $this->createGithubActionOutput(), ); @@ -339,17 +332,29 @@ public function logWillWriteStructuredJsonWhenAgentEnvironmentIsDetected(): void /** * @return void */ - protected function tearDown(): void + #[Test] + public function logWillKeepPlainTextOutputWhenAgentEnvironmentIsDetectedDuringComposerTests(): void { - $_SERVER = $this->server; + $this->environment->isAgentPresent() + ->willReturn(true); + $this->environment->isComposerTestRun() + ->willReturn(true); - if (false === $this->composerTestsAreRunningEnv) { - putenv('COMPOSER_TESTS_ARE_RUNNING'); + $logger = new OutputFormatLogger( + new ArgvInput(['dev-tools']), + $this->output->reveal(), + $this->clock->reveal(), + $this->environment->reveal(), + new CompositeContextProcessor([new CommandInputProcessor(), new CommandOutputProcessor()]), + $this->createGithubActionOutput(), + ); - return; - } + $this->output->writeln('2026-04-21T16:00:00+00:00 [INFO] Agent ready') + ->shouldBeCalledOnce(); + $this->errorOutput->writeln(Argument::type('string')) + ->shouldNotBeCalled(); - putenv('COMPOSER_TESTS_ARE_RUNNING=' . $this->composerTestsAreRunningEnv); + $logger->info('Agent ready'); } /** From c4407d1ecd6878e8591b91a3c8d68c1e087fd06d Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 21:52:43 +0000 Subject: [PATCH 15/35] Update wiki submodule pointer for PR #329 --- .github/wiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/wiki b/.github/wiki index 6ce765289e..9639bb75ee 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit 6ce765289ee623e1d55567d6c548b4b10c4a1550 +Subproject commit 9639bb75ee4cc06d1f0feb0c397b54ef530092e0 From 36dde183bfa90a48fc137a42633ef98bffa1a8b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 18:57:38 -0300 Subject: [PATCH 16/35] [console] Normalize structured command output --- src/Console/Command/CodeStyleCommand.php | 4 +-- src/Console/Command/TestsCommand.php | 2 +- .../Console/Command/CodeStyleCommandTest.php | 26 ++++++++---------- tests/Console/Command/TestsCommandTest.php | 2 +- .../Processor/CommandOutputProcessorTest.php | 27 +++++++++++++++++++ 5 files changed, 42 insertions(+), 19 deletions(-) diff --git a/src/Console/Command/CodeStyleCommand.php b/src/Console/Command/CodeStyleCommand.php index 5eb59ecec3..ef3b600abf 100644 --- a/src/Console/Command/CodeStyleCommand.php +++ b/src/Console/Command/CodeStyleCommand.php @@ -164,14 +164,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int return $this->success('Code style checks completed successfully.', $input, [ 'fix' => $fix, 'config' => self::CONFIG, - 'process_output' => $processOutput instanceof BufferedOutput ? $processOutput->fetch() : null, + 'output' => $processOutput, ]); } return $this->failure('Code style checks failed.', $input, [ 'fix' => $fix, 'config' => self::CONFIG, - 'process_output' => $processOutput instanceof BufferedOutput ? $processOutput->fetch() : null, + 'output' => $processOutput, ]); } } diff --git a/src/Console/Command/TestsCommand.php b/src/Console/Command/TestsCommand.php index c363c0f1f5..b56f17d009 100644 --- a/src/Console/Command/TestsCommand.php +++ b/src/Console/Command/TestsCommand.php @@ -366,7 +366,7 @@ private function resolveStructuredProcessResultPayload(OutputInterface $processO ]; } - if (null !== $supplementalOutput) { + if (null !== $supplementalOutput && ! \is_array($decoded)) { $payload['raw_output'] = $supplementalOutput; } diff --git a/tests/Console/Command/CodeStyleCommandTest.php b/tests/Console/Command/CodeStyleCommandTest.php index 493530343b..16ee25ee17 100644 --- a/tests/Console/Command/CodeStyleCommandTest.php +++ b/tests/Console/Command/CodeStyleCommandTest.php @@ -129,12 +129,10 @@ public function executeWillReturnSuccessWhenProcessQueueSucceeds(): void $this->logger->log( 'info', 'Code style checks completed successfully.', - [ - 'input' => $this->input->reveal(), - 'fix' => false, - 'config' => CodeStyleCommand::CONFIG, - 'process_output' => null, - ], + Argument::that(fn(array $context): bool => $this->input->reveal() === $context['input'] + && false === $context['fix'] + && CodeStyleCommand::CONFIG === $context['config'] + && $context['output'] instanceof OutputInterface), )->shouldBeCalled(); self::assertSame(CodeStyleCommand::SUCCESS, $this->executeCommand()); @@ -155,14 +153,12 @@ public function executeWillReturnFailureWhenProcessQueueFails(): void ->shouldBeCalled(); $this->logger->error( 'Code style checks failed.', - [ - 'input' => $this->input->reveal(), - 'file' => null, - 'line' => null, - 'fix' => false, - 'config' => CodeStyleCommand::CONFIG, - 'process_output' => null, - ], + Argument::that(fn(array $context): bool => $this->input->reveal() === $context['input'] + && null === $context['file'] + && null === $context['line'] + && false === $context['fix'] + && CodeStyleCommand::CONFIG === $context['config'] + && $context['output'] instanceof OutputInterface), )->shouldBeCalled(); self::assertSame(CodeStyleCommand::FAILURE, $this->executeCommand()); @@ -190,7 +186,7 @@ public function executeWillCaptureBufferedOutputWhenJsonIsRequested(): void 'info', 'Code style checks completed successfully.', Argument::that(fn(array $context): bool => $this->input->reveal() === $context['input'] - && \is_string($context['process_output'])), + && $context['output'] instanceof OutputInterface), )->shouldBeCalled(); self::assertSame(CodeStyleCommand::SUCCESS, $this->executeCommand()); diff --git a/tests/Console/Command/TestsCommandTest.php b/tests/Console/Command/TestsCommandTest.php index 549192e630..07322725d1 100644 --- a/tests/Console/Command/TestsCommandTest.php +++ b/tests/Console/Command/TestsCommandTest.php @@ -439,7 +439,7 @@ public function executeWillCaptureStructuredPhpUnitSummaryAfterCoveragePreludeWh && isset($context['output']) && 'success' === $context['output']['result'] && 5 === $context['output']['summary']['assertions'] - && 'Generating code coverage report in PHP format ... done [00:00.002]' === $context['output']['raw_output']), + && ! isset($context['output']['raw_output'])), )->shouldBeCalled(); $this->output->writeln(Argument::cetera())->shouldNotBeCalled(); diff --git a/tests/Console/Logger/Processor/CommandOutputProcessorTest.php b/tests/Console/Logger/Processor/CommandOutputProcessorTest.php index 9f88b38dfc..8373872178 100644 --- a/tests/Console/Logger/Processor/CommandOutputProcessorTest.php +++ b/tests/Console/Logger/Processor/CommandOutputProcessorTest.php @@ -190,6 +190,33 @@ public function processWillDiscardPlainTextPreambleBeforeStructuredJsonOutput(): ], $context['output']); } + /** + * @return void + */ + #[Test] + public function processWillDecodeTheFinalStructuredJsonAfterPlainTextToolOutput(): void + { + $processor = new CommandOutputProcessor(); + $output = new BufferedOutput(); + $output->write( + "composer-normalize warning before machine output.\n" + . "Another advisory line.\n" + . "{\"totals\":{\"changed_files\":0,\"errors\":0},\"changed_files\":[\"src/Foo.php\"]}\n" + ); + + $context = $processor->process([ + 'output' => $output, + ]); + + self::assertSame([ + 'totals' => [ + 'changed_files' => 0, + 'errors' => 0, + ], + 'changed_files' => [], + ], $context['output']); + } + /** * @return void */ From c5181d6141b15a715fec73b92eb91638dd1aec81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 19:02:04 -0300 Subject: [PATCH 17/35] [tests] Stabilize structured reporter expectations --- .../ComposerDependencyAnalyserConfig.php | 1 + .../ComposerDependencyAnalyserConfigTest.php | 8 ++++ tests/Console/Command/TestsCommandTest.php | 41 ++++++++++++------- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/src/Config/ComposerDependencyAnalyserConfig.php b/src/Config/ComposerDependencyAnalyserConfig.php index f62f95f4ba..b1044307be 100644 --- a/src/Config/ComposerDependencyAnalyserConfig.php +++ b/src/Config/ComposerDependencyAnalyserConfig.php @@ -53,6 +53,7 @@ final class ComposerDependencyAnalyserConfig * @var array */ public const array DEFAULT_PACKAGED_UNUSED_DEPENDENCIES = [ + 'ergebnis/phpunit-agent-reporter', 'ergebnis/composer-normalize', 'fakerphp/faker', 'fast-forward/phpdoc-bootstrap-template', diff --git a/tests/Config/ComposerDependencyAnalyserConfigTest.php b/tests/Config/ComposerDependencyAnalyserConfigTest.php index 902f086e9e..e2cbbaa1d0 100644 --- a/tests/Config/ComposerDependencyAnalyserConfigTest.php +++ b/tests/Config/ComposerDependencyAnalyserConfigTest.php @@ -111,6 +111,10 @@ static function (Configuration $configuration) use (&$customizeWasCalled): void $configuration->getIgnoreList() ->shouldIgnoreError(ErrorType::UNUSED_DEPENDENCY, null, 'rector/jack') ); + self::assertTrue( + $configuration->getIgnoreList() + ->shouldIgnoreError(ErrorType::UNUSED_DEPENDENCY, null, 'ergebnis/phpunit-agent-reporter') + ); self::assertTrue( $configuration->getIgnoreList() ->shouldIgnoreError(ErrorType::UNUSED_DEPENDENCY, null, 'vendor/custom-package') @@ -123,6 +127,10 @@ static function (Configuration $configuration) use (&$customizeWasCalled): void #[Test] public function itWillExposeReusablePackagedDependencyDefaults(): void { + self::assertContains( + 'ergebnis/phpunit-agent-reporter', + ComposerDependencyAnalyserConfig::DEFAULT_PACKAGED_UNUSED_DEPENDENCIES, + ); self::assertContains( 'rector/jack', ComposerDependencyAnalyserConfig::DEFAULT_PACKAGED_UNUSED_DEPENDENCIES, diff --git a/tests/Console/Command/TestsCommandTest.php b/tests/Console/Command/TestsCommandTest.php index 07322725d1..320c864141 100644 --- a/tests/Console/Command/TestsCommandTest.php +++ b/tests/Console/Command/TestsCommandTest.php @@ -259,11 +259,7 @@ public function executeWillDisablePhpUnitProgressWhenJsonIsRequested(): void ->willReturn(false); $this->processQueue->add( - Argument::that(static fn(Process $process): bool => str_contains( - $process->getCommandLine(), - '--no-progress', - ) && ! str_contains($process->getCommandLine(), '--colors=always') - && 'fast-forward/dev-tools' === $process->getEnv()['AI_AGENT']), + Argument::that(fn(Process $process): bool => $this->usesStructuredPhpUnitExecution($process)), false, false, 'Running PHPUnit Tests' @@ -302,11 +298,7 @@ public function executeWillCaptureStructuredPhpUnitSummaryWhenAgentEnvironmentIs ->willReturn(false); $this->processQueue->add( - Argument::that(static fn(Process $process): bool => str_contains( - $process->getCommandLine(), - '--no-progress', - ) && ! str_contains($process->getCommandLine(), '--colors=always') - && 'fast-forward/dev-tools' === $process->getEnv()['AI_AGENT']), + Argument::that(fn(Process $process): bool => $this->usesStructuredPhpUnitExecution($process)), false, false, 'Running PHPUnit Tests' @@ -376,10 +368,7 @@ public function executeWillKeepPrettyJsonInsideTheStandardCommandLogOutput(): vo ->willReturn(true); $this->processQueue->add( - Argument::that(static fn(Process $process): bool => str_contains( - $process->getCommandLine(), - '--no-progress', - ) && 'fast-forward/dev-tools' === $process->getEnv()['AI_AGENT']), + Argument::that(fn(Process $process): bool => $this->usesStructuredPhpUnitExecution($process)), false, false, 'Running PHPUnit Tests' @@ -745,4 +734,28 @@ private function invokeExecute(): int return (new ReflectionMethod($this->command, 'execute')) ->invoke($this->command, $this->input->reveal(), $this->output->reveal()); } + + /** + * @param Process $process + * + * @return bool + */ + private function usesStructuredPhpUnitExecution(Process $process): bool + { + if (! str_contains($process->getCommandLine(), '--no-progress')) { + return false; + } + + if (str_contains($process->getCommandLine(), '--colors=always')) { + return false; + } + + $processEnvironment = $process->getEnv(); + + if (\array_key_exists('AI_AGENT', $processEnvironment)) { + return 'fast-forward/dev-tools' === $processEnvironment['AI_AGENT']; + } + + return false !== getenv('AI_AGENT'); + } } From 539d5c97565b3b2f472363be758443d2abd61194 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 22:03:01 +0000 Subject: [PATCH 18/35] Update wiki submodule pointer for PR #329 --- .github/wiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/wiki b/.github/wiki index 9639bb75ee..89957be97c 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit 9639bb75ee4cc06d1f0feb0c397b54ef530092e0 +Subproject commit 89957be97c8454cff1a1936a65bacad01b6b503a From 5892d7a3a5978731abfdf0b3ea9cf84d7062e28a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 19:11:33 -0300 Subject: [PATCH 19/35] [console] Centralize non-terminal command logging --- src/Console/Command/AgentsCommand.php | 4 +-- src/Console/Command/CopyResourceCommand.php | 4 +-- src/Console/Command/FundingCommand.php | 18 ++++------ src/Console/Command/GitAttributesCommand.php | 18 ++++------ src/Console/Command/GitHooksCommand.php | 8 ++--- src/Console/Command/LicenseCommand.php | 7 ++-- src/Console/Command/PhpDocCommand.php | 6 ++-- src/Console/Command/SkillsCommand.php | 4 +-- src/Console/Command/SyncCommand.php | 7 ++-- src/Console/Command/TestsCommand.php | 7 +--- .../Command/Traits/LogsCommandResults.php | 33 ++++++++++++++----- .../Console/Command/CodeStyleCommandTest.php | 4 +-- tests/Console/Command/TestsCommandTest.php | 12 +++---- 13 files changed, 65 insertions(+), 67 deletions(-) diff --git a/src/Console/Command/AgentsCommand.php b/src/Console/Command/AgentsCommand.php index d26595ac31..b5365f8bad 100644 --- a/src/Console/Command/AgentsCommand.php +++ b/src/Console/Command/AgentsCommand.php @@ -80,7 +80,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $packageAgentsPath = DevToolsPathResolver::getPackagePath(self::AGENTS_DIRECTORY); $agentsDir = $this->filesystem->getAbsolutePath(self::AGENTS_DIRECTORY); - $this->logger->info('Starting agents synchronization...'); + $this->log('Starting agents synchronization...', $input); if (! $this->filesystem->exists($packageAgentsPath)) { return $this->failure( @@ -99,7 +99,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (! $this->filesystem->exists($agentsDir)) { $this->filesystem->mkdir($agentsDir); $directoryCreated = true; - $this->logger->info('Created .agents/agents directory.'); + $this->log('Created .agents/agents directory.', $input); } $result = $this->synchronizer->synchronize($agentsDir, $packageAgentsPath, self::AGENTS_DIRECTORY); diff --git a/src/Console/Command/CopyResourceCommand.php b/src/Console/Command/CopyResourceCommand.php index 950a485df3..6104d7e5c4 100644 --- a/src/Console/Command/CopyResourceCommand.php +++ b/src/Console/Command/CopyResourceCommand.php @@ -246,10 +246,10 @@ private function copyFile( if (($overwrite || $dryRun || $check || $interactive) && $this->filesystem->exists($targetPath)) { $comparison = $this->fileDiffer->diff($sourcePath, $targetPath); - $this->logger->notice( + $this->notice( $comparison->getSummary(), + $input, [ - 'input' => $input, 'source_path' => $sourcePath, 'target_path' => $targetPath, ], diff --git a/src/Console/Command/FundingCommand.php b/src/Console/Command/FundingCommand.php index 966e487f86..ce72e105c0 100644 --- a/src/Console/Command/FundingCommand.php +++ b/src/Console/Command/FundingCommand.php @@ -133,9 +133,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $check = (bool) $input->getOption('check'); $interactive = (bool) $input->getOption('interactive'); - $this->logger->info('Synchronizing funding metadata...', [ - 'input' => $input, - ]); + $this->log('Synchronizing funding metadata...', $input); if (! $this->filesystem->exists($composerFile)) { $this->notice( @@ -228,22 +226,18 @@ private function handleComposerFile( \sprintf('Updating managed file %s from generated funding metadata synchronization.', $composerFile), ); - $this->logger->notice( - $comparison->getSummary(), - [ - 'input' => $input, - 'composer_file' => $composerFile, - ], - ); + $this->notice($comparison->getSummary(), $input, [ + 'composer_file' => $composerFile, + ]); if ($comparison->isChanged()) { $consoleDiff = $this->fileDiffer->formatForConsole($comparison->getDiff(), $output->isDecorated()); if (null !== $consoleDiff) { - $this->logger->notice( + $this->notice( $consoleDiff, + $input, [ - 'input' => $input, 'composer_file' => $composerFile, 'diff' => $comparison->getDiff(), ], diff --git a/src/Console/Command/GitAttributesCommand.php b/src/Console/Command/GitAttributesCommand.php index fe4fec7f99..3c58b73fdb 100644 --- a/src/Console/Command/GitAttributesCommand.php +++ b/src/Console/Command/GitAttributesCommand.php @@ -135,9 +135,7 @@ protected function configure(): void */ protected function execute(InputInterface $input, OutputInterface $output): int { - $this->logger->info('Synchronizing .gitattributes export-ignore rules...', [ - 'input' => $input, - ]); + $this->log('Synchronizing .gitattributes export-ignore rules...', $input); $dryRun = (bool) $input->getOption('dry-run'); $check = (bool) $input->getOption('check'); $interactive = (bool) $input->getOption('interactive'); @@ -175,22 +173,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int \sprintf('Updating managed file %s from generated .gitattributes synchronization.', $gitattributesPath), ); - $this->logger->notice( - $comparison->getSummary(), - [ - 'input' => $input, - 'gitattributes_path' => $gitattributesPath, - ], - ); + $this->notice($comparison->getSummary(), $input, [ + 'gitattributes_path' => $gitattributesPath, + ]); if ($comparison->isChanged()) { $consoleDiff = $this->fileDiffer->formatForConsole($comparison->getDiff(), $output->isDecorated()); if (null !== $consoleDiff) { - $this->logger->notice( + $this->notice( $consoleDiff, + $input, [ - 'input' => $input, 'gitattributes_path' => $gitattributesPath, 'diff' => $comparison->getDiff(), ], diff --git a/src/Console/Command/GitHooksCommand.php b/src/Console/Command/GitHooksCommand.php index 9b08e94ec5..c8eaf4d95c 100644 --- a/src/Console/Command/GitHooksCommand.php +++ b/src/Console/Command/GitHooksCommand.php @@ -169,10 +169,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int ? $this->fileDiffer->diff($sourcePath, $hookPath) : $this->compareRenderedHookContents($sourcePath, $hookPath, $renderedSourceContents); - $this->logger->notice( + $this->notice( $comparison->getSummary(), + $input, [ - 'input' => $input, 'hook_name' => $file->getFilename(), 'hook_path' => $hookPath, ], @@ -182,10 +182,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $consoleDiff = $this->fileDiffer->formatForConsole($comparison->getDiff(), $output->isDecorated()); if (null !== $consoleDiff) { - $this->logger->notice( + $this->notice( $consoleDiff, + $input, [ - 'input' => $input, 'hook_name' => $file->getFilename(), 'hook_path' => $hookPath, 'diff' => $comparison->getDiff(), diff --git a/src/Console/Command/LicenseCommand.php b/src/Console/Command/LicenseCommand.php index 94314345b6..e3860ed260 100644 --- a/src/Console/Command/LicenseCommand.php +++ b/src/Console/Command/LicenseCommand.php @@ -150,8 +150,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int : \sprintf('Updating managed file %s from generated LICENSE content.', $targetPath), ); - $this->logger->notice($comparison->getSummary(), [ - 'input' => $input, + $this->notice($comparison->getSummary(), $input, [ 'target_path' => $targetPath, ]); @@ -159,10 +158,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $consoleDiff = $this->fileDiffer->formatForConsole($comparison->getDiff(), $output->isDecorated()); if (null !== $consoleDiff) { - $this->logger->notice( + $this->notice( $consoleDiff, + $input, [ - 'input' => $input, 'target_path' => $targetPath, 'diff' => $comparison->getDiff(), ], diff --git a/src/Console/Command/PhpDocCommand.php b/src/Console/Command/PhpDocCommand.php index ae93f35bdd..ae498ec977 100644 --- a/src/Console/Command/PhpDocCommand.php +++ b/src/Console/Command/PhpDocCommand.php @@ -258,8 +258,10 @@ private function ensureDocHeaderExists(InputInterface $input): void try { $this->filesystem->dumpFile(self::FILENAME, $docHeader); } catch (Throwable) { - $this->logger->warning( - 'Skipping .docheader creation because the destination file could not be written.' + $this->log( + 'Skipping .docheader creation because the destination file could not be written.', + $input, + logLevel: LogLevel::WARNING, ); return; diff --git a/src/Console/Command/SkillsCommand.php b/src/Console/Command/SkillsCommand.php index 726bec3e82..72fa23600c 100644 --- a/src/Console/Command/SkillsCommand.php +++ b/src/Console/Command/SkillsCommand.php @@ -112,7 +112,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $packageSkillsPath = DevToolsPathResolver::getPackagePath(self::SKILLS_DIRECTORY); $skillsDir = $this->filesystem->getAbsolutePath(self::SKILLS_DIRECTORY); - $this->logger->info('Starting skills synchronization...'); + $this->log('Starting skills synchronization...', $input); if (! $this->filesystem->exists($packageSkillsPath)) { return $this->failure( @@ -131,7 +131,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (! $this->filesystem->exists($skillsDir)) { $this->filesystem->mkdir($skillsDir); $directoryCreated = true; - $this->logger->info('Created .agents/skills directory.'); + $this->log('Created .agents/skills directory.', $input); } $result = $this->synchronizer->synchronize($skillsDir, $packageSkillsPath, self::SKILLS_DIRECTORY); diff --git a/src/Console/Command/SyncCommand.php b/src/Console/Command/SyncCommand.php index fca5f1c16a..4b99b002ce 100644 --- a/src/Console/Command/SyncCommand.php +++ b/src/Console/Command/SyncCommand.php @@ -160,11 +160,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int ); if ($dryRun || $check || $interactive) { - $this->logger->warning( + $this->log( 'Skipping wiki, skills, and agents during preview/check modes because they do not yet expose non-destructive verification.', - [ - 'input' => $input, - ], + $input, + logLevel: LogLevel::WARNING, ); } else { $this->queueDevToolsCommand(['wiki', '--init'], true, $jsonOutput, $prettyJsonOutput); diff --git a/src/Console/Command/TestsCommand.php b/src/Console/Command/TestsCommand.php index b56f17d009..7549e6c0e9 100644 --- a/src/Console/Command/TestsCommand.php +++ b/src/Console/Command/TestsCommand.php @@ -171,12 +171,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $processOutput = $structuredOutput ? new BufferedOutput() : $output; $cacheEnabled = $this->isCacheEnabled($input); - if (! $structuredOutput) { - $this->getLogger() - ->info('Running PHPUnit tests...', [ - 'input' => $input, - ]); - } + $this->log('Running PHPUnit tests...', $input); try { $minimumCoverage = $this->resolveMinimumCoverage($input); diff --git a/src/Console/Command/Traits/LogsCommandResults.php b/src/Console/Command/Traits/LogsCommandResults.php index 4b99172bed..63e22ace5c 100644 --- a/src/Console/Command/Traits/LogsCommandResults.php +++ b/src/Console/Command/Traits/LogsCommandResults.php @@ -35,27 +35,46 @@ trait LogsCommandResults use HasCommandLogger; /** - * Logs a non-terminal informational message unless structured JSON output is active. + * Logs a non-terminal command message unless structured JSON output is active. * * @param string $message the progress message * @param InputInterface $input the originating command input * @param array $context optional extra log context + * @param string $logLevel the PSR-3 log level used for the message * * @return void */ - private function intermediateInfo(string $message, InputInterface $input, array $context = []): void - { + private function log( + string $message, + InputInterface $input, + array $context = [], + string $logLevel = LogLevel::INFO, + ): void { if (method_exists($this, 'isJsonOutput') && $this->isJsonOutput($input)) { return; } $this->getLogger() - ->info($message, [ + ->log($logLevel, $message, [ 'input' => $input, ...$context, ]); } + /** + * Logs a non-terminal informational message unless structured JSON output is active. + * + * @param string $message the progress message + * @param InputInterface $input the originating command input + * @param array $context optional extra log context + * + * @return void + */ + private function intermediateInfo(string $message, InputInterface $input, array $context = []): void + { + $this->log($message, $input, $context); + } + /** * Logs an informational command message at notice level. * @@ -67,11 +86,7 @@ private function intermediateInfo(string $message, InputInterface $input, array */ private function notice(string $message, InputInterface $input, array $context = []): void { - $this->getLogger() - ->notice($message, [ - 'input' => $input, - ...$context, - ]); + $this->log($message, $input, $context, LogLevel::NOTICE); } /** diff --git a/tests/Console/Command/CodeStyleCommandTest.php b/tests/Console/Command/CodeStyleCommandTest.php index 16ee25ee17..35696e8c21 100644 --- a/tests/Console/Command/CodeStyleCommandTest.php +++ b/tests/Console/Command/CodeStyleCommandTest.php @@ -122,7 +122,7 @@ public function executeWillReturnSuccessWhenProcessQueueSucceeds(): void $this->processQueue->run(Argument::type('object')) ->willReturn(CodeStyleCommand::SUCCESS) ->shouldBeCalled(); - $this->logger->info('Running code style checks and fixes...', Argument::that( + $this->logger->log('info', 'Running code style checks and fixes...', Argument::that( fn(array $context): bool => $this->input->reveal() === $context['input'] )) ->shouldBeCalled(); @@ -147,7 +147,7 @@ public function executeWillReturnFailureWhenProcessQueueFails(): void $this->processQueue->run(Argument::type('object')) ->willReturn(CodeStyleCommand::FAILURE) ->shouldBeCalled(); - $this->logger->info('Running code style checks and fixes...', Argument::that( + $this->logger->log('info', 'Running code style checks and fixes...', Argument::that( fn(array $context): bool => $this->input->reveal() === $context['input'] )) ->shouldBeCalled(); diff --git a/tests/Console/Command/TestsCommandTest.php b/tests/Console/Command/TestsCommandTest.php index 320c864141..572cfd74ea 100644 --- a/tests/Console/Command/TestsCommandTest.php +++ b/tests/Console/Command/TestsCommandTest.php @@ -209,7 +209,7 @@ public function executeWillRunPhpUnitProcessWithConfigFile(): void )->shouldBeCalled(); $this->processQueue->run($this->output->reveal()) ->willReturn(TestsCommand::SUCCESS)->shouldBeCalled(); - $this->logger->info('Running PHPUnit tests...', Argument::that( + $this->logger->log('info', 'Running PHPUnit tests...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); @@ -543,7 +543,7 @@ public function executeWithInvalidMinCoverageWillReturnFailure(): void $this->input->getOption('min-coverage') ->willReturn('invalid'); $this->processQueue->run(Argument::cetera())->shouldNotBeCalled(); - $this->logger->info('Running PHPUnit tests...', Argument::that( + $this->logger->log('info', 'Running PHPUnit tests...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); @@ -566,7 +566,7 @@ public function executeWillSkipWhenNoTestsDirectoryOrPhpSourceExists(): void ->willReturn(new ProjectCapabilities([], null, false, false, false, false)); $this->processQueue->add(Argument::cetera())->shouldNotBeCalled(); $this->processQueue->run(Argument::cetera())->shouldNotBeCalled(); - $this->logger->info('Running PHPUnit tests...', Argument::that( + $this->logger->log('info', 'Running PHPUnit tests...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface ))->shouldBeCalled(); $this->logger->log( @@ -595,7 +595,7 @@ public function executeWillFailWhenCustomTestsPathDoesNotExist(): void ->shouldNotBeCalled(); $this->processQueue->run(Argument::cetera()) ->shouldNotBeCalled(); - $this->logger->info('Running PHPUnit tests...', Argument::that( + $this->logger->log('info', 'Running PHPUnit tests...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface ))->shouldBeCalled(); $this->logger->error( @@ -628,7 +628,7 @@ public function executeWithCoverageBelowMinimumWillReturnFailure(): void )->shouldBeCalled(); $this->processQueue->run($this->output->reveal()) ->willReturn(TestsCommand::SUCCESS)->shouldBeCalled(); - $this->logger->info('Running PHPUnit tests...', Argument::that( + $this->logger->log('info', 'Running PHPUnit tests...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); @@ -710,7 +710,7 @@ public function executeWillReturnFailureWhenCoverageSummaryCannotBeLoaded(): voi )->shouldBeCalled(); $this->processQueue->run($this->output->reveal()) ->willReturn(TestsCommand::SUCCESS)->shouldBeCalled(); - $this->logger->info('Running PHPUnit tests...', Argument::that( + $this->logger->log('info', 'Running PHPUnit tests...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); From 053b739e0c10a1812d87490c1601cdc2790d4dd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 19:14:45 -0300 Subject: [PATCH 20/35] [container] Remove redundant DevTools accessors --- README.md | 4 ++-- bin/dev-tools.php | 3 ++- docs/api/composer-integration.rst | 5 +++-- docs/internals/architecture.rst | 7 ++++--- .../Capability/DevToolsCommandProvider.php | 3 ++- src/Console/DevTools.php | 20 ------------------- tests/Console/DevToolsTest.php | 6 +++--- 7 files changed, 16 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 649b7af582..322c741f7a 100644 --- a/README.md +++ b/README.md @@ -335,8 +335,8 @@ authoring. - `Composer Plugin` - `FastForward\DevTools\Composer\Plugin` exposes the packaged command set to Composer and runs `dev-tools:sync` after install and update. -- `DevTools Container` - `FastForward\DevTools\Console\DevTools::create()` - builds a shared container from `DevToolsServiceProvider`, which wires +- `DevTools Container` - `FastForward\DevTools\Container\ContainerFactory::get(FastForward\DevTools\Console\DevTools::class)` + resolves the shared application from `DevToolsServiceProvider`, which wires process execution, filesystem access, changelog services, Git helpers, diffing, reporting, and template loading. - `Generic Link Synchronization` - diff --git a/bin/dev-tools.php b/bin/dev-tools.php index 2747b30f31..cf0b8bf72d 100644 --- a/bin/dev-tools.php +++ b/bin/dev-tools.php @@ -20,6 +20,7 @@ namespace FastForward\DevTools; use FastForward\DevTools\Console\DevTools; +use FastForward\DevTools\Container\ContainerFactory; $autoloadCandidates = [\dirname(__DIR__, 4) . '/vendor/autoload.php', \dirname(__DIR__) . '/vendor/autoload.php']; @@ -27,7 +28,7 @@ if (is_file($autoloadCandidate)) { require_once $autoloadCandidate; - exit(DevTools::create()->run()); + exit(ContainerFactory::get(DevTools::class)->run()); } } diff --git a/docs/api/composer-integration.rst b/docs/api/composer-integration.rst index d08a0bbce5..9aa33d7348 100644 --- a/docs/api/composer-integration.rst +++ b/docs/api/composer-integration.rst @@ -12,8 +12,9 @@ Startup Chain ``vendor/autoload.php`` and falls back to the package autoloader. 3. ``bin/dev-tools.php`` starts ``FastForward\DevTools\Console\DevTools`` and appends ``--no-plugins``. -4. ``FastForward\DevTools\Console\DevTools::create()`` builds the shared - container from ``FastForward\DevTools\ServiceProvider\DevToolsServiceProvider``. +4. ``FastForward\DevTools\Container\ContainerFactory::get(FastForward\DevTools\Console\DevTools::class)`` + resolves the shared application container from + ``FastForward\DevTools\ServiceProvider\DevToolsServiceProvider``. 5. ``FastForward\DevTools\Console\CommandLoader\DevToolsCommandLoader`` lazily discovers ``#[AsCommand]`` classes from the command namespace. 6. ``FastForward\DevTools\Composer\Capability\DevToolsCommandProvider`` later diff --git a/docs/internals/architecture.rst b/docs/internals/architecture.rst index d7c679d256..666be893c4 100644 --- a/docs/internals/architecture.rst +++ b/docs/internals/architecture.rst @@ -10,8 +10,9 @@ Local Command Lifecycle 1. ``bin/dev-tools`` loads ``bin/dev-tools.php``. 2. ``bin/dev-tools.php`` prefers the consumer ``vendor/autoload.php`` and falls back to the package autoloader. -3. ``FastForward\DevTools\Console\DevTools::create()`` builds a shared - container from ``FastForward\DevTools\ServiceProvider\DevToolsServiceProvider``. +3. ``FastForward\DevTools\Container\ContainerFactory::get(FastForward\DevTools\Console\DevTools::class)`` + resolves the shared application container from + ``FastForward\DevTools\ServiceProvider\DevToolsServiceProvider``. 4. ``FastForward\DevTools\Console\CommandLoader\DevToolsCommandLoader`` lazily discovers ``#[AsCommand]`` classes and resolves them from that container. @@ -61,7 +62,7 @@ Dependency Injection -------------------- ``DevToolsServiceProvider`` builds the shared application container used by -``DevTools::create()``. Most commands receive collaborators through +``ContainerFactory::get(DevTools::class)``. Most commands receive collaborators through constructor injection once resolved by that container, while command discovery itself stays lazy through ``DevToolsCommandLoader``. diff --git a/src/Composer/Capability/DevToolsCommandProvider.php b/src/Composer/Capability/DevToolsCommandProvider.php index d8014ca62b..105d49f508 100644 --- a/src/Composer/Capability/DevToolsCommandProvider.php +++ b/src/Composer/Capability/DevToolsCommandProvider.php @@ -23,6 +23,7 @@ use FastForward\DevTools\Composer\Command\ProxyCommand; use FastForward\DevTools\Composer\DevToolsPluginInterface; use FastForward\DevTools\Console\DevTools; +use FastForward\DevTools\Container\ContainerFactory; use Symfony\Component\Console\Command\Command; /** @@ -54,7 +55,7 @@ public function getCommands() { $commands = []; - foreach (DevTools::create()->all() as $registeredName => $command) { + foreach (ContainerFactory::get(DevTools::class)->all() as $registeredName => $command) { /** * Composer plugin registrations must be canonicalized to one command per Symfony command. * The application exposes alias keys in `all()`, but Composer interprets each entry as diff --git a/src/Console/DevTools.php b/src/Console/DevTools.php index 70621bce3c..7360b1a14e 100644 --- a/src/Console/DevTools.php +++ b/src/Console/DevTools.php @@ -20,7 +20,6 @@ namespace FastForward\DevTools\Console; use FastForward\DevTools\Console\Command\SelfUpdateCommand; -use FastForward\DevTools\Container\ContainerFactory; use FastForward\DevTools\Environment\EnvironmentInterface; use FastForward\DevTools\Environment\RuntimeEnvironmentInterface; use FastForward\DevTools\Path\ManagedWorkspace; @@ -29,7 +28,6 @@ use FastForward\DevTools\SelfUpdate\VersionCheckNotifierInterface; use FastForward\DevTools\SelfUpdate\WorkingDirectorySwitcherInterface; use Override; -use Psr\Container\ContainerInterface; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; @@ -164,24 +162,6 @@ public function doRun(InputInterface $input, OutputInterface $output): int return parent::doRun($input, $output); } - /** - * Create DevTools instance from container. - * - * @return DevTools - */ - public static function create(): self - { - return ContainerFactory::get(self::class); - } - - /** - * Retrieves the shared DevTools service container. - */ - public static function getContainer(): ContainerInterface - { - return ContainerFactory::create(); - } - /** * Resolves the raw working-directory option before command parsing. * diff --git a/tests/Console/DevToolsTest.php b/tests/Console/DevToolsTest.php index b71bcbed57..d8511670ab 100644 --- a/tests/Console/DevToolsTest.php +++ b/tests/Console/DevToolsTest.php @@ -506,14 +506,14 @@ public function __construct() * @return void */ #[Test] - public function createWillReturnInstanceOfDevTools(): void + public function containerFactoryWillReturnASharedDevToolsInstance(): void { ContainerFactory::reset(); - $devTools = DevTools::create(); + $devTools = ContainerFactory::get(DevTools::class); self::assertInstanceOf(DevTools::class, $devTools); - self::assertSame($devTools, DevTools::create()); + self::assertSame($devTools, ContainerFactory::get(DevTools::class)); } /** From 7b9a497759a4c6e535f46a990e7ee4e81502a1cc Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 22:15:55 +0000 Subject: [PATCH 21/35] Update wiki submodule pointer for PR #329 --- .github/wiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/wiki b/.github/wiki index 89957be97c..2eeea15fa3 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit 89957be97c8454cff1a1936a65bacad01b6b503a +Subproject commit 2eeea15fa35d6f940fd2e2ac73b8284cb896f0aa From 8fcb3379ce998d10adea91889d0b9cb1f7d0bd15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 19:18:23 -0300 Subject: [PATCH 22/35] [console] Remove intermediateInfo helper --- src/Console/Command/CodeStyleCommand.php | 2 +- src/Console/Command/DependenciesCommand.php | 2 +- src/Console/Command/DocsCommand.php | 2 +- src/Console/Command/MetricsCommand.php | 2 +- src/Console/Command/PhpDocCommand.php | 4 ++-- src/Console/Command/RefactorCommand.php | 2 +- src/Console/Command/ReportsCommand.php | 2 +- src/Console/Command/StandardsCommand.php | 2 +- src/Console/Command/SyncCommand.php | 2 +- src/Console/Command/Traits/LogsCommandResults.php | 14 -------------- src/Console/Command/WikiCommand.php | 2 +- 11 files changed, 11 insertions(+), 25 deletions(-) diff --git a/src/Console/Command/CodeStyleCommand.php b/src/Console/Command/CodeStyleCommand.php index ef3b600abf..87fbbeaf0e 100644 --- a/src/Console/Command/CodeStyleCommand.php +++ b/src/Console/Command/CodeStyleCommand.php @@ -121,7 +121,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $fix = (bool) $input->getOption('fix'); $progress = ! $jsonOutput && (bool) $input->getOption('progress'); - $this->intermediateInfo('Running code style checks and fixes...', $input); + $this->log('Running code style checks and fixes...', $input); $composerUpdate = $this->processBuilder ->withArgument('--lock') diff --git a/src/Console/Command/DependenciesCommand.php b/src/Console/Command/DependenciesCommand.php index 8f8a7c81a0..d816421c0c 100644 --- a/src/Console/Command/DependenciesCommand.php +++ b/src/Console/Command/DependenciesCommand.php @@ -150,7 +150,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int ); } - $this->intermediateInfo('Running dependency analysis...', $input); + $this->log('Running dependency analysis...', $input); $this->processQueue->add( process: $this->getComposerDependencyAnalyserCommand($input), diff --git a/src/Console/Command/DocsCommand.php b/src/Console/Command/DocsCommand.php index 5f80e61409..7dc15469b1 100644 --- a/src/Console/Command/DocsCommand.php +++ b/src/Console/Command/DocsCommand.php @@ -155,7 +155,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $template = DevToolsPathResolver::getPreferredVendorPath(self::DEFAULT_TEMPLATE); } - $this->intermediateInfo('Generating API documentation...', $input); + $this->log('Generating API documentation...', $input); if ( ! $projectCapabilities->hasGuideDirectory() diff --git a/src/Console/Command/MetricsCommand.php b/src/Console/Command/MetricsCommand.php index a346def444..88660e6c2e 100644 --- a/src/Console/Command/MetricsCommand.php +++ b/src/Console/Command/MetricsCommand.php @@ -135,7 +135,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $exclude = (string) $input->getOption('exclude'); $junit = $input->getOption('junit'); - $this->intermediateInfo('Running code metrics analysis...', $input); + $this->log('Running code metrics analysis...', $input); $processBuilder = $this->processBuilder ->withArgument('--ansi') diff --git a/src/Console/Command/PhpDocCommand.php b/src/Console/Command/PhpDocCommand.php index ae498ec977..1bce7f5a07 100644 --- a/src/Console/Command/PhpDocCommand.php +++ b/src/Console/Command/PhpDocCommand.php @@ -152,7 +152,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $progress = ! $jsonOutput && (bool) $input->getOption('progress'); $cacheEnabled = $this->isCacheEnabled($input); - $this->intermediateInfo('Checking and fixing PHPDocs...', $input); + $this->log('Checking and fixing PHPDocs...', $input); $this->ensureDocHeaderExists($input); @@ -267,6 +267,6 @@ private function ensureDocHeaderExists(InputInterface $input): void return; } - $this->intermediateInfo('Created .docheader from repository template.', $input); + $this->log('Created .docheader from repository template.', $input); } } diff --git a/src/Console/Command/RefactorCommand.php b/src/Console/Command/RefactorCommand.php index 6eca3fb806..c9f6dc7c25 100644 --- a/src/Console/Command/RefactorCommand.php +++ b/src/Console/Command/RefactorCommand.php @@ -120,7 +120,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $fix = (bool) $input->getOption('fix'); $progress = ! $jsonOutput && (bool) $input->getOption('progress'); - $this->intermediateInfo('Running Rector for code refactoring...', $input); + $this->log('Running Rector for code refactoring...', $input); $processBuilder = $this->processBuilder ->withArgument('--ansi') diff --git a/src/Console/Command/ReportsCommand.php b/src/Console/Command/ReportsCommand.php index 5a4e1edbc3..d618a01c45 100644 --- a/src/Console/Command/ReportsCommand.php +++ b/src/Console/Command/ReportsCommand.php @@ -129,7 +129,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $coveragePath = (string) $input->getOption('coverage'); $metricsPath = (string) $input->getOption('metrics'); - $this->intermediateInfo('Generating frontpage for Fast Forward documentation...', $input); + $this->log('Generating frontpage for Fast Forward documentation...', $input); $docsBuilder = $this->processBuilder ->withArgument('--target', $target); diff --git a/src/Console/Command/StandardsCommand.php b/src/Console/Command/StandardsCommand.php index 62237a0a40..3331039d67 100644 --- a/src/Console/Command/StandardsCommand.php +++ b/src/Console/Command/StandardsCommand.php @@ -109,7 +109,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $commands = []; $fix = (bool) $input->getOption('fix'); - $this->intermediateInfo('Running code standards checks...', $input); + $this->log('Running code standards checks...', $input); foreach (['refactor', 'phpdoc', 'code-style', 'reports'] as $command) { $commands[] = $command; diff --git a/src/Console/Command/SyncCommand.php b/src/Console/Command/SyncCommand.php index 4b99b002ce..d26769b4b1 100644 --- a/src/Console/Command/SyncCommand.php +++ b/src/Console/Command/SyncCommand.php @@ -112,7 +112,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int ]; $allowDetached = ! $dryRun && ! $check && ! $interactive; - $this->intermediateInfo('Starting dev-tools synchronization...', $input); + $this->log('Starting dev-tools synchronization...', $input); $this->queueDevToolsCommand(['update-composer-json', ...$modeArguments], false, $jsonOutput, $prettyJsonOutput); $this->queueDevToolsCommand(['funding', ...$modeArguments], false, $jsonOutput, $prettyJsonOutput); diff --git a/src/Console/Command/Traits/LogsCommandResults.php b/src/Console/Command/Traits/LogsCommandResults.php index 63e22ace5c..b261792f2c 100644 --- a/src/Console/Command/Traits/LogsCommandResults.php +++ b/src/Console/Command/Traits/LogsCommandResults.php @@ -61,20 +61,6 @@ private function log( ]); } - /** - * Logs a non-terminal informational message unless structured JSON output is active. - * - * @param string $message the progress message - * @param InputInterface $input the originating command input - * @param array $context optional extra log context - * - * @return void - */ - private function intermediateInfo(string $message, InputInterface $input, array $context = []): void - { - $this->log($message, $input, $context); - } - /** * Logs an informational command message at notice level. * diff --git a/src/Console/Command/WikiCommand.php b/src/Console/Command/WikiCommand.php index 323c71efbc..33f616fd5e 100644 --- a/src/Console/Command/WikiCommand.php +++ b/src/Console/Command/WikiCommand.php @@ -140,7 +140,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return $this->initializeWikiSubmodule($input, $target, $processOutput); } - $this->intermediateInfo('Generating wiki documentation...', $input); + $this->log('Generating wiki documentation...', $input); $projectCapabilities = $this->projectCapabilitiesResolver->resolve(wikiTarget: $target); From 35380c1c4cd947bd22e518f0b1dfeed5bd5a5874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 19:20:06 -0300 Subject: [PATCH 23/35] [console] Remove unused notice helper --- src/Console/Command/Traits/LogsCommandResults.php | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/Console/Command/Traits/LogsCommandResults.php b/src/Console/Command/Traits/LogsCommandResults.php index b261792f2c..7eda868631 100644 --- a/src/Console/Command/Traits/LogsCommandResults.php +++ b/src/Console/Command/Traits/LogsCommandResults.php @@ -61,20 +61,6 @@ private function log( ]); } - /** - * Logs an informational command message at notice level. - * - * @param string $message the notice message - * @param InputInterface $input the originating command input - * @param array $context optional extra log context - * - * @return void - */ - private function notice(string $message, InputInterface $input, array $context = []): void - { - $this->log($message, $input, $context, LogLevel::NOTICE); - } - /** * Logs a successful command result and returns the success exit code. * From bdaf69330e74c2c8a78b59eb39633637acbe813f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 19:52:23 -0300 Subject: [PATCH 24/35] Refine structured command logging --- src/Console/Command/AgentsCommand.php | 3 - src/Console/Command/ChangelogCheckCommand.php | 3 - src/Console/Command/ChangelogEntryCommand.php | 3 - .../Command/ChangelogNextVersionCommand.php | 3 - .../Command/ChangelogPromoteCommand.php | 3 - src/Console/Command/ChangelogShowCommand.php | 3 - src/Console/Command/CodeOwnersCommand.php | 10 ++-- src/Console/Command/CodeStyleCommand.php | 3 - src/Console/Command/CopyResourceCommand.php | 25 ++++---- src/Console/Command/DependenciesCommand.php | 3 - src/Console/Command/DocsCommand.php | 13 ++--- src/Console/Command/FundingCommand.php | 47 +++++++-------- src/Console/Command/GitAttributesCommand.php | 26 +++++---- src/Console/Command/GitHooksCommand.php | 23 ++++---- src/Console/Command/GitIgnoreCommand.php | 14 ++--- src/Console/Command/LicenseCommand.php | 24 ++++---- src/Console/Command/MetricsCommand.php | 3 - src/Console/Command/PhpDocCommand.php | 4 +- src/Console/Command/RefactorCommand.php | 3 - src/Console/Command/ReportsCommand.php | 8 +-- src/Console/Command/SelfUpdateCommand.php | 6 +- src/Console/Command/SkillsCommand.php | 3 - src/Console/Command/StandardsCommand.php | 3 - src/Console/Command/SyncCommand.php | 4 +- .../Command/Traits/HasCommandLogger.php | 39 +++---------- .../Command/Traits/LogsCommandResults.php | 16 ++++-- .../Command/UpdateComposerJsonCommand.php | 11 ++-- src/Console/Command/WikiCommand.php | 3 - src/Console/Input/HasJsonOption.php | 20 ++++++- .../Processor/CommandInputProcessor.php | 17 +++++- tests/Console/Command/AgentsCommandTest.php | 25 ++++++-- .../Command/ChangelogCheckCommandTest.php | 9 ++- .../Command/ChangelogEntryCommandTest.php | 9 ++- .../ChangelogNextVersionCommandTest.php | 4 +- .../Command/ChangelogPromoteCommandTest.php | 9 ++- .../Command/ChangelogShowCommandTest.php | 4 +- .../Console/Command/CodeOwnersCommandTest.php | 9 ++- .../Console/Command/CodeStyleCommandTest.php | 8 ++- .../Command/CopyResourceCommandTest.php | 9 ++- .../Command/DependenciesCommandTest.php | 8 +-- tests/Console/Command/DocsCommandTest.php | 4 +- tests/Console/Command/FundingCommandTest.php | 9 ++- .../Command/GitAttributesCommandTest.php | 9 ++- tests/Console/Command/GitHooksCommandTest.php | 9 ++- .../Console/Command/GitIgnoreCommandTest.php | 9 ++- tests/Console/Command/LicenseCommandTest.php | 9 ++- tests/Console/Command/MetricsCommandTest.php | 5 +- tests/Console/Command/PhpDocCommandTest.php | 10 +++- tests/Console/Command/RefactorCommandTest.php | 4 +- tests/Console/Command/ReportsCommandTest.php | 4 +- .../Console/Command/SelfUpdateCommandTest.php | 8 +++ tests/Console/Command/SkillsCommandTest.php | 25 ++++++-- .../Console/Command/StandardsCommandTest.php | 4 +- tests/Console/Command/SyncCommandTest.php | 5 +- tests/Console/Command/TestsCommandTest.php | 12 ++-- .../Command/UpdateComposerJsonCommandTest.php | 9 ++- tests/Console/Command/WikiCommandTest.php | 4 +- tests/Container/UsesContainerFactory.php | 57 +++++++++++++++++++ 58 files changed, 394 insertions(+), 242 deletions(-) create mode 100644 tests/Container/UsesContainerFactory.php diff --git a/src/Console/Command/AgentsCommand.php b/src/Console/Command/AgentsCommand.php index b5365f8bad..c4bdce0855 100644 --- a/src/Console/Command/AgentsCommand.php +++ b/src/Console/Command/AgentsCommand.php @@ -24,7 +24,6 @@ use FastForward\DevTools\Filesystem\FilesystemInterface; use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Sync\PackagedDirectorySynchronizer; -use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -48,12 +47,10 @@ final class AgentsCommand extends Command /** * @param PackagedDirectorySynchronizer $synchronizer * @param FilesystemInterface $filesystem - * @param LoggerInterface $logger */ public function __construct( private readonly PackagedDirectorySynchronizer $synchronizer, private readonly FilesystemInterface $filesystem, - private readonly LoggerInterface $logger, ) { parent::__construct(); } diff --git a/src/Console/Command/ChangelogCheckCommand.php b/src/Console/Command/ChangelogCheckCommand.php index eaebc859b7..c9d09aa1cb 100644 --- a/src/Console/Command/ChangelogCheckCommand.php +++ b/src/Console/Command/ChangelogCheckCommand.php @@ -23,7 +23,6 @@ use FastForward\DevTools\Changelog\Checker\UnreleasedEntryCheckerInterface; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FilesystemInterface; -use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -45,12 +44,10 @@ final class ChangelogCheckCommand extends Command /** * @param FilesystemInterface $filesystem * @param UnreleasedEntryCheckerInterface $unreleasedEntryChecker - * @param LoggerInterface $logger */ public function __construct( private readonly FilesystemInterface $filesystem, private readonly UnreleasedEntryCheckerInterface $unreleasedEntryChecker, - private readonly LoggerInterface $logger, ) { parent::__construct(); } diff --git a/src/Console/Command/ChangelogEntryCommand.php b/src/Console/Command/ChangelogEntryCommand.php index 7de3a385fe..5808d8da2d 100644 --- a/src/Console/Command/ChangelogEntryCommand.php +++ b/src/Console/Command/ChangelogEntryCommand.php @@ -25,7 +25,6 @@ use FastForward\DevTools\Changelog\Manager\ChangelogManagerInterface; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FilesystemInterface; -use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -48,12 +47,10 @@ final class ChangelogEntryCommand extends Command /** * @param FilesystemInterface $filesystem * @param ChangelogManagerInterface $changelogManager - * @param LoggerInterface $logger */ public function __construct( private readonly FilesystemInterface $filesystem, private readonly ChangelogManagerInterface $changelogManager, - private readonly LoggerInterface $logger, ) { parent::__construct(); } diff --git a/src/Console/Command/ChangelogNextVersionCommand.php b/src/Console/Command/ChangelogNextVersionCommand.php index 18b9400702..3d3a84f24a 100644 --- a/src/Console/Command/ChangelogNextVersionCommand.php +++ b/src/Console/Command/ChangelogNextVersionCommand.php @@ -24,7 +24,6 @@ use FastForward\DevTools\Changelog\Manager\ChangelogManagerInterface; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FilesystemInterface; -use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -46,12 +45,10 @@ final class ChangelogNextVersionCommand extends Command /** * @param FilesystemInterface $filesystem * @param ChangelogManagerInterface $changelogManager - * @param LoggerInterface $logger */ public function __construct( private readonly FilesystemInterface $filesystem, private readonly ChangelogManagerInterface $changelogManager, - private readonly LoggerInterface $logger, ) { parent::__construct(); } diff --git a/src/Console/Command/ChangelogPromoteCommand.php b/src/Console/Command/ChangelogPromoteCommand.php index 6425f07159..e4a6a0a9b8 100644 --- a/src/Console/Command/ChangelogPromoteCommand.php +++ b/src/Console/Command/ChangelogPromoteCommand.php @@ -25,7 +25,6 @@ use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FilesystemInterface; use Psr\Clock\ClockInterface; -use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -49,13 +48,11 @@ final class ChangelogPromoteCommand extends Command * @param FilesystemInterface $filesystem * @param ChangelogManagerInterface $changelogManager * @param ClockInterface $clock - * @param LoggerInterface $logger */ public function __construct( private readonly FilesystemInterface $filesystem, private readonly ChangelogManagerInterface $changelogManager, private readonly ClockInterface $clock, - private readonly LoggerInterface $logger, ) { parent::__construct(); } diff --git a/src/Console/Command/ChangelogShowCommand.php b/src/Console/Command/ChangelogShowCommand.php index dbc54c59e8..44f2a45afe 100644 --- a/src/Console/Command/ChangelogShowCommand.php +++ b/src/Console/Command/ChangelogShowCommand.php @@ -24,7 +24,6 @@ use FastForward\DevTools\Changelog\Manager\ChangelogManagerInterface; use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Filesystem\FilesystemInterface; -use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -44,12 +43,10 @@ final class ChangelogShowCommand extends Command /** * @param FilesystemInterface $filesystem * @param ChangelogManagerInterface $changelogManager - * @param LoggerInterface $logger */ public function __construct( private readonly FilesystemInterface $filesystem, private readonly ChangelogManagerInterface $changelogManager, - private readonly LoggerInterface $logger, ) { parent::__construct(); } diff --git a/src/Console/Command/CodeOwnersCommand.php b/src/Console/Command/CodeOwnersCommand.php index 669109f137..3734d55dda 100644 --- a/src/Console/Command/CodeOwnersCommand.php +++ b/src/Console/Command/CodeOwnersCommand.php @@ -24,7 +24,6 @@ use FastForward\DevTools\CodeOwners\CodeOwnersGenerator; use FastForward\DevTools\Filesystem\FilesystemInterface; use FastForward\DevTools\Resource\FileDiffer; -use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -53,14 +52,12 @@ final class CodeOwnersCommand extends Command * @param CodeOwnersGenerator $generator the generator used to infer and render CODEOWNERS contents * @param FilesystemInterface $filesystem the filesystem used to read and write the target file * @param FileDiffer $fileDiffer the differ used to report managed-file drift - * @param LoggerInterface $logger the output-aware logger * @param SymfonyStyle $io the SymfonyStyle instance for interactive prompts */ public function __construct( private readonly CodeOwnersGenerator $generator, private readonly FilesystemInterface $filesystem, private readonly FileDiffer $fileDiffer, - private readonly LoggerInterface $logger, private readonly SymfonyStyle $io, ) { parent::__construct(); @@ -153,21 +150,22 @@ protected function execute(InputInterface $input, OutputInterface $output): int : \sprintf('Updating managed file %s from generated CODEOWNERS content.', $targetPath), ); - $this->notice($comparison->getSummary(), $input, [ + $this->log($comparison->getSummary(), $input, [ 'target_path' => $targetPath, - ]); + ], LogLevel::NOTICE); if ($comparison->isChanged()) { $consoleDiff = $this->fileDiffer->formatForConsole($comparison->getDiff(), $output->isDecorated()); if (null !== $consoleDiff) { - $this->notice( + $this->log( $consoleDiff, $input, [ 'target_path' => $targetPath, 'diff' => $comparison->getDiff(), ], + LogLevel::NOTICE, ); } } diff --git a/src/Console/Command/CodeStyleCommand.php b/src/Console/Command/CodeStyleCommand.php index 87fbbeaf0e..f3953680ef 100644 --- a/src/Console/Command/CodeStyleCommand.php +++ b/src/Console/Command/CodeStyleCommand.php @@ -24,7 +24,6 @@ use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; -use Psr\Log\LoggerInterface; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -63,13 +62,11 @@ final class CodeStyleCommand extends Command * @param FileLocatorInterface $fileLocator locates the configuration file required by EasyCodingStandard * @param ProcessBuilderInterface $processBuilder builds the process instances used to execute Composer and ECS commands * @param ProcessQueueInterface $processQueue queues and executes the generated processes in the required order - * @param LoggerInterface $logger logs command feedback */ public function __construct( private readonly FileLocatorInterface $fileLocator, private readonly ProcessBuilderInterface $processBuilder, private readonly ProcessQueueInterface $processQueue, - private readonly LoggerInterface $logger, ) { parent::__construct(); } diff --git a/src/Console/Command/CopyResourceCommand.php b/src/Console/Command/CopyResourceCommand.php index 6104d7e5c4..26a32c821c 100644 --- a/src/Console/Command/CopyResourceCommand.php +++ b/src/Console/Command/CopyResourceCommand.php @@ -24,7 +24,6 @@ use FastForward\DevTools\Filesystem\FinderFactoryInterface; use FastForward\DevTools\Filesystem\FilesystemInterface; use FastForward\DevTools\Resource\FileDiffer; -use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; @@ -56,7 +55,6 @@ final class CopyResourceCommand extends Command * @param FileLocatorInterface $fileLocator the locator used to resolve source resources * @param FinderFactoryInterface $finderFactory the factory used to create finders for directory resources * @param FileDiffer $fileDiffer the service used to summarize overwrite changes - * @param LoggerInterface $logger the output-aware logger * @param SymfonyStyle $io the input/output service used to interact with the user */ public function __construct( @@ -64,7 +62,6 @@ public function __construct( private readonly FileLocatorInterface $fileLocator, private readonly FinderFactoryInterface $finderFactory, private readonly FileDiffer $fileDiffer, - private readonly LoggerInterface $logger, private readonly SymfonyStyle $io, ) { parent::__construct(); @@ -162,11 +159,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int * @param string $sourcePath the resolved source directory * @param string $targetPath the resolved target directory * @param bool $overwrite whether existing files MAY be overwritten + * @param bool $dryRun whether the command is previewing changes only + * @param bool $check whether the command SHOULD fail on detected drift + * @param bool $interactive whether the command SHOULD prompt before overwriting drifted files + * @param InputInterface $input the originating command input * @param OutputInterface $output the output used to report copy results - * @param bool $dryRun - * @param bool $check - * @param bool $interactive - * @param InputInterface $input * * @return int the command status code */ @@ -213,11 +210,11 @@ private function copyDirectory( * @param string $sourcePath the resolved source file * @param string $targetPath the resolved target file * @param bool $overwrite whether an existing target file MAY be overwritten + * @param bool $dryRun whether the command is previewing changes only + * @param bool $check whether the command SHOULD fail on detected drift + * @param bool $interactive whether the command SHOULD prompt before overwriting drifted files + * @param InputInterface $input the originating command input * @param OutputInterface $output the output used to report copy results - * @param bool $dryRun - * @param bool $check - * @param bool $interactive - * @param InputInterface $input * * @return int the command status code */ @@ -246,20 +243,21 @@ private function copyFile( if (($overwrite || $dryRun || $check || $interactive) && $this->filesystem->exists($targetPath)) { $comparison = $this->fileDiffer->diff($sourcePath, $targetPath); - $this->notice( + $this->log( $comparison->getSummary(), $input, [ 'source_path' => $sourcePath, 'target_path' => $targetPath, ], + LogLevel::NOTICE, ); if ($comparison->isChanged()) { $consoleDiff = $this->fileDiffer->formatForConsole($comparison->getDiff(), $output->isDecorated()); if (null !== $consoleDiff) { - $this->notice( + $this->log( $consoleDiff, $input, [ @@ -267,6 +265,7 @@ private function copyFile( 'target_path' => $targetPath, 'diff' => $comparison->getDiff(), ], + LogLevel::NOTICE, ); } } diff --git a/src/Console/Command/DependenciesCommand.php b/src/Console/Command/DependenciesCommand.php index d816421c0c..4f4e5c1077 100644 --- a/src/Console/Command/DependenciesCommand.php +++ b/src/Console/Command/DependenciesCommand.php @@ -26,7 +26,6 @@ use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use InvalidArgumentException; -use Psr\Log\LoggerInterface; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -61,13 +60,11 @@ final class DependenciesCommand extends Command * @param ProcessBuilderInterface $processBuilder creates analyzer and upgrade processes * @param ProcessQueueInterface $processQueue executes queued processes * @param FileLocatorInterface $fileLocator resolves the dependency analyser configuration - * @param LoggerInterface $logger writes command feedback */ public function __construct( private readonly ProcessBuilderInterface $processBuilder, private readonly ProcessQueueInterface $processQueue, private readonly FileLocatorInterface $fileLocator, - private readonly LoggerInterface $logger, ) { return parent::__construct(); } diff --git a/src/Console/Command/DocsCommand.php b/src/Console/Command/DocsCommand.php index 7dc15469b1..e718cbebff 100644 --- a/src/Console/Command/DocsCommand.php +++ b/src/Console/Command/DocsCommand.php @@ -31,7 +31,6 @@ use FastForward\DevTools\Path\ManagedWorkspace; use FastForward\DevTools\Project\ProjectCapabilities; use FastForward\DevTools\Project\ProjectCapabilitiesResolverInterface; -use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -71,11 +70,10 @@ final class DocsCommand extends Command * * @param ProcessBuilderInterface $processBuilder the process builder for executing phpDocumentor * @param ProcessQueueInterface $processQueue the process queue for managing execution - * @param Environment $renderer + * @param Environment $renderer renders phpDocumentor configuration templates * @param FilesystemInterface $filesystem the filesystem for handling file operations * @param ComposerJsonInterface $composer the composer.json handler for accessing project metadata * @param ProjectCapabilitiesResolverInterface $projectCapabilitiesResolver the project capability resolver - * @param LoggerInterface $logger the output-aware logger */ public function __construct( private readonly ProcessBuilderInterface $processBuilder, @@ -84,7 +82,6 @@ public function __construct( private readonly FilesystemInterface $filesystem, private readonly ComposerJsonInterface $composer, private readonly ProjectCapabilitiesResolverInterface $projectCapabilitiesResolver, - private readonly LoggerInterface $logger, ) { parent::__construct(); } @@ -130,12 +127,10 @@ protected function configure(): void } /** - * Generates the HTML API documentation for the configured source tree. + * Generates API documentation for the configured project surface. * - * @param InputInterface $input the input details for the command - * @param OutputInterface $output the output mechanism for logging - * - * @return int the final execution status code + * @param InputInterface $input + * @param OutputInterface $output */ protected function execute(InputInterface $input, OutputInterface $output): int { diff --git a/src/Console/Command/FundingCommand.php b/src/Console/Command/FundingCommand.php index ce72e105c0..088e4560d0 100644 --- a/src/Console/Command/FundingCommand.php +++ b/src/Console/Command/FundingCommand.php @@ -28,7 +28,7 @@ use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use FastForward\DevTools\Resource\FileDiffer; -use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -60,7 +60,6 @@ final class FundingCommand extends Command * @param FileDiffer $fileDiffer the differ used to summarize managed-file drift * @param ProcessBuilderInterface $processBuilder the process builder used to normalize composer.json after updates * @param ProcessQueueInterface $processQueue the process queue used to execute composer normalize - * @param LoggerInterface $logger the output-aware logger * @param SymfonyStyle $io the input/output service used to interact with the user */ public function __construct( @@ -71,7 +70,6 @@ public function __construct( private readonly FileDiffer $fileDiffer, private readonly ProcessBuilderInterface $processBuilder, private readonly ProcessQueueInterface $processQueue, - private readonly LoggerInterface $logger, private readonly SymfonyStyle $io, ) { parent::__construct(); @@ -136,13 +134,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->log('Synchronizing funding metadata...', $input); if (! $this->filesystem->exists($composerFile)) { - $this->notice( + $this->log( 'Composer file {composer_file} does not exist. Skipping funding synchronization.', $input, [ 'composer_file' => $composerFile, 'funding_file' => $fundingFile, ], + LogLevel::NOTICE, ); return $this->success( @@ -152,7 +151,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'composer_file' => $composerFile, 'funding_file' => $fundingFile, ], - 'notice', + LogLevel::NOTICE, ); } @@ -226,21 +225,22 @@ private function handleComposerFile( \sprintf('Updating managed file %s from generated funding metadata synchronization.', $composerFile), ); - $this->notice($comparison->getSummary(), $input, [ + $this->log($comparison->getSummary(), $input, [ 'composer_file' => $composerFile, - ]); + ], LogLevel::NOTICE); if ($comparison->isChanged()) { $consoleDiff = $this->fileDiffer->formatForConsole($comparison->getDiff(), $output->isDecorated()); if (null !== $consoleDiff) { - $this->notice( + $this->log( $consoleDiff, $input, [ 'composer_file' => $composerFile, 'diff' => $comparison->getDiff(), ], + LogLevel::NOTICE, ); } } @@ -273,14 +273,14 @@ private function handleComposerFile( [ 'composer_file' => $composerFile, ], - 'notice', + LogLevel::NOTICE, ); } if ($interactive && $input->isInteractive() && ! $this->shouldWriteManagedFile($composerFile)) { - $this->notice('Skipped updating {composer_file}.', $input, [ + $this->log('Skipped updating {composer_file}.', $input, [ 'composer_file' => $composerFile, - ]); + ], LogLevel::NOTICE); return $this->success( 'Funding synchronization was skipped for {composer_file}.', @@ -288,7 +288,7 @@ private function handleComposerFile( [ 'composer_file' => $composerFile, ], - 'notice', + LogLevel::NOTICE, ); } @@ -339,12 +339,13 @@ private function handleFundingFile( OutputInterface $output, ): int { if (null === $updatedFundingContents && null === $currentFundingContents) { - $this->notice( + $this->log( 'No supported funding metadata found. Skipping .github/FUNDING.yml synchronization.', $input, [ 'funding_file' => $fundingFile, ], + LogLevel::NOTICE, ); return $this->success( @@ -353,7 +354,7 @@ private function handleFundingFile( [ 'funding_file' => $fundingFile, ], - 'notice', + LogLevel::NOTICE, ); } @@ -380,22 +381,22 @@ private function handleFundingFile( : \sprintf('Updating managed file %s from generated funding metadata synchronization.', $fundingFile), ); - $this->logger->notice($comparison->getSummary(), [ - 'input' => $input, + $this->log($comparison->getSummary(), $input, [ 'funding_file' => $fundingFile, - ]); + ], LogLevel::NOTICE); if ($comparison->isChanged()) { $consoleDiff = $this->fileDiffer->formatForConsole($comparison->getDiff(), $output->isDecorated()); if (null !== $consoleDiff) { - $this->logger->notice( + $this->log( $consoleDiff, + $input, [ - 'input' => $input, 'funding_file' => $fundingFile, 'diff' => $comparison->getDiff(), ], + LogLevel::NOTICE, ); } } @@ -428,14 +429,14 @@ private function handleFundingFile( [ 'funding_file' => $fundingFile, ], - 'notice', + LogLevel::NOTICE, ); } if ($interactive && $input->isInteractive() && ! $this->shouldWriteManagedFile($fundingFile)) { - $this->notice('Skipped updating {funding_file}.', $input, [ + $this->log('Skipped updating {funding_file}.', $input, [ 'funding_file' => $fundingFile, - ]); + ], LogLevel::NOTICE); return $this->success( 'Funding synchronization was skipped for {funding_file}.', @@ -443,7 +444,7 @@ private function handleFundingFile( [ 'funding_file' => $fundingFile, ], - 'notice', + LogLevel::NOTICE, ); } diff --git a/src/Console/Command/GitAttributesCommand.php b/src/Console/Command/GitAttributesCommand.php index 3c58b73fdb..ebd61497fb 100644 --- a/src/Console/Command/GitAttributesCommand.php +++ b/src/Console/Command/GitAttributesCommand.php @@ -30,7 +30,7 @@ use FastForward\DevTools\GitAttributes\ReaderInterface; use FastForward\DevTools\GitAttributes\WriterInterface; use FastForward\DevTools\Resource\FileDiffer; -use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -77,7 +77,6 @@ final class GitAttributesCommand extends Command * @param FilesystemInterface $filesystem the filesystem component * @param ComposerJsonInterface $composer the composer.json accessor * @param FileDiffer $fileDiffer the file differ used to summarize synchronization changes - * @param LoggerInterface $logger the output-aware logger * @param SymfonyStyle $io the input/output service used to interact with the user */ public function __construct( @@ -90,7 +89,6 @@ public function __construct( private readonly ComposerJsonInterface $composer, private readonly FilesystemInterface $filesystem, private readonly FileDiffer $fileDiffer, - private readonly LoggerInterface $logger, private readonly SymfonyStyle $io, ) { parent::__construct(); @@ -152,12 +150,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int $entries = [...$existingFolders, ...$existingFiles]; if ([] === $entries) { - $this->notice('No candidate paths found in repository. Skipping .gitattributes sync.', $input); + $this->log( + 'No candidate paths found in repository. Skipping .gitattributes sync.', + $input, + logLevel: LogLevel::NOTICE, + ); return $this->success( 'No .gitattributes synchronization changes were required.', $input, - logLevel: 'notice', + logLevel: LogLevel::NOTICE, ); } @@ -173,21 +175,22 @@ protected function execute(InputInterface $input, OutputInterface $output): int \sprintf('Updating managed file %s from generated .gitattributes synchronization.', $gitattributesPath), ); - $this->notice($comparison->getSummary(), $input, [ + $this->log($comparison->getSummary(), $input, [ 'gitattributes_path' => $gitattributesPath, - ]); + ], LogLevel::NOTICE); if ($comparison->isChanged()) { $consoleDiff = $this->fileDiffer->formatForConsole($comparison->getDiff(), $output->isDecorated()); if (null !== $consoleDiff) { - $this->notice( + $this->log( $consoleDiff, $input, [ 'gitattributes_path' => $gitattributesPath, 'diff' => $comparison->getDiff(), ], + LogLevel::NOTICE, ); } } @@ -214,17 +217,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int [ 'gitattributes_path' => $gitattributesPath, ], - 'notice', + LogLevel::NOTICE, ); } if ($interactive && $input->isInteractive() && ! $this->shouldWriteGitAttributes($gitattributesPath)) { - $this->notice( + $this->log( 'Skipped updating {gitattributes_path}.', $input, [ 'gitattributes_path' => $gitattributesPath, ], + LogLevel::NOTICE, ); return $this->success( @@ -233,7 +237,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int [ 'gitattributes_path' => $gitattributesPath, ], - 'notice', + LogLevel::NOTICE, ); } diff --git a/src/Console/Command/GitHooksCommand.php b/src/Console/Command/GitHooksCommand.php index c8eaf4d95c..aa903c8f25 100644 --- a/src/Console/Command/GitHooksCommand.php +++ b/src/Console/Command/GitHooksCommand.php @@ -26,7 +26,7 @@ use FastForward\DevTools\GitHooks\HookContentRenderer; use FastForward\DevTools\Resource\FileDiff; use FastForward\DevTools\Resource\FileDiffer; -use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -60,7 +60,6 @@ final class GitHooksCommand extends Command * @param FinderFactoryInterface $finderFactory the factory used to create finders for hook files * @param HookContentRenderer $hookContentRenderer renders packaged hooks with runtime-specific placeholders * @param FileDiffer $fileDiffer the file differ used to summarize synchronization changes - * @param LoggerInterface $logger the output-aware logger * @param SymfonyStyle $io the input/output service used to interact with the user */ public function __construct( @@ -69,7 +68,6 @@ public function __construct( private readonly FinderFactoryInterface $finderFactory, private readonly HookContentRenderer $hookContentRenderer, private readonly FileDiffer $fileDiffer, - private readonly LoggerInterface $logger, private readonly SymfonyStyle $io, ) { parent::__construct(); @@ -152,13 +150,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int $hookPath = Path::join($targetPath, $file->getRelativePathname()); if (! $overwrite && ! $dryRun && ! $check && ! $interactive && $this->filesystem->exists($hookPath)) { - $this->notice( + $this->log( 'Skipped existing {hook_name} hook.', $input, [ 'hook_name' => $file->getFilename(), 'hook_path' => $hookPath, ], + LogLevel::NOTICE, ); continue; @@ -169,20 +168,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int ? $this->fileDiffer->diff($sourcePath, $hookPath) : $this->compareRenderedHookContents($sourcePath, $hookPath, $renderedSourceContents); - $this->notice( + $this->log( $comparison->getSummary(), $input, [ 'hook_name' => $file->getFilename(), 'hook_path' => $hookPath, ], + LogLevel::NOTICE, ); if ($comparison->isChanged()) { $consoleDiff = $this->fileDiffer->formatForConsole($comparison->getDiff(), $output->isDecorated()); if (null !== $consoleDiff) { - $this->notice( + $this->log( $consoleDiff, $input, [ @@ -190,6 +190,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'hook_path' => $hookPath, 'diff' => $comparison->getDiff(), ], + LogLevel::NOTICE, ); } } @@ -209,13 +210,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int } if ($interactive && $input->isInteractive() && ! $this->shouldReplaceHook($hookPath)) { - $this->notice( + $this->log( 'Skipped replacing {hook_path}.', $input, [ 'hook_name' => $file->getFilename(), 'hook_path' => $hookPath, ], + LogLevel::NOTICE, ); continue; @@ -325,16 +327,15 @@ private function installHook( return true; } catch (IOExceptionInterface $ioException) { - $this->logger->error( + $this->failure( 'Failed to install {hook_name} hook automatically. Remove or unlock {hook_path} and rerun git-hooks.', + $input, [ - 'input' => $input, 'hook_name' => $this->filesystem->getBasename($hookPath), 'hook_path' => $hookPath, 'error' => $ioException->getMessage(), - 'file' => $ioException->getPath() ?? $hookPath, - 'line' => null, ], + $ioException->getPath() ?? $hookPath, ); return false; diff --git a/src/Console/Command/GitIgnoreCommand.php b/src/Console/Command/GitIgnoreCommand.php index 57bc7072ec..54d4199990 100644 --- a/src/Console/Command/GitIgnoreCommand.php +++ b/src/Console/Command/GitIgnoreCommand.php @@ -26,7 +26,6 @@ use FastForward\DevTools\GitIgnore\WriterInterface; use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Resource\FileDiffer; -use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; @@ -69,7 +68,6 @@ final class GitIgnoreCommand extends Command * @param WriterInterface|null $writer the writer component * @param FileLocatorInterface $fileLocator the file locator * @param FileDiffer $fileDiffer the file differ used to summarize synchronization changes - * @param LoggerInterface $logger the output-aware logger * @param SymfonyStyle $io the input/output service used to interact with the user */ public function __construct( @@ -78,7 +76,6 @@ public function __construct( private readonly WriterInterface $writer, private readonly FileLocatorInterface $fileLocator, private readonly FileDiffer $fileDiffer, - private readonly LoggerInterface $logger, private readonly SymfonyStyle $io, ) { parent::__construct(); @@ -138,9 +135,7 @@ protected function configure(): void */ protected function execute(InputInterface $input, OutputInterface $output): int { - $this->logger->info('Merging .gitignore files...', [ - 'input' => $input, - ]); + $this->log('Merging .gitignore files...', $input); $sourcePath = $input->getOption('source'); $targetPath = $input->getOption('target'); @@ -160,21 +155,22 @@ protected function execute(InputInterface $input, OutputInterface $output): int \sprintf('Updating managed file %s from generated .gitignore synchronization.', $merged->path()), ); - $this->notice($comparison->getSummary(), $input, [ + $this->log($comparison->getSummary(), $input, [ 'target_path' => $merged->path(), - ]); + ], LogLevel::NOTICE); if ($comparison->isChanged()) { $consoleDiff = $this->fileDiffer->formatForConsole($comparison->getDiff(), $output->isDecorated()); if (null !== $consoleDiff) { - $this->notice( + $this->log( $consoleDiff, $input, [ 'target_path' => $merged->path(), 'diff' => $comparison->getDiff(), ], + LogLevel::NOTICE, ); } } diff --git a/src/Console/Command/LicenseCommand.php b/src/Console/Command/LicenseCommand.php index e3860ed260..f96940f28f 100644 --- a/src/Console/Command/LicenseCommand.php +++ b/src/Console/Command/LicenseCommand.php @@ -24,7 +24,7 @@ use FastForward\DevTools\Filesystem\FilesystemInterface; use FastForward\DevTools\License\GeneratorInterface; use FastForward\DevTools\Resource\FileDiffer; -use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -55,14 +55,12 @@ final class LicenseCommand extends Command * @param GeneratorInterface $generator the generator component * @param FilesystemInterface $filesystem the filesystem component * @param FileDiffer $fileDiffer the file differ used to summarize synchronization changes - * @param LoggerInterface $logger the output-aware logger * @param SymfonyStyle $io the input/output service used to interact with the user */ public function __construct( private readonly GeneratorInterface $generator, private readonly FilesystemInterface $filesystem, private readonly FileDiffer $fileDiffer, - private readonly LoggerInterface $logger, private readonly SymfonyStyle $io, ) { parent::__construct(); @@ -122,12 +120,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int $generatedContent = $this->generator->generateContent(); if (null === $generatedContent) { - $this->notice( + $this->log( 'No supported license found in composer.json or license is unsupported. Skipping LICENSE generation.', $input, [ 'target_path' => $targetPath, ], + LogLevel::NOTICE, ); return $this->success( @@ -136,7 +135,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int [ 'target_path' => $targetPath, ], - 'notice', + LogLevel::NOTICE, ); } @@ -150,21 +149,22 @@ protected function execute(InputInterface $input, OutputInterface $output): int : \sprintf('Updating managed file %s from generated LICENSE content.', $targetPath), ); - $this->notice($comparison->getSummary(), $input, [ + $this->log($comparison->getSummary(), $input, [ 'target_path' => $targetPath, - ]); + ], LogLevel::NOTICE); if ($comparison->isChanged()) { $consoleDiff = $this->fileDiffer->formatForConsole($comparison->getDiff(), $output->isDecorated()); if (null !== $consoleDiff) { - $this->notice( + $this->log( $consoleDiff, $input, [ 'target_path' => $targetPath, 'diff' => $comparison->getDiff(), ], + LogLevel::NOTICE, ); } } @@ -197,14 +197,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int [ 'target_path' => $targetPath, ], - 'notice', + LogLevel::NOTICE, ); } if ($interactive && $input->isInteractive() && ! $this->shouldWriteLicense($targetPath)) { - $this->notice('Skipped updating {target_path}.', $input, [ + $this->log('Skipped updating {target_path}.', $input, [ 'target_path' => $targetPath, - ]); + ], LogLevel::NOTICE); return $this->success( 'LICENSE generation was skipped.', @@ -212,7 +212,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int [ 'target_path' => $targetPath, ], - 'notice', + LogLevel::NOTICE, ); } diff --git a/src/Console/Command/MetricsCommand.php b/src/Console/Command/MetricsCommand.php index 88660e6c2e..97e101996e 100644 --- a/src/Console/Command/MetricsCommand.php +++ b/src/Console/Command/MetricsCommand.php @@ -25,7 +25,6 @@ use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use FastForward\DevTools\Path\ManagedWorkspace; -use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -77,12 +76,10 @@ final class MetricsCommand extends Command /** * @param ProcessBuilderInterface $processBuilder the builder used to assemble the PhpMetrics process * @param ProcessQueueInterface $processQueue the queue used to execute the PhpMetrics process - * @param LoggerInterface $logger the output-aware logger */ public function __construct( private readonly ProcessBuilderInterface $processBuilder, private readonly ProcessQueueInterface $processQueue, - private readonly LoggerInterface $logger, ) { parent::__construct(); } diff --git a/src/Console/Command/PhpDocCommand.php b/src/Console/Command/PhpDocCommand.php index 1bce7f5a07..62270dc7c4 100644 --- a/src/Console/Command/PhpDocCommand.php +++ b/src/Console/Command/PhpDocCommand.php @@ -29,7 +29,7 @@ use FastForward\DevTools\Process\ProcessQueueInterface; use FastForward\DevTools\Path\ManagedWorkspace; use Psr\Clock\ClockInterface; -use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; use Twig\Environment; use Throwable; use FastForward\DevTools\Rector\AddMissingMethodPhpDocRector; @@ -82,7 +82,6 @@ final class PhpDocCommand extends Command * @param ComposerJsonInterface $composer * @param Environment $renderer * @param ClockInterface $clock - * @param LoggerInterface $logger the output-aware logger */ public function __construct( private readonly ProcessBuilderInterface $processBuilder, @@ -92,7 +91,6 @@ public function __construct( private readonly FilesystemInterface $filesystem, private readonly Environment $renderer, private readonly ClockInterface $clock, - private readonly LoggerInterface $logger, ) { parent::__construct(); } diff --git a/src/Console/Command/RefactorCommand.php b/src/Console/Command/RefactorCommand.php index c9f6dc7c25..6da0bdd9cb 100644 --- a/src/Console/Command/RefactorCommand.php +++ b/src/Console/Command/RefactorCommand.php @@ -24,7 +24,6 @@ use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; -use Psr\Log\LoggerInterface; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -58,13 +57,11 @@ final class RefactorCommand extends Command * @param FileLocatorInterface $fileLocator the file locator * @param ProcessBuilderInterface $processBuilder the process builder * @param ProcessQueueInterface $processQueue the process queue - * @param LoggerInterface $logger the output-aware logger */ public function __construct( private readonly FileLocatorInterface $fileLocator, private readonly ProcessBuilderInterface $processBuilder, private readonly ProcessQueueInterface $processQueue, - private readonly LoggerInterface $logger, ) { parent::__construct(); } diff --git a/src/Console/Command/ReportsCommand.php b/src/Console/Command/ReportsCommand.php index d618a01c45..d9658cb59e 100644 --- a/src/Console/Command/ReportsCommand.php +++ b/src/Console/Command/ReportsCommand.php @@ -26,7 +26,6 @@ use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; use FastForward\DevTools\Path\ManagedWorkspace; -use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -35,8 +34,7 @@ use Symfony\Component\Console\Output\OutputInterface; /** - * Coordinates the generation of Fast Forward documentation frontpage and related reports. - * This class MUST NOT be overridden and SHALL securely combine docs and testing commands. + * Coordinates documentation, coverage, and metrics report generation. */ #[AsCommand( name: 'standards:reports', @@ -54,18 +52,16 @@ final class ReportsCommand extends Command * * @param ProcessBuilderInterface $processBuilder the builder instance used to construct execution processes * @param ProcessQueueInterface $processQueue the execution queue mechanism for running sub-processes - * @param LoggerInterface $logger the output-aware logger */ public function __construct( private readonly ProcessBuilderInterface $processBuilder, private readonly ProcessQueueInterface $processQueue, - private readonly LoggerInterface $logger, ) { parent::__construct(); } /** - * @return void + * Configures the report generation options. */ protected function configure(): void { diff --git a/src/Console/Command/SelfUpdateCommand.php b/src/Console/Command/SelfUpdateCommand.php index e7166de71d..de9ab96b04 100644 --- a/src/Console/Command/SelfUpdateCommand.php +++ b/src/Console/Command/SelfUpdateCommand.php @@ -23,7 +23,6 @@ use FastForward\DevTools\Reflection\ClassReflection; use FastForward\DevTools\SelfUpdate\SelfUpdateRunnerInterface; use FastForward\DevTools\SelfUpdate\SelfUpdateScopeResolverInterface; -use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -44,12 +43,10 @@ final class SelfUpdateCommand extends Command /** * @param SelfUpdateRunnerInterface $selfUpdateRunner the runner that executes Composer's update command * @param SelfUpdateScopeResolverInterface $scopeResolver resolves whether the active binary is globally installed - * @param LoggerInterface $logger the output-aware logger */ public function __construct( private readonly SelfUpdateRunnerInterface $selfUpdateRunner, private readonly SelfUpdateScopeResolverInterface $scopeResolver, - private readonly LoggerInterface $logger, ) { parent::__construct(); } @@ -100,8 +97,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $global = $this->scopeResolver->isGlobalInstallation(); - $this->logger->info('Updating DevTools installation...', [ - 'input' => $input, + $this->log('Updating DevTools installation...', $input, [ 'global' => $global, ]); diff --git a/src/Console/Command/SkillsCommand.php b/src/Console/Command/SkillsCommand.php index 72fa23600c..38f3058e28 100644 --- a/src/Console/Command/SkillsCommand.php +++ b/src/Console/Command/SkillsCommand.php @@ -24,7 +24,6 @@ use FastForward\DevTools\Filesystem\FilesystemInterface; use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Sync\PackagedDirectorySynchronizer; -use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -65,12 +64,10 @@ final class SkillsCommand extends Command * @param FilesystemInterface $filesystem filesystem used to resolve * and manage the skills * directory structure - * @param LoggerInterface $logger logger used for command feedback */ public function __construct( private readonly PackagedDirectorySynchronizer $synchronizer, private readonly FilesystemInterface $filesystem, - private readonly LoggerInterface $logger, ) { parent::__construct(); } diff --git a/src/Console/Command/StandardsCommand.php b/src/Console/Command/StandardsCommand.php index 3331039d67..d1272f5d48 100644 --- a/src/Console/Command/StandardsCommand.php +++ b/src/Console/Command/StandardsCommand.php @@ -26,7 +26,6 @@ use FastForward\DevTools\Path\ManagedWorkspace; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; -use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -52,12 +51,10 @@ final class StandardsCommand extends Command /** * @param ProcessBuilderInterface $processBuilder * @param ProcessQueueInterface $processQueue - * @param LoggerInterface $logger */ public function __construct( private readonly ProcessBuilderInterface $processBuilder, private readonly ProcessQueueInterface $processQueue, - private readonly LoggerInterface $logger, ) { parent::__construct(); } diff --git a/src/Console/Command/SyncCommand.php b/src/Console/Command/SyncCommand.php index d26769b4b1..4f4ae847a7 100644 --- a/src/Console/Command/SyncCommand.php +++ b/src/Console/Command/SyncCommand.php @@ -24,7 +24,7 @@ use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; -use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -48,12 +48,10 @@ final class SyncCommand extends Command /** * @param ProcessBuilderInterface $processBuilder * @param ProcessQueueInterface $processQueue - * @param LoggerInterface $logger */ public function __construct( private readonly ProcessBuilderInterface $processBuilder, private readonly ProcessQueueInterface $processQueue, - private readonly LoggerInterface $logger, ) { parent::__construct(); } diff --git a/src/Console/Command/Traits/HasCommandLogger.php b/src/Console/Command/Traits/HasCommandLogger.php index 058f189576..9aadcc4d1c 100644 --- a/src/Console/Command/Traits/HasCommandLogger.php +++ b/src/Console/Command/Traits/HasCommandLogger.php @@ -20,47 +20,26 @@ namespace FastForward\DevTools\Console\Command\Traits; use FastForward\DevTools\Container\ContainerFactory; -use LogicException; use Psr\Log\LoggerInterface; /** * Resolves the logger expected by command result helper traits. * - * The consuming command MAY expose an initialized `$logger` property. When it - * does not, the trait SHALL resolve the shared logger from the DevTools - * container so reusable traits can stay decoupled from constructor wiring. + * The trait caches the shared logger lazily so consuming commands do not need + * to carry constructor wiring for internal logging helpers. */ trait HasCommandLogger { /** - * Returns the logger configured on the consuming command. - * - * @throws LogicException when the consuming command does not expose a valid logger property + * Caches the logger resolved for the consuming command. + */ + private ?LoggerInterface $logger = null; + + /** + * Returns the logger configured for the consuming command. */ public function getLogger(): LoggerInterface { - if (property_exists($this, 'logger') && $this->logger instanceof LoggerInterface) { - return $this->logger; - } - - if (property_exists($this, 'logger') && null !== $this->logger) { - throw new LogicException(\sprintf( - 'Commands using %s MUST expose a %s instance on the $logger property.', - LogsCommandResults::class, - LoggerInterface::class, - )); - } - - $logger = ContainerFactory::get(LoggerInterface::class); - - if (! $logger instanceof LoggerInterface) { - throw new LogicException(\sprintf( - 'Commands using %s MUST resolve a %s instance from the shared container.', - LogsCommandResults::class, - LoggerInterface::class, - )); - } - - return $logger; + return $this->logger ??= ContainerFactory::get(LoggerInterface::class); } } diff --git a/src/Console/Command/Traits/LogsCommandResults.php b/src/Console/Command/Traits/LogsCommandResults.php index 7eda868631..4638c4766d 100644 --- a/src/Console/Command/Traits/LogsCommandResults.php +++ b/src/Console/Command/Traits/LogsCommandResults.php @@ -54,11 +54,17 @@ private function log( return; } - $this->getLogger() - ->log($logLevel, $message, [ - 'input' => $input, - ...$context, - ]); + $context = [ + 'input' => $input, + ...$context, + ]; + + match ($logLevel) { + LogLevel::INFO => $this->getLogger()->info($message, $context), + LogLevel::NOTICE => $this->getLogger()->notice($message, $context), + LogLevel::WARNING => $this->getLogger()->warning($message, $context), + default => $this->getLogger()->log($logLevel, $message, $context), + }; } /** diff --git a/src/Console/Command/UpdateComposerJsonCommand.php b/src/Console/Command/UpdateComposerJsonCommand.php index 88382d82e5..38969fc7f9 100644 --- a/src/Console/Command/UpdateComposerJsonCommand.php +++ b/src/Console/Command/UpdateComposerJsonCommand.php @@ -26,7 +26,6 @@ use FastForward\DevTools\GrumPhp\ManagedConfigPathSynchronizer; use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Resource\FileDiffer; -use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Attribute\AsCommand; @@ -61,7 +60,6 @@ final class UpdateComposerJsonCommand extends Command * @param FileLocatorInterface $fileLocator the locator used to resolve packaged configuration files * @param ManagedConfigPathSynchronizer $managedConfigPathSynchronizer synchronizes managed GrumPHP metadata * @param FileDiffer $fileDiffer the file differ used to summarize synchronization changes - * @param LoggerInterface $logger the output-aware logger * @param SymfonyStyle $io the input/output service used to interact with the user */ public function __construct( @@ -70,7 +68,6 @@ public function __construct( private readonly FileLocatorInterface $fileLocator, private readonly ManagedConfigPathSynchronizer $managedConfigPathSynchronizer, private readonly FileDiffer $fileDiffer, - private readonly LoggerInterface $logger, private readonly SymfonyStyle $io, ) { parent::__construct(); @@ -144,18 +141,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int \sprintf('Updating managed file %s from generated dev-tools composer.json configuration.', $file), ); - $this->notice($comparison->getSummary(), $input, [ + $this->log($comparison->getSummary(), $input, [ 'file' => $file, - ]); + ], LogLevel::NOTICE); if ($comparison->isChanged()) { $consoleDiff = $this->fileDiffer->formatForConsole($comparison->getDiff(), $output->isDecorated()); if (null !== $consoleDiff) { - $this->notice($consoleDiff, $input, [ + $this->log($consoleDiff, $input, [ 'file' => $file, 'diff' => $comparison->getDiff(), - ]); + ], LogLevel::NOTICE); } } diff --git a/src/Console/Command/WikiCommand.php b/src/Console/Command/WikiCommand.php index 33f616fd5e..67a3f049f0 100644 --- a/src/Console/Command/WikiCommand.php +++ b/src/Console/Command/WikiCommand.php @@ -30,7 +30,6 @@ use FastForward\DevTools\Process\ProcessQueueInterface; use FastForward\DevTools\Path\ManagedWorkspace; use FastForward\DevTools\Project\ProjectCapabilitiesResolverInterface; -use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -71,7 +70,6 @@ final class WikiCommand extends Command * @param FilesystemInterface $filesystem the filesystem used to inspect the wiki target * @param GitClientInterface $gitClient * @param ProjectCapabilitiesResolverInterface $projectCapabilitiesResolver the project capability resolver - * @param LoggerInterface $logger the output-aware logger */ public function __construct( private readonly ProcessBuilderInterface $processBuilder, @@ -80,7 +78,6 @@ public function __construct( private readonly FilesystemInterface $filesystem, private readonly GitClientInterface $gitClient, private readonly ProjectCapabilitiesResolverInterface $projectCapabilitiesResolver, - private readonly LoggerInterface $logger, ) { parent::__construct(); } diff --git a/src/Console/Input/HasJsonOption.php b/src/Console/Input/HasJsonOption.php index 3af859380c..5031ce0a02 100644 --- a/src/Console/Input/HasJsonOption.php +++ b/src/Console/Input/HasJsonOption.php @@ -23,6 +23,7 @@ use FastForward\DevTools\Environment\RuntimeEnvironmentInterface; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; +use Throwable; /** * Provides the standard JSON output options used by DevTools commands. @@ -58,7 +59,7 @@ protected function isJsonOutput(InputInterface $input): bool return true; } - if ((bool) $input->getOption('json')) { + if ($this->isOptionEnabled($input, 'json')) { return true; } @@ -72,7 +73,7 @@ protected function isJsonOutput(InputInterface $input): bool */ protected function isPrettyJsonOutput(InputInterface $input): bool { - return (bool) $input->getOption('pretty-json'); + return $this->isOptionEnabled($input, 'pretty-json'); } /** @@ -112,4 +113,19 @@ private function resolveRuntimeEnvironment(): ?RuntimeEnvironmentInterface return $this->runtimeEnvironment; } + + /** + * Determines whether a boolean input option was enabled. + * + * @param InputInterface $input + * @param string $option + */ + private function isOptionEnabled(InputInterface $input, string $option): bool + { + try { + return (bool) $input->getOption($option); + } catch (Throwable) { + return false; + } + } } diff --git a/src/Console/Logger/Processor/CommandInputProcessor.php b/src/Console/Logger/Processor/CommandInputProcessor.php index d9b6581a38..0159210c53 100644 --- a/src/Console/Logger/Processor/CommandInputProcessor.php +++ b/src/Console/Logger/Processor/CommandInputProcessor.php @@ -25,6 +25,7 @@ use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; +use Throwable; /** * Expands command input instances into structured context entries. @@ -113,7 +114,7 @@ private function extractProvidedArguments(InputInterface $input): array $arguments = []; $arrayParameters = $this->resolveArrayParameters($input); - foreach ($input->getArguments() as $name => $value) { + foreach ($this->resolveArguments($input) as $name => $value) { if (null === $value) { continue; } @@ -146,6 +147,20 @@ private function extractProvidedArguments(InputInterface $input): array return $arguments; } + /** + * @param InputInterface $input + * + * @return array + */ + private function resolveArguments(InputInterface $input): array + { + try { + return $input->getArguments(); + } catch (Throwable) { + return []; + } + } + /** * @param InputInterface $input * diff --git a/tests/Console/Command/AgentsCommandTest.php b/tests/Console/Command/AgentsCommandTest.php index 8c6de41d3a..3204e22a1c 100644 --- a/tests/Console/Command/AgentsCommandTest.php +++ b/tests/Console/Command/AgentsCommandTest.php @@ -34,6 +34,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use ReflectionMethod; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -48,6 +49,7 @@ final class AgentsCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; private ObjectProphecy $synchronizer; @@ -69,7 +71,13 @@ protected function setUp(): void $this->synchronizer = $this->prophesize(PackagedDirectorySynchronizer::class); $this->filesystem = $this->prophesize(FilesystemInterface::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->input = $this->prophesize(InputInterface::class); + + $this->input->getOption('json') + ->willReturn(false); + $this->input->getOption('pretty-json') + ->willReturn(false); $this->output = $this->prophesize(OutputInterface::class); $this->filesystem->getAbsolutePath('.agents/agents') ->willReturn(getcwd() . '/.agents/agents'); @@ -77,7 +85,6 @@ protected function setUp(): void $this->command = new AgentsCommand( $this->synchronizer->reveal(), $this->filesystem->reveal(), - $this->logger->reveal(), ); } @@ -92,7 +99,9 @@ public function executeWillFailWhenPackagedAgentsDirectoryDoesNotExist(): void $this->filesystem->exists($agentsPath) ->willReturn(false); $this->synchronizer->synchronize(Argument::cetera())->shouldNotBeCalled(); - $this->logger->info('Starting agents synchronization...') + $this->logger->info('Starting agents synchronization...', [ + 'input' => $this->input->reveal(), + ]) ->shouldBeCalledOnce(); $this->logger->error( 'No packaged .agents/agents found at: {packaged_agents_path}', @@ -125,9 +134,13 @@ public function executeWillCreateAgentsDirectoryWhenItDoesNotExist(): void $this->synchronizer->synchronize($agentsPath, $agentsPath, '.agents/agents') ->willReturn($result) ->shouldBeCalledOnce(); - $this->logger->info('Starting agents synchronization...') + $this->logger->info('Starting agents synchronization...', [ + 'input' => $this->input->reveal(), + ]) ->shouldBeCalledOnce(); - $this->logger->info('Created .agents/agents directory.') + $this->logger->info('Created .agents/agents directory.', [ + 'input' => $this->input->reveal(), + ]) ->shouldBeCalledOnce(); $this->logger->log( 'info', @@ -158,7 +171,9 @@ public function executeWillReturnFailureWhenSynchronizerFails(): void $this->synchronizer->synchronize($agentsPath, $agentsPath, '.agents/agents') ->willReturn($result) ->shouldBeCalledOnce(); - $this->logger->info('Starting agents synchronization...') + $this->logger->info('Starting agents synchronization...', [ + 'input' => $this->input->reveal(), + ]) ->shouldBeCalledOnce(); $this->logger->error( 'Agents synchronization failed.', diff --git a/tests/Console/Command/ChangelogCheckCommandTest.php b/tests/Console/Command/ChangelogCheckCommandTest.php index 962fdc97c5..c81779014f 100644 --- a/tests/Console/Command/ChangelogCheckCommandTest.php +++ b/tests/Console/Command/ChangelogCheckCommandTest.php @@ -24,6 +24,7 @@ use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Filesystem\FilesystemInterface; use Psr\Log\LoggerInterface; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesTrait; @@ -40,6 +41,7 @@ final class ChangelogCheckCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; /** * @var ObjectProphecy @@ -67,7 +69,13 @@ protected function setUp(): void $this->checker = $this->prophesize(UnreleasedEntryCheckerInterface::class); $this->filesystem = $this->prophesize(FilesystemInterface::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->input = $this->prophesize(InputInterface::class); + + $this->input->getOption('json') + ->willReturn(false); + $this->input->getOption('pretty-json') + ->willReturn(false); $this->output = $this->prophesize(OutputInterface::class); $this->input->getOption('against') ->willReturn(null); @@ -78,7 +86,6 @@ protected function setUp(): void $this->command = new ChangelogCheckCommand( $this->filesystem->reveal(), $this->checker->reveal(), - $this->logger->reveal(), ); } diff --git a/tests/Console/Command/ChangelogEntryCommandTest.php b/tests/Console/Command/ChangelogEntryCommandTest.php index 9d5b551752..f70ed17737 100644 --- a/tests/Console/Command/ChangelogEntryCommandTest.php +++ b/tests/Console/Command/ChangelogEntryCommandTest.php @@ -32,6 +32,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use ReflectionMethod; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -42,6 +43,7 @@ final class ChangelogEntryCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; private ObjectProphecy $filesystem; @@ -63,7 +65,13 @@ protected function setUp(): void $this->filesystem = $this->prophesize(FilesystemInterface::class); $this->changelogManager = $this->prophesize(ChangelogManagerInterface::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->input = $this->prophesize(InputInterface::class); + + $this->input->getOption('json') + ->willReturn(false); + $this->input->getOption('pretty-json') + ->willReturn(false); $this->output = $this->prophesize(OutputInterface::class); $this->input->getOption('file') @@ -82,7 +90,6 @@ protected function setUp(): void $this->command = new ChangelogEntryCommand( $this->filesystem->reveal(), $this->changelogManager->reveal(), - $this->logger->reveal(), ); } diff --git a/tests/Console/Command/ChangelogNextVersionCommandTest.php b/tests/Console/Command/ChangelogNextVersionCommandTest.php index 6cb33a4fee..97d7e66993 100644 --- a/tests/Console/Command/ChangelogNextVersionCommandTest.php +++ b/tests/Console/Command/ChangelogNextVersionCommandTest.php @@ -31,6 +31,7 @@ use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use ReflectionMethod; use RuntimeException; use Symfony\Component\Console\Input\InputInterface; @@ -41,6 +42,7 @@ final class ChangelogNextVersionCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; /** * @var ObjectProphecy @@ -71,6 +73,7 @@ protected function setUp(): void $this->filesystem = $this->prophesize(FilesystemInterface::class); $this->changelogManager = $this->prophesize(ChangelogManagerInterface::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->input = $this->prophesize(InputInterface::class); $this->output = $this->prophesize(OutputInterface::class); @@ -88,7 +91,6 @@ protected function setUp(): void $this->command = new ChangelogNextVersionCommand( $this->filesystem->reveal(), $this->changelogManager->reveal(), - $this->logger->reveal(), ); } diff --git a/tests/Console/Command/ChangelogPromoteCommandTest.php b/tests/Console/Command/ChangelogPromoteCommandTest.php index cbd575a700..7d8ead64c2 100644 --- a/tests/Console/Command/ChangelogPromoteCommandTest.php +++ b/tests/Console/Command/ChangelogPromoteCommandTest.php @@ -32,6 +32,7 @@ use Prophecy\Prophecy\ObjectProphecy; use Psr\Clock\ClockInterface; use Psr\Log\LoggerInterface; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use ReflectionMethod; use RuntimeException; use Symfony\Component\Console\Input\InputInterface; @@ -42,6 +43,7 @@ final class ChangelogPromoteCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; private ObjectProphecy $filesystem; @@ -66,7 +68,13 @@ protected function setUp(): void $this->changelogManager = $this->prophesize(ChangelogManagerInterface::class); $this->clock = $this->prophesize(ClockInterface::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->input = $this->prophesize(InputInterface::class); + + $this->input->getOption('json') + ->willReturn(false); + $this->input->getOption('pretty-json') + ->willReturn(false); $this->output = $this->prophesize(OutputInterface::class); $this->input->getOption('file') @@ -84,7 +92,6 @@ protected function setUp(): void $this->filesystem->reveal(), $this->changelogManager->reveal(), $this->clock->reveal(), - $this->logger->reveal(), ); } diff --git a/tests/Console/Command/ChangelogShowCommandTest.php b/tests/Console/Command/ChangelogShowCommandTest.php index 224c33942f..8f3acb6824 100644 --- a/tests/Console/Command/ChangelogShowCommandTest.php +++ b/tests/Console/Command/ChangelogShowCommandTest.php @@ -31,6 +31,7 @@ use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use ReflectionMethod; use RuntimeException; use Symfony\Component\Console\Input\InputInterface; @@ -41,6 +42,7 @@ final class ChangelogShowCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; private ObjectProphecy $changelogManager; @@ -62,6 +64,7 @@ protected function setUp(): void $this->changelogManager = $this->prophesize(ChangelogManagerInterface::class); $this->filesystem = $this->prophesize(FilesystemInterface::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->input = $this->prophesize(InputInterface::class); $this->output = $this->prophesize(OutputInterface::class); @@ -79,7 +82,6 @@ protected function setUp(): void $this->command = new ChangelogShowCommand( $this->filesystem->reveal(), $this->changelogManager->reveal(), - $this->logger->reveal(), ); } diff --git a/tests/Console/Command/CodeOwnersCommandTest.php b/tests/Console/Command/CodeOwnersCommandTest.php index d2e1b7b089..333748d25e 100644 --- a/tests/Console/Command/CodeOwnersCommandTest.php +++ b/tests/Console/Command/CodeOwnersCommandTest.php @@ -36,6 +36,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use ReflectionMethod; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -46,6 +47,7 @@ final class CodeOwnersCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; /** * @var ObjectProphecy @@ -91,9 +93,15 @@ protected function setUp(): void $this->generator = $this->prophesize(CodeOwnersGenerator::class); $this->filesystem = $this->prophesize(FilesystemInterface::class); $this->input = $this->prophesize(InputInterface::class); + + $this->input->getOption('json') + ->willReturn(false); + $this->input->getOption('pretty-json') + ->willReturn(false); $this->output = $this->prophesize(OutputInterface::class); $this->fileDiffer = $this->prophesize(FileDiffer::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->io = $this->prophesize(SymfonyStyle::class); $this->input->getOption('file') @@ -121,7 +129,6 @@ protected function setUp(): void $this->generator->reveal(), $this->filesystem->reveal(), $this->fileDiffer->reveal(), - $this->logger->reveal(), $this->io->reveal(), ); } diff --git a/tests/Console/Command/CodeStyleCommandTest.php b/tests/Console/Command/CodeStyleCommandTest.php index 35696e8c21..60b5578b00 100644 --- a/tests/Console/Command/CodeStyleCommandTest.php +++ b/tests/Console/Command/CodeStyleCommandTest.php @@ -34,6 +34,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use ReflectionMethod; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Formatter\OutputFormatter; @@ -48,6 +49,7 @@ final class CodeStyleCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; private ObjectProphecy $fileLocator; @@ -77,6 +79,7 @@ protected function setUp(): void $this->input = $this->prophesize(InputInterface::class); $this->output = $this->prophesize(OutputInterface::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->input->getOption('fix') ->willReturn(false); @@ -106,7 +109,6 @@ protected function setUp(): void $this->fileLocator->reveal(), $this->processBuilder->reveal(), $this->processQueue->reveal(), - $this->logger->reveal(), ); } @@ -122,7 +124,7 @@ public function executeWillReturnSuccessWhenProcessQueueSucceeds(): void $this->processQueue->run(Argument::type('object')) ->willReturn(CodeStyleCommand::SUCCESS) ->shouldBeCalled(); - $this->logger->log('info', 'Running code style checks and fixes...', Argument::that( + $this->logger->info('Running code style checks and fixes...', Argument::that( fn(array $context): bool => $this->input->reveal() === $context['input'] )) ->shouldBeCalled(); @@ -147,7 +149,7 @@ public function executeWillReturnFailureWhenProcessQueueFails(): void $this->processQueue->run(Argument::type('object')) ->willReturn(CodeStyleCommand::FAILURE) ->shouldBeCalled(); - $this->logger->log('info', 'Running code style checks and fixes...', Argument::that( + $this->logger->info('Running code style checks and fixes...', Argument::that( fn(array $context): bool => $this->input->reveal() === $context['input'] )) ->shouldBeCalled(); diff --git a/tests/Console/Command/CopyResourceCommandTest.php b/tests/Console/Command/CopyResourceCommandTest.php index df7a3fda11..9026f9bee2 100644 --- a/tests/Console/Command/CopyResourceCommandTest.php +++ b/tests/Console/Command/CopyResourceCommandTest.php @@ -34,6 +34,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use ReflectionMethod; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Input\InputInterface; @@ -50,6 +51,7 @@ final class CopyResourceCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; private ObjectProphecy $filesystem; @@ -84,9 +86,15 @@ protected function setUp(): void $this->fileLocator = $this->prophesize(FileLocatorInterface::class); $this->finderFactory = $this->prophesize(FinderFactoryInterface::class); $this->input = $this->prophesize(InputInterface::class); + + $this->input->getOption('json') + ->willReturn(false); + $this->input->getOption('pretty-json') + ->willReturn(false); $this->output = $this->prophesize(OutputInterface::class); $this->fileDiffer = $this->prophesize(FileDiffer::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->io = $this->prophesize(SymfonyStyle::class); $this->output->isDecorated() ->willReturn(false); @@ -109,7 +117,6 @@ protected function setUp(): void $this->fileLocator->reveal(), $this->finderFactory->reveal(), $this->fileDiffer->reveal(), - $this->logger->reveal(), $this->io->reveal(), ); } diff --git a/tests/Console/Command/DependenciesCommandTest.php b/tests/Console/Command/DependenciesCommandTest.php index cc8e2351e7..c5909cb408 100644 --- a/tests/Console/Command/DependenciesCommandTest.php +++ b/tests/Console/Command/DependenciesCommandTest.php @@ -36,6 +36,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use ReflectionMethod; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Formatter\OutputFormatter; @@ -51,6 +52,7 @@ final class DependenciesCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; private ObjectProphecy $processQueue; @@ -74,6 +76,7 @@ protected function setUp(): void $this->output = $this->prophesize(OutputInterface::class); $this->fileLocator = $this->prophesize(FileLocatorInterface::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->fileLocator->locate('composer-dependency-analyser.php') ->willReturn('/app/composer-dependency-analyser.php'); @@ -102,7 +105,6 @@ protected function setUp(): void new ProcessBuilder(), $this->processQueue->reveal(), $this->fileLocator->reveal(), - $this->logger->reveal(), ); } @@ -247,7 +249,6 @@ private function assertComposerDependencyAnalyserEnvironment(string $expectedVal $processBuilder->reveal(), $this->processQueue->reveal(), $this->fileLocator->reveal(), - $this->logger->reveal(), ); $processBuilder->withArgument('--config', '/app/composer-dependency-analyser.php') @@ -279,7 +280,6 @@ public function jackBreakpointProcessWillUseTheResolvedJackBinary(): void $processBuilder->reveal(), $this->processQueue->reveal(), $this->fileLocator->reveal(), - $this->logger->reveal(), ); $processBuilder->withArgument('--limit', '5') @@ -305,7 +305,6 @@ public function openVersionsProcessWillUseTheResolvedJackBinary(): void $processBuilder->reveal(), $this->processQueue->reveal(), $this->fileLocator->reveal(), - $this->logger->reveal(), ); $processBuilder->withArgument('--dry-run') @@ -331,7 +330,6 @@ public function raiseToInstalledProcessWillUseTheResolvedJackBinary(): void $processBuilder->reveal(), $this->processQueue->reveal(), $this->fileLocator->reveal(), - $this->logger->reveal(), ); $processBuilder->withArgument('--dry-run') diff --git a/tests/Console/Command/DocsCommandTest.php b/tests/Console/Command/DocsCommandTest.php index 8a886067fc..193ed4a42f 100644 --- a/tests/Console/Command/DocsCommandTest.php +++ b/tests/Console/Command/DocsCommandTest.php @@ -40,6 +40,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use ReflectionMethod; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Input\InputInterface; @@ -56,6 +57,7 @@ final class DocsCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; private ObjectProphecy $processBuilder; @@ -91,6 +93,7 @@ protected function setUp(): void $this->composer = $this->prophesize(ComposerJsonInterface::class); $this->projectCapabilitiesResolver = $this->prophesize(ProjectCapabilitiesResolverInterface::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->input = $this->prophesize(InputInterface::class); $this->output = $this->prophesize(OutputInterface::class); $this->process = $this->prophesize(Process::class); @@ -165,7 +168,6 @@ protected function setUp(): void $this->filesystem->reveal(), $this->composer->reveal(), $this->projectCapabilitiesResolver->reveal(), - $this->logger->reveal(), ); } diff --git a/tests/Console/Command/FundingCommandTest.php b/tests/Console/Command/FundingCommandTest.php index 47e658440b..ff57089220 100644 --- a/tests/Console/Command/FundingCommandTest.php +++ b/tests/Console/Command/FundingCommandTest.php @@ -41,6 +41,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use ReflectionMethod; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -59,6 +60,7 @@ final class FundingCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; private ObjectProphecy $filesystem; @@ -87,6 +89,11 @@ protected function setUp(): void { $this->filesystem = $this->prophesize(FilesystemInterface::class); $this->input = $this->prophesize(InputInterface::class); + + $this->input->getOption('json') + ->willReturn(false); + $this->input->getOption('pretty-json') + ->willReturn(false); $this->output = $this->prophesize(OutputInterface::class); $this->fileDiffer = $this->prophesize(FileDiffer::class); $this->processBuilder = $this->prophesize(ProcessBuilderInterface::class); @@ -94,6 +101,7 @@ protected function setUp(): void $this->normalizeProcess = $this->prophesize(Process::class); $this->io = $this->prophesize(SymfonyStyle::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->output->isDecorated() ->willReturn(false); $this->output->writeln(Argument::any()); @@ -133,7 +141,6 @@ protected function setUp(): void $this->fileDiffer->reveal(), $this->processBuilder->reveal(), $this->processQueue->reveal(), - $this->logger->reveal(), $this->io->reveal(), ); } diff --git a/tests/Console/Command/GitAttributesCommandTest.php b/tests/Console/Command/GitAttributesCommandTest.php index f25f85a219..c241c8eb99 100644 --- a/tests/Console/Command/GitAttributesCommandTest.php +++ b/tests/Console/Command/GitAttributesCommandTest.php @@ -42,6 +42,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use ReflectionMethod; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -54,6 +55,7 @@ final class GitAttributesCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; /** * @var ObjectProphecy @@ -133,9 +135,15 @@ protected function setUp(): void $this->composerJson = $this->prophesize(ComposerJsonInterface::class); $this->filesystem = $this->prophesize(FilesystemInterface::class); $this->input = $this->prophesize(InputInterface::class); + + $this->input->getOption('json') + ->willReturn(false); + $this->input->getOption('pretty-json') + ->willReturn(false); $this->output = $this->prophesize(OutputInterface::class); $this->fileDiffer = $this->prophesize(FileDiffer::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->io = $this->prophesize(SymfonyStyle::class); $this->output->isDecorated() ->willReturn(false); @@ -168,7 +176,6 @@ protected function setUp(): void $this->composerJson->reveal(), $this->filesystem->reveal(), $this->fileDiffer->reveal(), - $this->logger->reveal(), $this->io->reveal(), ); } diff --git a/tests/Console/Command/GitHooksCommandTest.php b/tests/Console/Command/GitHooksCommandTest.php index 17d014b5b9..c947290a10 100644 --- a/tests/Console/Command/GitHooksCommandTest.php +++ b/tests/Console/Command/GitHooksCommandTest.php @@ -34,6 +34,7 @@ use PHPUnit\Framework\Attributes\UsesTrait; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; @@ -62,6 +63,7 @@ final class GitHooksCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; private ObjectProphecy $filesystem; @@ -96,9 +98,15 @@ protected function setUp(): void $this->fileLocator = $this->prophesize(FileLocatorInterface::class); $this->finderFactory = $this->prophesize(FinderFactoryInterface::class); $this->input = $this->prophesize(InputInterface::class); + + $this->input->getOption('json') + ->willReturn(false); + $this->input->getOption('pretty-json') + ->willReturn(false); $this->output = $this->prophesize(OutputInterface::class); $this->fileDiffer = $this->prophesize(FileDiffer::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->io = $this->prophesize(SymfonyStyle::class); $this->output->isDecorated() ->willReturn(false); @@ -127,7 +135,6 @@ protected function setUp(): void $this->finderFactory->reveal(), new HookContentRenderer(), $this->fileDiffer->reveal(), - $this->logger->reveal(), $this->io->reveal(), ); } diff --git a/tests/Console/Command/GitIgnoreCommandTest.php b/tests/Console/Command/GitIgnoreCommandTest.php index 4bebec70a8..1199e8914f 100644 --- a/tests/Console/Command/GitIgnoreCommandTest.php +++ b/tests/Console/Command/GitIgnoreCommandTest.php @@ -30,6 +30,7 @@ use FastForward\DevTools\Resource\FileDiff; use FastForward\DevTools\Resource\FileDiffer; use Psr\Log\LoggerInterface; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -48,6 +49,7 @@ final class GitIgnoreCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; /** * @var ObjectProphecy @@ -122,9 +124,15 @@ protected function setUp(): void $this->reader = $this->prophesize(ReaderInterface::class); $this->writer = $this->prophesize(WriterInterface::class); $this->input = $this->prophesize(InputInterface::class); + + $this->input->getOption('json') + ->willReturn(false); + $this->input->getOption('pretty-json') + ->willReturn(false); $this->output = $this->prophesize(OutputInterface::class); $this->fileDiffer = $this->prophesize(FileDiffer::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->io = $this->prophesize(SymfonyStyle::class); $this->output->isDecorated() ->willReturn(false); @@ -181,7 +189,6 @@ protected function setUp(): void $this->writer->reveal(), $this->fileLocator->reveal(), $this->fileDiffer->reveal(), - $this->logger->reveal(), $this->io->reveal(), ); } diff --git a/tests/Console/Command/LicenseCommandTest.php b/tests/Console/Command/LicenseCommandTest.php index 598c680bc6..9b26ccad2d 100644 --- a/tests/Console/Command/LicenseCommandTest.php +++ b/tests/Console/Command/LicenseCommandTest.php @@ -31,6 +31,7 @@ use FastForward\DevTools\Resource\FileDiff; use FastForward\DevTools\Resource\FileDiffer; use Psr\Log\LoggerInterface; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -54,6 +55,7 @@ final class LicenseCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; /** * @var ObjectProphecy @@ -96,9 +98,15 @@ protected function setUp(): void $this->generator = $this->prophesize(GeneratorInterface::class); $this->filesystem = $this->prophesize(FilesystemInterface::class); $this->input = $this->prophesize(InputInterface::class); + + $this->input->getOption('json') + ->willReturn(false); + $this->input->getOption('pretty-json') + ->willReturn(false); $this->output = $this->prophesize(OutputInterface::class); $this->fileDiffer = $this->prophesize(FileDiffer::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->io = $this->prophesize(SymfonyStyle::class); $this->output->isDecorated() ->willReturn(false); @@ -121,7 +129,6 @@ protected function setUp(): void $this->generator->reveal(), $this->filesystem->reveal(), $this->fileDiffer->reveal(), - $this->logger->reveal(), $this->io->reveal(), ); } diff --git a/tests/Console/Command/MetricsCommandTest.php b/tests/Console/Command/MetricsCommandTest.php index c15196a83c..4b4842540c 100644 --- a/tests/Console/Command/MetricsCommandTest.php +++ b/tests/Console/Command/MetricsCommandTest.php @@ -36,6 +36,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use ReflectionMethod; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Input\InputInterface; @@ -53,6 +54,7 @@ final class MetricsCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; private ObjectProphecy $processBuilder; @@ -76,6 +78,7 @@ protected function setUp(): void $this->processBuilder = $this->prophesize(ProcessBuilderInterface::class); $this->processQueue = $this->prophesize(ProcessQueueInterface::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->input = $this->prophesize(InputInterface::class); $this->output = $this->prophesize(OutputInterface::class); $this->process = $this->prophesize(Process::class); @@ -110,7 +113,6 @@ protected function setUp(): void $this->command = new MetricsCommand( $this->processBuilder->reveal(), $this->processQueue->reveal(), - $this->logger->reveal(), ); } @@ -227,7 +229,6 @@ public function configureWillExcludeCustomRelativeWorkspaceByDefault(): void $command = new MetricsCommand( $this->processBuilder->reveal(), $this->processQueue->reveal(), - $this->logger->reveal(), ); self::assertSame( diff --git a/tests/Console/Command/PhpDocCommandTest.php b/tests/Console/Command/PhpDocCommandTest.php index cb6c37ad34..6339dbffa4 100644 --- a/tests/Console/Command/PhpDocCommandTest.php +++ b/tests/Console/Command/PhpDocCommandTest.php @@ -43,6 +43,8 @@ use Prophecy\Prophecy\ObjectProphecy; use Psr\Clock\ClockInterface; use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use ReflectionMethod; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Formatter\OutputFormatter; @@ -61,6 +63,7 @@ final class PhpDocCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; private ObjectProphecy $processBuilder; @@ -99,6 +102,7 @@ protected function setUp(): void $this->renderer = $this->prophesize(Environment::class); $this->clock = $this->prophesize(ClockInterface::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->input = $this->prophesize(InputInterface::class); $this->output = $this->prophesize(OutputInterface::class); $this->process = $this->prophesize(Process::class); @@ -169,7 +173,6 @@ protected function setUp(): void $this->filesystem->reveal(), $this->renderer->reveal(), $this->clock->reveal(), - $this->logger->reveal(), ); } @@ -254,7 +257,10 @@ public function executeWillHandleDumpFileExceptionAndContinueRunningProcesses(): )) ->shouldBeCalled(); $this->logger->warning( - 'Skipping .docheader creation because the destination file could not be written.' + 'Skipping .docheader creation because the destination file could not be written.', + Argument::that( + static fn(array $context): bool => $context['input'] instanceof InputInterface + ), )->shouldBeCalled(); $this->logger->error( 'PHPDoc checks failed.', diff --git a/tests/Console/Command/RefactorCommandTest.php b/tests/Console/Command/RefactorCommandTest.php index 5c26678bc5..d2d837c406 100644 --- a/tests/Console/Command/RefactorCommandTest.php +++ b/tests/Console/Command/RefactorCommandTest.php @@ -34,6 +34,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use ReflectionMethod; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Console\Formatter\OutputFormatter; @@ -48,6 +49,7 @@ final class RefactorCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; private ObjectProphecy $fileLocator; @@ -77,6 +79,7 @@ protected function setUp(): void $this->input = $this->prophesize(InputInterface::class); $this->output = $this->prophesize(OutputInterface::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->input->getOption('fix') ->willReturn(false); @@ -103,7 +106,6 @@ protected function setUp(): void $this->fileLocator->reveal(), $this->processBuilder->reveal(), $this->processQueue->reveal(), - $this->logger->reveal(), ); } diff --git a/tests/Console/Command/ReportsCommandTest.php b/tests/Console/Command/ReportsCommandTest.php index e051bae96f..684ca9f756 100644 --- a/tests/Console/Command/ReportsCommandTest.php +++ b/tests/Console/Command/ReportsCommandTest.php @@ -35,6 +35,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use ReflectionMethod; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Input\InputInterface; @@ -48,6 +49,7 @@ final class ReportsCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; private ObjectProphecy $processBuilder; @@ -71,6 +73,7 @@ protected function setUp(): void $this->processBuilder = $this->prophesize(ProcessBuilderInterface::class); $this->processQueue = $this->prophesize(ProcessQueueInterface::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->input = $this->prophesize(InputInterface::class); $this->output = $this->prophesize(OutputInterface::class); $this->process = $this->prophesize(Process::class); @@ -110,7 +113,6 @@ protected function setUp(): void $this->command = new ReportsCommand( $this->processBuilder->reveal(), $this->processQueue->reveal(), - $this->logger->reveal(), ); } diff --git a/tests/Console/Command/SelfUpdateCommandTest.php b/tests/Console/Command/SelfUpdateCommandTest.php index c3b4baa452..481266cc9b 100644 --- a/tests/Console/Command/SelfUpdateCommandTest.php +++ b/tests/Console/Command/SelfUpdateCommandTest.php @@ -33,6 +33,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use ReflectionMethod; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -43,6 +44,7 @@ final class SelfUpdateCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; /** * @var ObjectProphecy @@ -79,7 +81,13 @@ protected function setUp(): void $this->selfUpdateRunner = $this->prophesize(SelfUpdateRunnerInterface::class); $this->scopeResolver = $this->prophesize(SelfUpdateScopeResolverInterface::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->input = $this->prophesize(InputInterface::class); + + $this->input->getOption('json') + ->willReturn(false); + $this->input->getOption('pretty-json') + ->willReturn(false); $this->output = $this->prophesize(OutputInterface::class); $this->logger->info(Argument::cetera()) ->will(static function (): void {}); diff --git a/tests/Console/Command/SkillsCommandTest.php b/tests/Console/Command/SkillsCommandTest.php index d2e72d79c9..ad99de8b2f 100644 --- a/tests/Console/Command/SkillsCommandTest.php +++ b/tests/Console/Command/SkillsCommandTest.php @@ -34,6 +34,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use ReflectionMethod; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -48,6 +49,7 @@ final class SkillsCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; private ObjectProphecy $synchronizer; @@ -69,7 +71,13 @@ protected function setUp(): void $this->synchronizer = $this->prophesize(PackagedDirectorySynchronizer::class); $this->filesystem = $this->prophesize(FilesystemInterface::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->input = $this->prophesize(InputInterface::class); + + $this->input->getOption('json') + ->willReturn(false); + $this->input->getOption('pretty-json') + ->willReturn(false); $this->output = $this->prophesize(OutputInterface::class); $this->filesystem->getAbsolutePath('.agents/skills') ->willReturn(getcwd() . '/.agents/skills'); @@ -77,7 +85,6 @@ protected function setUp(): void $this->command = new SkillsCommand( $this->synchronizer->reveal(), $this->filesystem->reveal(), - $this->logger->reveal(), ); } @@ -92,7 +99,9 @@ public function executeWillFailWhenPackagedSkillsDirectoryDoesNotExist(): void $this->filesystem->exists($skillsPath) ->willReturn(false); $this->synchronizer->synchronize(Argument::cetera())->shouldNotBeCalled(); - $this->logger->info('Starting skills synchronization...') + $this->logger->info('Starting skills synchronization...', [ + 'input' => $this->input->reveal(), + ]) ->shouldBeCalledOnce(); $this->logger->error( 'No packaged skills found at: {packaged_skills_path}', @@ -125,9 +134,13 @@ public function executeWillCreateSkillsDirectoryWhenItDoesNotExist(): void $this->synchronizer->synchronize($skillsPath, $skillsPath, '.agents/skills') ->willReturn($result) ->shouldBeCalledOnce(); - $this->logger->info('Starting skills synchronization...') + $this->logger->info('Starting skills synchronization...', [ + 'input' => $this->input->reveal(), + ]) ->shouldBeCalledOnce(); - $this->logger->info('Created .agents/skills directory.') + $this->logger->info('Created .agents/skills directory.', [ + 'input' => $this->input->reveal(), + ]) ->shouldBeCalledOnce(); $this->logger->log( 'info', @@ -158,7 +171,9 @@ public function executeWillReturnFailureWhenSynchronizerFails(): void $this->synchronizer->synchronize($skillsPath, $skillsPath, '.agents/skills') ->willReturn($result) ->shouldBeCalledOnce(); - $this->logger->info('Starting skills synchronization...') + $this->logger->info('Starting skills synchronization...', [ + 'input' => $this->input->reveal(), + ]) ->shouldBeCalledOnce(); $this->logger->error( 'Skills synchronization failed.', diff --git a/tests/Console/Command/StandardsCommandTest.php b/tests/Console/Command/StandardsCommandTest.php index a7f9c36a02..d50ae4d2a3 100644 --- a/tests/Console/Command/StandardsCommandTest.php +++ b/tests/Console/Command/StandardsCommandTest.php @@ -33,6 +33,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use ReflectionMethod; use FastForward\DevTools\Path\ManagedWorkspace; use FastForward\DevTools\Path\DevToolsPathResolver; @@ -46,6 +47,7 @@ final class StandardsCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; private ObjectProphecy $processBuilder; @@ -67,6 +69,7 @@ protected function setUp(): void $this->processBuilder = $this->prophesize(ProcessBuilderInterface::class); $this->processQueue = $this->prophesize(ProcessQueueInterface::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->input = $this->prophesize(InputInterface::class); $this->output = $this->prophesize(OutputInterface::class); @@ -94,7 +97,6 @@ protected function setUp(): void $this->command = new StandardsCommand( $this->processBuilder->reveal(), $this->processQueue->reveal(), - $this->logger->reveal(), ); } diff --git a/tests/Console/Command/SyncCommandTest.php b/tests/Console/Command/SyncCommandTest.php index 49d1f303e4..aaa9a411c2 100644 --- a/tests/Console/Command/SyncCommandTest.php +++ b/tests/Console/Command/SyncCommandTest.php @@ -33,6 +33,8 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use ReflectionMethod; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -45,6 +47,7 @@ final class SyncCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; private ObjectProphecy $processQueue; @@ -65,6 +68,7 @@ protected function setUp(): void $this->input = $this->prophesize(InputInterface::class); $this->output = $this->prophesize(OutputInterface::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->input->getOption(Argument::type('string'))->willReturn(false); $this->input->getOption('json') @@ -75,7 +79,6 @@ protected function setUp(): void $this->command = new SyncCommand( new ProcessBuilder(), $this->processQueue->reveal(), - $this->logger->reveal(), ); } diff --git a/tests/Console/Command/TestsCommandTest.php b/tests/Console/Command/TestsCommandTest.php index 572cfd74ea..320c864141 100644 --- a/tests/Console/Command/TestsCommandTest.php +++ b/tests/Console/Command/TestsCommandTest.php @@ -209,7 +209,7 @@ public function executeWillRunPhpUnitProcessWithConfigFile(): void )->shouldBeCalled(); $this->processQueue->run($this->output->reveal()) ->willReturn(TestsCommand::SUCCESS)->shouldBeCalled(); - $this->logger->log('info', 'Running PHPUnit tests...', Argument::that( + $this->logger->info('Running PHPUnit tests...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); @@ -543,7 +543,7 @@ public function executeWithInvalidMinCoverageWillReturnFailure(): void $this->input->getOption('min-coverage') ->willReturn('invalid'); $this->processQueue->run(Argument::cetera())->shouldNotBeCalled(); - $this->logger->log('info', 'Running PHPUnit tests...', Argument::that( + $this->logger->info('Running PHPUnit tests...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); @@ -566,7 +566,7 @@ public function executeWillSkipWhenNoTestsDirectoryOrPhpSourceExists(): void ->willReturn(new ProjectCapabilities([], null, false, false, false, false)); $this->processQueue->add(Argument::cetera())->shouldNotBeCalled(); $this->processQueue->run(Argument::cetera())->shouldNotBeCalled(); - $this->logger->log('info', 'Running PHPUnit tests...', Argument::that( + $this->logger->info('Running PHPUnit tests...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface ))->shouldBeCalled(); $this->logger->log( @@ -595,7 +595,7 @@ public function executeWillFailWhenCustomTestsPathDoesNotExist(): void ->shouldNotBeCalled(); $this->processQueue->run(Argument::cetera()) ->shouldNotBeCalled(); - $this->logger->log('info', 'Running PHPUnit tests...', Argument::that( + $this->logger->info('Running PHPUnit tests...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface ))->shouldBeCalled(); $this->logger->error( @@ -628,7 +628,7 @@ public function executeWithCoverageBelowMinimumWillReturnFailure(): void )->shouldBeCalled(); $this->processQueue->run($this->output->reveal()) ->willReturn(TestsCommand::SUCCESS)->shouldBeCalled(); - $this->logger->log('info', 'Running PHPUnit tests...', Argument::that( + $this->logger->info('Running PHPUnit tests...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); @@ -710,7 +710,7 @@ public function executeWillReturnFailureWhenCoverageSummaryCannotBeLoaded(): voi )->shouldBeCalled(); $this->processQueue->run($this->output->reveal()) ->willReturn(TestsCommand::SUCCESS)->shouldBeCalled(); - $this->logger->log('info', 'Running PHPUnit tests...', Argument::that( + $this->logger->info('Running PHPUnit tests...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); diff --git a/tests/Console/Command/UpdateComposerJsonCommandTest.php b/tests/Console/Command/UpdateComposerJsonCommandTest.php index d53cfc21de..a4be05992e 100644 --- a/tests/Console/Command/UpdateComposerJsonCommandTest.php +++ b/tests/Console/Command/UpdateComposerJsonCommandTest.php @@ -31,6 +31,7 @@ use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; @@ -50,6 +51,7 @@ final class UpdateComposerJsonCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; private ObjectProphecy $filesystem; @@ -78,9 +80,15 @@ protected function setUp(): void $this->filesystem = $this->prophesize(FilesystemInterface::class); $this->fileLocator = $this->prophesize(FileLocatorInterface::class); $this->input = $this->prophesize(InputInterface::class); + + $this->input->getOption('json') + ->willReturn(false); + $this->input->getOption('pretty-json') + ->willReturn(false); $this->output = $this->prophesize(OutputInterface::class); $this->fileDiffer = $this->prophesize(FileDiffer::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->io = $this->prophesize(SymfonyStyle::class); $this->output->isDecorated() ->willReturn(false); @@ -107,7 +115,6 @@ protected function setUp(): void $this->fileLocator->reveal(), new ManagedConfigPathSynchronizer(), $this->fileDiffer->reveal(), - $this->logger->reveal(), $this->io->reveal(), ); } diff --git a/tests/Console/Command/WikiCommandTest.php b/tests/Console/Command/WikiCommandTest.php index dd17257a5b..410789a78f 100644 --- a/tests/Console/Command/WikiCommandTest.php +++ b/tests/Console/Command/WikiCommandTest.php @@ -41,6 +41,7 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; +use FastForward\DevTools\Tests\Container\UsesContainerFactory; use ReflectionMethod; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -57,6 +58,7 @@ final class WikiCommandTest extends TestCase { use ProphecyTrait; + use UsesContainerFactory; private ObjectProphecy $processBuilder; @@ -92,6 +94,7 @@ protected function setUp(): void $this->gitClient = $this->prophesize(GitClientInterface::class); $this->projectCapabilitiesResolver = $this->prophesize(ProjectCapabilitiesResolverInterface::class); $this->logger = $this->prophesize(LoggerInterface::class); + $this->setContainerEntry(LoggerInterface::class, $this->logger->reveal()); $this->input = $this->prophesize(InputInterface::class); $this->output = $this->prophesize(OutputInterface::class); $this->process = $this->prophesize(Process::class); @@ -132,7 +135,6 @@ protected function setUp(): void $this->filesystem->reveal(), $this->gitClient->reveal(), $this->projectCapabilitiesResolver->reveal(), - $this->logger->reveal(), ); } diff --git a/tests/Container/UsesContainerFactory.php b/tests/Container/UsesContainerFactory.php new file mode 100644 index 0000000000..5647f4027c --- /dev/null +++ b/tests/Container/UsesContainerFactory.php @@ -0,0 +1,57 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://github.com/php-fast-forward/ + * @see https://github.com/php-fast-forward/dev-tools + * @see https://github.com/php-fast-forward/dev-tools/issues + * @see https://php-fast-forward.github.io/dev-tools/ + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\DevTools\Tests\Container; + +use FastForward\DevTools\Container\ContainerFactory; + +/** + * Resets the shared DevTools container around a test class lifecycle. + */ +trait UsesContainerFactory +{ + /** + * @return void + */ + public static function setUpBeforeClass(): void + { + ContainerFactory::reset(); + } + + /** + * @return void + */ + public static function tearDownAfterClass(): void + { + ContainerFactory::reset(); + } + + /** + * Overrides a shared container entry for the current test case. + * + * @param string $id + * @param mixed $value + * + * @return void + */ + protected function setContainerEntry(string $id, mixed $value): void + { + ContainerFactory::set($id, $value); + } +} From ce1c6adca7c303714f50ca23097f6b3a7cea2efa Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 22:53:34 +0000 Subject: [PATCH 25/35] Update wiki submodule pointer for PR #329 --- .github/wiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/wiki b/.github/wiki index 2eeea15fa3..ca356029e6 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit 2eeea15fa35d6f940fd2e2ac73b8284cb896f0aa +Subproject commit ca356029e6b59e5c223dbc73969ce1ff4bb9fd28 From e5e137b81eaaafdb5487db480c3a36ee8bcece23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 22:04:42 -0300 Subject: [PATCH 26/35] Fix coverage metadata for command provider test --- .../DevToolsCommandProviderTest.php | 112 +++++++----------- 1 file changed, 42 insertions(+), 70 deletions(-) diff --git a/tests/Composer/Capability/DevToolsCommandProviderTest.php b/tests/Composer/Capability/DevToolsCommandProviderTest.php index 613c0cbd2f..f371d616c7 100644 --- a/tests/Composer/Capability/DevToolsCommandProviderTest.php +++ b/tests/Composer/Capability/DevToolsCommandProviderTest.php @@ -23,28 +23,35 @@ use FastForward\DevTools\Composer\Command\ProxyCommand; use FastForward\DevTools\Composer\DevToolsPluginInterface; use FastForward\DevTools\Console\Command\FixtureWithoutAsCommand; -use FastForward\DevTools\Console\DevTools; use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Path\DevToolsPathResolver; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; +use stdClass; #[CoversClass(DevToolsCommandProvider::class)] #[UsesClass(ContainerFactory::class)] -#[UsesClass(DevTools::class)] +#[UsesClass(DevToolsPathResolver::class)] +#[UsesClass(DevToolsServiceProvider::class)] #[UsesClass(ProxyCommand::class)] final class DevToolsCommandProviderTest extends TestCase { use ProphecyTrait; - private ObjectProphecy $commandLoader; - private ObjectProphecy $plugin; + /** + * @var array + */ + private array $applicationCommands = []; + + private stdClass $applicationState; + private DevToolsCommandProvider $commandProvider; /** @@ -53,20 +60,10 @@ final class DevToolsCommandProviderTest extends TestCase protected function setUp(): void { ContainerFactory::reset(); - $this->commandLoader = $this->prophesize(CommandLoaderInterface::class); + $this->applicationState = new stdClass(); + $this->applicationState->commands = &$this->applicationCommands; $this->plugin = $this->prophesize(DevToolsPluginInterface::class); - $this->commandLoader->getNames() - ->willReturn([]); - $this->commandLoader->has('help') - ->willReturn(false); - $this->commandLoader->has('list') - ->willReturn(false); - $this->commandLoader->has('complete') - ->willReturn(false); - $this->commandLoader->has('_complete') - ->willReturn(false); - $this->plugin->isRegisteredCommand(null) ->willReturn(false); $this->plugin->isRegisteredCommand('agents') @@ -92,7 +89,20 @@ protected function setUp(): void 'plugin' => $this->plugin->reveal(), ]); - ContainerFactory::set(CommandLoaderInterface::class, $this->commandLoader->reveal()); + ContainerFactory::set('FastForward\DevTools\Console\DevTools', new class ($this->applicationState) { + public function __construct( + private readonly stdClass $state, + ) { + } + + /** + * @return array + */ + public function all(): array + { + return $this->state->commands; + } + }); } /** @@ -127,14 +137,7 @@ public function getCommandsWillReturnComposerProxyCommandsForRegisteredSymfonyCo $symfonyCommand->setHelp(''); $symfonyCommand->setHidden(false); - $this->commandLoader->getNames() - ->willReturn(['agents']); - $this->commandLoader->has('agents') - ->willReturn(true) - ->shouldBeCalledOnce(); - $this->commandLoader->get('agents') - ->willReturn($symfonyCommand) - ->shouldBeCalledOnce(); + $this->applicationCommands = ['agents' => $symfonyCommand]; $commands = array_values($this->commandProvider->getCommands()); $command = $commands[0]; @@ -157,18 +160,10 @@ public function getCommandsWillIgnoreAliasEntriesFromApplicationAllRegistry(): v $symfonyCommand->setHelp(''); $symfonyCommand->setHidden(false); - $this->commandLoader->getNames() - ->willReturn(['reports:tests', 'tests']); - $this->commandLoader->has('reports:tests') - ->willReturn(true) - ->shouldBeCalledOnce(); - $this->commandLoader->has('tests') - ->shouldNotBeCalled(); - $this->commandLoader->get('reports:tests') - ->willReturn($symfonyCommand) - ->shouldBeCalledOnce(); - $this->commandLoader->get('tests') - ->shouldNotBeCalled(); + $this->applicationCommands = [ + 'reports:tests' => $symfonyCommand, + 'tests' => $symfonyCommand, + ]; $commands = array_values($this->commandProvider->getCommands()); $proxyCommand = $commands[0]; @@ -191,18 +186,10 @@ public function getCommandsWillPreserveSafeAliasesThroughComposerPlugin(): void $symfonyCommand->setHelp(''); $symfonyCommand->setHidden(false); - $this->commandLoader->getNames() - ->willReturn(['dev-tools:standards', 'standards']); - $this->commandLoader->has('dev-tools:standards') - ->willReturn(true) - ->shouldBeCalledOnce(); - $this->commandLoader->has('standards') - ->shouldNotBeCalled(); - $this->commandLoader->get('dev-tools:standards') - ->willReturn($symfonyCommand) - ->shouldBeCalledOnce(); - $this->commandLoader->get('standards') - ->shouldNotBeCalled(); + $this->applicationCommands = [ + 'dev-tools:standards' => $symfonyCommand, + 'standards' => $symfonyCommand, + ]; $proxyCommand = array_values($this->commandProvider->getCommands())[0]; @@ -223,18 +210,10 @@ public function getCommandsWillNotExposeSelfUpdateAliasToComposer(): void $symfonyCommand->setHelp(''); $symfonyCommand->setHidden(false); - $this->commandLoader->getNames() - ->willReturn(['dev-tools:self-update', 'self-update']); - $this->commandLoader->has('dev-tools:self-update') - ->willReturn(true) - ->shouldBeCalledOnce(); - $this->commandLoader->has('self-update') - ->shouldNotBeCalled(); - $this->commandLoader->get('dev-tools:self-update') - ->willReturn($symfonyCommand) - ->shouldBeCalledOnce(); - $this->commandLoader->get('self-update') - ->shouldNotBeCalled(); + $this->applicationCommands = [ + 'dev-tools:self-update' => $symfonyCommand, + 'self-update' => $symfonyCommand, + ]; $proxyCommand = array_values($this->commandProvider->getCommands())[0]; @@ -255,14 +234,7 @@ public function getCommandsWillNotExposeCommandsOwnedByComposer(): void $symfonyCommand->setHelp(''); $symfonyCommand->setHidden(false); - $this->commandLoader->getNames() - ->willReturn(['install']); - $this->commandLoader->has('install') - ->willReturn(true) - ->shouldBeCalledOnce(); - $this->commandLoader->get('install') - ->willReturn($symfonyCommand) - ->shouldBeCalledOnce(); + $this->applicationCommands = ['install' => $symfonyCommand]; self::assertSame([], $this->commandProvider->getCommands()); } From 9ab54d9718ab81b0f7ab0166778b70afd2dda525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 22:07:28 -0300 Subject: [PATCH 27/35] Preserve structured PHPUnit raw output --- src/Console/Command/TestsCommand.php | 2 +- tests/Console/Command/TestsCommandTest.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Console/Command/TestsCommand.php b/src/Console/Command/TestsCommand.php index 7549e6c0e9..bc034e5eab 100644 --- a/src/Console/Command/TestsCommand.php +++ b/src/Console/Command/TestsCommand.php @@ -361,7 +361,7 @@ private function resolveStructuredProcessResultPayload(OutputInterface $processO ]; } - if (null !== $supplementalOutput && ! \is_array($decoded)) { + if (null !== $supplementalOutput) { $payload['raw_output'] = $supplementalOutput; } diff --git a/tests/Console/Command/TestsCommandTest.php b/tests/Console/Command/TestsCommandTest.php index 320c864141..c31248b318 100644 --- a/tests/Console/Command/TestsCommandTest.php +++ b/tests/Console/Command/TestsCommandTest.php @@ -23,6 +23,7 @@ use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Console\Command\TestsCommand; use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; use FastForward\DevTools\Environment\RuntimeEnvironmentInterface; use FastForward\DevTools\Filesystem\FilesystemInterface; use FastForward\DevTools\PhpUnit\Bootstrap\BootstrapShimGenerator; @@ -57,6 +58,7 @@ #[UsesClass(BootstrapShimGenerator::class)] #[UsesClass(CoverageSummary::class)] #[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsServiceProvider::class)] #[UsesClass(DevToolsPathResolver::class)] #[UsesClass(ProcessBuilder::class)] #[UsesClass(ManagedWorkspace::class)] @@ -428,7 +430,7 @@ public function executeWillCaptureStructuredPhpUnitSummaryAfterCoveragePreludeWh && isset($context['output']) && 'success' === $context['output']['result'] && 5 === $context['output']['summary']['assertions'] - && ! isset($context['output']['raw_output'])), + && "Generating code coverage report in PHP format ... done [00:00.002]" === $context['output']['raw_output']), )->shouldBeCalled(); $this->output->writeln(Argument::cetera())->shouldNotBeCalled(); From 22859e70b5adba6291d59a41a637d0f1930c34e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 22:33:09 -0300 Subject: [PATCH 28/35] Fix coverage metadata for command tests --- .../Capability/DevToolsCommandProviderTest.php | 13 +++++++++---- tests/Console/Command/AgentsCommandTest.php | 13 +++++++++---- .../Command/ChangelogCheckCommandTest.php | 16 ++++++++++++---- .../Command/ChangelogEntryCommandTest.php | 12 ++++++++++++ .../ChangelogNextVersionCommandTest.php | 11 +++++++++++ .../Command/ChangelogPromoteCommandTest.php | 11 +++++++++++ .../Command/ChangelogShowCommandTest.php | 16 ++++++++++++---- .../Console/Command/CodeOwnersCommandTest.php | 10 ++++++++++ tests/Console/Command/CodeStyleCommandTest.php | 8 ++++++++ .../Command/CopyResourceCommandTest.php | 10 ++++++++++ .../Command/DependenciesCommandTest.php | 8 ++++++++ tests/Console/Command/DocsCommandTest.php | 8 ++++++++ tests/Console/Command/FundingCommandTest.php | 10 ++++++++++ .../Command/GitAttributesCommandTest.php | 10 ++++++++++ tests/Console/Command/GitHooksCommandTest.php | 8 ++++++++ tests/Console/Command/GitIgnoreCommandTest.php | 8 ++++++++ tests/Console/Command/LicenseCommandTest.php | 10 ++++++++++ tests/Console/Command/MetricsCommandTest.php | 18 ++++++++++-------- tests/Console/Command/PhpDocCommandTest.php | 13 +++++++++---- tests/Console/Command/RefactorCommandTest.php | 8 ++++++++ tests/Console/Command/ReportsCommandTest.php | 13 +++++++++---- .../Console/Command/SelfUpdateCommandTest.php | 10 ++++++++++ tests/Console/Command/SkillsCommandTest.php | 13 +++++++++---- tests/Console/Command/StandardsCommandTest.php | 13 +++++++++---- tests/Console/Command/SyncCommandTest.php | 14 +++++++++----- tests/Console/Command/TestsCommandTest.php | 6 +++++- .../Command/UpdateComposerJsonCommandTest.php | 8 ++++++++ tests/Console/Command/WikiCommandTest.php | 8 ++++++++ tests/Console/Input/HasJsonOptionTest.php | 8 ++++++++ tests/Container/ContainerFactoryTest.php | 4 ++++ tests/Container/UsesContainerFactory.php | 10 ++++++++++ 31 files changed, 282 insertions(+), 46 deletions(-) diff --git a/tests/Composer/Capability/DevToolsCommandProviderTest.php b/tests/Composer/Capability/DevToolsCommandProviderTest.php index f371d616c7..e4623ff754 100644 --- a/tests/Composer/Capability/DevToolsCommandProviderTest.php +++ b/tests/Composer/Capability/DevToolsCommandProviderTest.php @@ -26,6 +26,7 @@ use FastForward\DevTools\Container\ContainerFactory; use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; use FastForward\DevTools\Path\DevToolsPathResolver; +use FastForward\DevTools\Console\Output\GithubActionOutput; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -34,6 +35,7 @@ use Prophecy\Prophecy\ObjectProphecy; use stdClass; +#[UsesClass(GithubActionOutput::class)] #[CoversClass(DevToolsCommandProvider::class)] #[UsesClass(ContainerFactory::class)] #[UsesClass(DevToolsPathResolver::class)] @@ -92,8 +94,7 @@ protected function setUp(): void ContainerFactory::set('FastForward\DevTools\Console\DevTools', new class ($this->applicationState) { public function __construct( private readonly stdClass $state, - ) { - } + ) {} /** * @return array @@ -137,7 +138,9 @@ public function getCommandsWillReturnComposerProxyCommandsForRegisteredSymfonyCo $symfonyCommand->setHelp(''); $symfonyCommand->setHidden(false); - $this->applicationCommands = ['agents' => $symfonyCommand]; + $this->applicationCommands = [ + 'agents' => $symfonyCommand, + ]; $commands = array_values($this->commandProvider->getCommands()); $command = $commands[0]; @@ -234,7 +237,9 @@ public function getCommandsWillNotExposeCommandsOwnedByComposer(): void $symfonyCommand->setHelp(''); $symfonyCommand->setHidden(false); - $this->applicationCommands = ['install' => $symfonyCommand]; + $this->applicationCommands = [ + 'install' => $symfonyCommand, + ]; self::assertSame([], $this->commandProvider->getCommands()); } diff --git a/tests/Console/Command/AgentsCommandTest.php b/tests/Console/Command/AgentsCommandTest.php index 3204e22a1c..ee0d31c8b5 100644 --- a/tests/Console/Command/AgentsCommandTest.php +++ b/tests/Console/Command/AgentsCommandTest.php @@ -25,6 +25,10 @@ use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Sync\PackagedDirectorySynchronizer; use FastForward\DevTools\Sync\SynchronizeResult; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -41,6 +45,10 @@ use function Safe\getcwd; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(AgentsCommand::class)] #[UsesClass(DevToolsPathResolver::class)] #[UsesClass(PackagedDirectorySynchronizer::class)] @@ -82,10 +90,7 @@ protected function setUp(): void $this->filesystem->getAbsolutePath('.agents/agents') ->willReturn(getcwd() . '/.agents/agents'); - $this->command = new AgentsCommand( - $this->synchronizer->reveal(), - $this->filesystem->reveal(), - ); + $this->command = new AgentsCommand($this->synchronizer->reveal(), $this->filesystem->reveal()); } /** diff --git a/tests/Console/Command/ChangelogCheckCommandTest.php b/tests/Console/Command/ChangelogCheckCommandTest.php index c81779014f..8cc0d5bdb7 100644 --- a/tests/Console/Command/ChangelogCheckCommandTest.php +++ b/tests/Console/Command/ChangelogCheckCommandTest.php @@ -25,8 +25,14 @@ use FastForward\DevTools\Filesystem\FilesystemInterface; use Psr\Log\LoggerInterface; use FastForward\DevTools\Tests\Container\UsesContainerFactory; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; +use FastForward\DevTools\Path\DevToolsPathResolver; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\Attributes\UsesTrait; use PHPUnit\Framework\TestCase; use Prophecy\Argument; @@ -36,6 +42,11 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsPathResolver::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(ChangelogCheckCommand::class)] #[UsesTrait(LogsCommandResults::class)] final class ChangelogCheckCommandTest extends TestCase @@ -83,10 +94,7 @@ protected function setUp(): void ->willReturn('CHANGELOG.md'); $this->filesystem->getAbsolutePath('CHANGELOG.md') ->willReturn('/repo/CHANGELOG.md'); - $this->command = new ChangelogCheckCommand( - $this->filesystem->reveal(), - $this->checker->reveal(), - ); + $this->command = new ChangelogCheckCommand($this->filesystem->reveal(), $this->checker->reveal()); } /** diff --git a/tests/Console/Command/ChangelogEntryCommandTest.php b/tests/Console/Command/ChangelogEntryCommandTest.php index f70ed17737..c3f175dea4 100644 --- a/tests/Console/Command/ChangelogEntryCommandTest.php +++ b/tests/Console/Command/ChangelogEntryCommandTest.php @@ -23,7 +23,13 @@ use FastForward\DevTools\Changelog\Manager\ChangelogManagerInterface; use FastForward\DevTools\Console\Command\ChangelogEntryCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; +use FastForward\DevTools\Console\Output\GithubActionOutput; use FastForward\DevTools\Filesystem\FilesystemInterface; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; +use FastForward\DevTools\Path\DevToolsPathResolver; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -37,6 +43,12 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsPathResolver::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] +#[UsesClass(GithubActionOutput::class)] #[CoversClass(ChangelogEntryCommand::class)] #[UsesClass(ChangelogEntryType::class)] #[UsesTrait(LogsCommandResults::class)] diff --git a/tests/Console/Command/ChangelogNextVersionCommandTest.php b/tests/Console/Command/ChangelogNextVersionCommandTest.php index 97d7e66993..a1897e4cf4 100644 --- a/tests/Console/Command/ChangelogNextVersionCommandTest.php +++ b/tests/Console/Command/ChangelogNextVersionCommandTest.php @@ -23,8 +23,14 @@ use FastForward\DevTools\Console\Command\ChangelogNextVersionCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Filesystem\FilesystemInterface; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; +use FastForward\DevTools\Path\DevToolsPathResolver; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\Attributes\UsesTrait; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; @@ -37,6 +43,11 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsPathResolver::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(ChangelogNextVersionCommand::class)] #[UsesTrait(LogsCommandResults::class)] final class ChangelogNextVersionCommandTest extends TestCase diff --git a/tests/Console/Command/ChangelogPromoteCommandTest.php b/tests/Console/Command/ChangelogPromoteCommandTest.php index 7d8ead64c2..dcbd11f101 100644 --- a/tests/Console/Command/ChangelogPromoteCommandTest.php +++ b/tests/Console/Command/ChangelogPromoteCommandTest.php @@ -24,8 +24,14 @@ use FastForward\DevTools\Console\Command\ChangelogPromoteCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Filesystem\FilesystemInterface; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; +use FastForward\DevTools\Path\DevToolsPathResolver; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\Attributes\UsesTrait; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; @@ -38,6 +44,11 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsPathResolver::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(ChangelogPromoteCommand::class)] #[UsesTrait(LogsCommandResults::class)] final class ChangelogPromoteCommandTest extends TestCase diff --git a/tests/Console/Command/ChangelogShowCommandTest.php b/tests/Console/Command/ChangelogShowCommandTest.php index 8f3acb6824..2d7033f7d2 100644 --- a/tests/Console/Command/ChangelogShowCommandTest.php +++ b/tests/Console/Command/ChangelogShowCommandTest.php @@ -23,8 +23,14 @@ use FastForward\DevTools\Console\Command\ChangelogShowCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Filesystem\FilesystemInterface; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; +use FastForward\DevTools\Path\DevToolsPathResolver; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\Attributes\UsesTrait; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; @@ -37,6 +43,11 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsPathResolver::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(ChangelogShowCommand::class)] #[UsesTrait(LogsCommandResults::class)] final class ChangelogShowCommandTest extends TestCase @@ -79,10 +90,7 @@ protected function setUp(): void $this->filesystem->getAbsolutePath('CHANGELOG.md') ->willReturn('/repo/CHANGELOG.md'); - $this->command = new ChangelogShowCommand( - $this->filesystem->reveal(), - $this->changelogManager->reveal(), - ); + $this->command = new ChangelogShowCommand($this->filesystem->reveal(), $this->changelogManager->reveal()); } /** diff --git a/tests/Console/Command/CodeOwnersCommandTest.php b/tests/Console/Command/CodeOwnersCommandTest.php index 333748d25e..b91aa28fd8 100644 --- a/tests/Console/Command/CodeOwnersCommandTest.php +++ b/tests/Console/Command/CodeOwnersCommandTest.php @@ -27,6 +27,11 @@ use FastForward\DevTools\Filesystem\FilesystemInterface; use FastForward\DevTools\Resource\FileDiff; use FastForward\DevTools\Resource\FileDiffer; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; +use FastForward\DevTools\Path\DevToolsPathResolver; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -41,6 +46,11 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsPathResolver::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(CodeOwnersCommand::class)] #[UsesClass(FileDiff::class)] #[UsesTrait(LogsCommandResults::class)] diff --git a/tests/Console/Command/CodeStyleCommandTest.php b/tests/Console/Command/CodeStyleCommandTest.php index 60b5578b00..4bb5648fb3 100644 --- a/tests/Console/Command/CodeStyleCommandTest.php +++ b/tests/Console/Command/CodeStyleCommandTest.php @@ -25,6 +25,10 @@ use FastForward\DevTools\Path\WorkingProjectPathResolver; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -42,6 +46,10 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(CodeStyleCommand::class)] #[UsesClass(DevToolsPathResolver::class)] #[UsesClass(WorkingProjectPathResolver::class)] diff --git a/tests/Console/Command/CopyResourceCommandTest.php b/tests/Console/Command/CopyResourceCommandTest.php index 9026f9bee2..d13134b527 100644 --- a/tests/Console/Command/CopyResourceCommandTest.php +++ b/tests/Console/Command/CopyResourceCommandTest.php @@ -26,6 +26,11 @@ use FastForward\DevTools\Filesystem\FilesystemInterface; use FastForward\DevTools\Resource\FileDiff; use FastForward\DevTools\Resource\FileDiffer; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; +use FastForward\DevTools\Path\DevToolsPathResolver; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -46,6 +51,11 @@ use function Safe\unlink; use function Safe\rmdir; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsPathResolver::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(CopyResourceCommand::class)] #[UsesClass(FileDiff::class)] final class CopyResourceCommandTest extends TestCase diff --git a/tests/Console/Command/DependenciesCommandTest.php b/tests/Console/Command/DependenciesCommandTest.php index c5909cb408..26035a1235 100644 --- a/tests/Console/Command/DependenciesCommandTest.php +++ b/tests/Console/Command/DependenciesCommandTest.php @@ -27,6 +27,10 @@ use FastForward\DevTools\Process\ProcessBuilder; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -44,6 +48,10 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(DependenciesCommand::class)] #[UsesClass(DevToolsPathResolver::class)] #[UsesClass(WorkingProjectPathResolver::class)] diff --git a/tests/Console/Command/DocsCommandTest.php b/tests/Console/Command/DocsCommandTest.php index 193ed4a42f..57c1679410 100644 --- a/tests/Console/Command/DocsCommandTest.php +++ b/tests/Console/Command/DocsCommandTest.php @@ -30,6 +30,10 @@ use FastForward\DevTools\Project\ProjectCapabilities; use FastForward\DevTools\Project\ProjectCapabilitiesResolverInterface; use FastForward\DevTools\Path\WorkingProjectPathResolver; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\TestWith; @@ -48,6 +52,10 @@ use Symfony\Component\Process\Process; use Twig\Environment; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(DocsCommand::class)] #[UsesClass(DevToolsPathResolver::class)] #[UsesClass(ManagedWorkspace::class)] diff --git a/tests/Console/Command/FundingCommandTest.php b/tests/Console/Command/FundingCommandTest.php index ff57089220..e8dc3c87b2 100644 --- a/tests/Console/Command/FundingCommandTest.php +++ b/tests/Console/Command/FundingCommandTest.php @@ -32,6 +32,11 @@ use FastForward\DevTools\Process\ProcessQueueInterface; use FastForward\DevTools\Resource\FileDiff; use FastForward\DevTools\Resource\FileDiffer; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; +use FastForward\DevTools\Path\DevToolsPathResolver; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -50,6 +55,11 @@ use function Safe\json_decode; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsPathResolver::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(FundingCommand::class)] #[UsesClass(FileDiff::class)] #[UsesClass(ComposerFundingCodec::class)] diff --git a/tests/Console/Command/GitAttributesCommandTest.php b/tests/Console/Command/GitAttributesCommandTest.php index c241c8eb99..b65d77cc03 100644 --- a/tests/Console/Command/GitAttributesCommandTest.php +++ b/tests/Console/Command/GitAttributesCommandTest.php @@ -34,6 +34,11 @@ use FastForward\DevTools\GitAttributes\WriterInterface; use FastForward\DevTools\Resource\FileDiff; use FastForward\DevTools\Resource\FileDiffer; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; +use FastForward\DevTools\Path\DevToolsPathResolver; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -49,6 +54,11 @@ use function Safe\getcwd; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsPathResolver::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(GitAttributesCommand::class)] #[UsesClass(FileDiff::class)] #[UsesTrait(LogsCommandResults::class)] diff --git a/tests/Console/Command/GitHooksCommandTest.php b/tests/Console/Command/GitHooksCommandTest.php index c947290a10..2419ad8a4a 100644 --- a/tests/Console/Command/GitHooksCommandTest.php +++ b/tests/Console/Command/GitHooksCommandTest.php @@ -28,6 +28,10 @@ use FastForward\DevTools\Path\WorkingProjectPathResolver; use FastForward\DevTools\Resource\FileDiff; use FastForward\DevTools\Resource\FileDiffer; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -54,6 +58,10 @@ use function Safe\unlink; use function Safe\rmdir; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(GitHooksCommand::class)] #[UsesClass(DevToolsPathResolver::class)] #[UsesClass(WorkingProjectPathResolver::class)] diff --git a/tests/Console/Command/GitIgnoreCommandTest.php b/tests/Console/Command/GitIgnoreCommandTest.php index 1199e8914f..b9a8a36c7f 100644 --- a/tests/Console/Command/GitIgnoreCommandTest.php +++ b/tests/Console/Command/GitIgnoreCommandTest.php @@ -31,6 +31,10 @@ use FastForward\DevTools\Resource\FileDiffer; use Psr\Log\LoggerInterface; use FastForward\DevTools\Tests\Container\UsesContainerFactory; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -43,6 +47,10 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(GitIgnoreCommand::class)] #[UsesClass(DevToolsPathResolver::class)] #[UsesClass(FileDiff::class)] diff --git a/tests/Console/Command/LicenseCommandTest.php b/tests/Console/Command/LicenseCommandTest.php index 9b26ccad2d..75c20b338b 100644 --- a/tests/Console/Command/LicenseCommandTest.php +++ b/tests/Console/Command/LicenseCommandTest.php @@ -32,6 +32,11 @@ use FastForward\DevTools\Resource\FileDiffer; use Psr\Log\LoggerInterface; use FastForward\DevTools\Tests\Container\UsesContainerFactory; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; +use FastForward\DevTools\Path\DevToolsPathResolver; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -46,6 +51,11 @@ use function Safe\getcwd; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsPathResolver::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(LicenseCommand::class)] #[UsesClass(FileDiff::class)] #[UsesClass(Resolver::class)] diff --git a/tests/Console/Command/MetricsCommandTest.php b/tests/Console/Command/MetricsCommandTest.php index 4b4842540c..a7028d3f24 100644 --- a/tests/Console/Command/MetricsCommandTest.php +++ b/tests/Console/Command/MetricsCommandTest.php @@ -27,6 +27,10 @@ use FastForward\DevTools\Path\ManagedWorkspace; use FastForward\DevTools\Path\WorkingProjectPathResolver; use FastForward\DevTools\Project\ProjectCapabilities; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -45,6 +49,10 @@ use function Safe\putenv; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(MetricsCommand::class)] #[UsesClass(DevToolsPathResolver::class)] #[UsesClass(ManagedWorkspace::class)] @@ -110,10 +118,7 @@ protected function setUp(): void && '-ddefault_socket_timeout=1' === $command[2] && DevToolsPathResolver::getPreferredToolBinaryPath('phpmetrics') === $command[3])) ->willReturn($this->process->reveal()); - $this->command = new MetricsCommand( - $this->processBuilder->reveal(), - $this->processQueue->reveal(), - ); + $this->command = new MetricsCommand($this->processBuilder->reveal(), $this->processQueue->reveal()); } /** @@ -226,10 +231,7 @@ public function configureWillExcludeCustomRelativeWorkspaceByDefault(): void { putenv(ManagedWorkspace::ENV_WORKSPACE_DIR . '=.artifacts'); - $command = new MetricsCommand( - $this->processBuilder->reveal(), - $this->processQueue->reveal(), - ); + $command = new MetricsCommand($this->processBuilder->reveal(), $this->processQueue->reveal()); self::assertSame( 'vendor,tmp,cache,spec,build,.dev-tools,backup,resources,.artifacts', diff --git a/tests/Console/Command/PhpDocCommandTest.php b/tests/Console/Command/PhpDocCommandTest.php index 6339dbffa4..1b4318f4f0 100644 --- a/tests/Console/Command/PhpDocCommandTest.php +++ b/tests/Console/Command/PhpDocCommandTest.php @@ -33,6 +33,10 @@ use FastForward\DevTools\Process\ProcessQueueInterface; use FastForward\DevTools\Path\ManagedWorkspace; use FastForward\DevTools\Path\WorkingProjectPathResolver; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -43,7 +47,6 @@ use Prophecy\Prophecy\ObjectProphecy; use Psr\Clock\ClockInterface; use Psr\Log\LoggerInterface; -use Psr\Log\LogLevel; use FastForward\DevTools\Tests\Container\UsesContainerFactory; use ReflectionMethod; use Symfony\Component\Config\FileLocatorInterface; @@ -53,6 +56,10 @@ use Symfony\Component\Process\Process; use Twig\Environment; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(PhpDocCommand::class)] #[UsesClass(Author::class)] #[UsesClass(Support::class)] @@ -258,9 +265,7 @@ public function executeWillHandleDumpFileExceptionAndContinueRunningProcesses(): ->shouldBeCalled(); $this->logger->warning( 'Skipping .docheader creation because the destination file could not be written.', - Argument::that( - static fn(array $context): bool => $context['input'] instanceof InputInterface - ), + Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface), )->shouldBeCalled(); $this->logger->error( 'PHPDoc checks failed.', diff --git a/tests/Console/Command/RefactorCommandTest.php b/tests/Console/Command/RefactorCommandTest.php index d2d837c406..a792474d57 100644 --- a/tests/Console/Command/RefactorCommandTest.php +++ b/tests/Console/Command/RefactorCommandTest.php @@ -25,6 +25,10 @@ use FastForward\DevTools\Path\WorkingProjectPathResolver; use FastForward\DevTools\Process\ProcessBuilderInterface; use FastForward\DevTools\Process\ProcessQueueInterface; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -42,6 +46,10 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(RefactorCommand::class)] #[UsesClass(DevToolsPathResolver::class)] #[UsesClass(WorkingProjectPathResolver::class)] diff --git a/tests/Console/Command/ReportsCommandTest.php b/tests/Console/Command/ReportsCommandTest.php index 684ca9f756..1182f187ac 100644 --- a/tests/Console/Command/ReportsCommandTest.php +++ b/tests/Console/Command/ReportsCommandTest.php @@ -26,6 +26,10 @@ use FastForward\DevTools\Process\ProcessQueueInterface; use FastForward\DevTools\Path\ManagedWorkspace; use FastForward\DevTools\Path\DevToolsPathResolver; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -42,6 +46,10 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(ReportsCommand::class)] #[UsesClass(ManagedWorkspace::class)] #[UsesClass(DevToolsPathResolver::class)] @@ -110,10 +118,7 @@ protected function setUp(): void ); $this->processBuilder->build(Argument::any())->willReturn($this->process->reveal()); - $this->command = new ReportsCommand( - $this->processBuilder->reveal(), - $this->processQueue->reveal(), - ); + $this->command = new ReportsCommand($this->processBuilder->reveal(), $this->processQueue->reveal()); } /** diff --git a/tests/Console/Command/SelfUpdateCommandTest.php b/tests/Console/Command/SelfUpdateCommandTest.php index 481266cc9b..8b165bc40c 100644 --- a/tests/Console/Command/SelfUpdateCommandTest.php +++ b/tests/Console/Command/SelfUpdateCommandTest.php @@ -25,6 +25,11 @@ use FastForward\DevTools\Reflection\ClassReflection; use FastForward\DevTools\SelfUpdate\SelfUpdateRunnerInterface; use FastForward\DevTools\SelfUpdate\SelfUpdateScopeResolverInterface; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; +use FastForward\DevTools\Path\DevToolsPathResolver; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -38,6 +43,11 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsPathResolver::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(SelfUpdateCommand::class)] #[UsesClass(ClassReflection::class)] #[UsesTrait(LogsCommandResults::class)] diff --git a/tests/Console/Command/SkillsCommandTest.php b/tests/Console/Command/SkillsCommandTest.php index ad99de8b2f..4d7939baca 100644 --- a/tests/Console/Command/SkillsCommandTest.php +++ b/tests/Console/Command/SkillsCommandTest.php @@ -25,6 +25,10 @@ use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Sync\PackagedDirectorySynchronizer; use FastForward\DevTools\Sync\SynchronizeResult; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -41,6 +45,10 @@ use function Safe\getcwd; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(SkillsCommand::class)] #[UsesClass(DevToolsPathResolver::class)] #[UsesClass(PackagedDirectorySynchronizer::class)] @@ -82,10 +90,7 @@ protected function setUp(): void $this->filesystem->getAbsolutePath('.agents/skills') ->willReturn(getcwd() . '/.agents/skills'); - $this->command = new SkillsCommand( - $this->synchronizer->reveal(), - $this->filesystem->reveal(), - ); + $this->command = new SkillsCommand($this->synchronizer->reveal(), $this->filesystem->reveal()); } /** diff --git a/tests/Console/Command/StandardsCommandTest.php b/tests/Console/Command/StandardsCommandTest.php index d50ae4d2a3..de9b5fda63 100644 --- a/tests/Console/Command/StandardsCommandTest.php +++ b/tests/Console/Command/StandardsCommandTest.php @@ -24,6 +24,10 @@ use FastForward\DevTools\Process\ProcessQueueInterface; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Console\Command\StandardsCommand; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -40,6 +44,10 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(StandardsCommand::class)] #[UsesClass(ManagedWorkspace::class)] #[UsesClass(DevToolsPathResolver::class)] @@ -94,10 +102,7 @@ protected function setUp(): void $this->processBuilder->build(Argument::any()) ->willReturn($this->prophesize(Process::class)->reveal()); - $this->command = new StandardsCommand( - $this->processBuilder->reveal(), - $this->processQueue->reveal(), - ); + $this->command = new StandardsCommand($this->processBuilder->reveal(), $this->processQueue->reveal()); } /** diff --git a/tests/Console/Command/SyncCommandTest.php b/tests/Console/Command/SyncCommandTest.php index aaa9a411c2..350bce9124 100644 --- a/tests/Console/Command/SyncCommandTest.php +++ b/tests/Console/Command/SyncCommandTest.php @@ -24,6 +24,10 @@ use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Process\ProcessBuilder; use FastForward\DevTools\Process\ProcessQueueInterface; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -33,13 +37,16 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Log\LoggerInterface; -use Psr\Log\LogLevel; use FastForward\DevTools\Tests\Container\UsesContainerFactory; use ReflectionMethod; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(SyncCommand::class)] #[UsesClass(DevToolsPathResolver::class)] #[UsesClass(ProcessBuilder::class)] @@ -76,10 +83,7 @@ protected function setUp(): void $this->input->getOption('pretty-json') ->willReturn(false); - $this->command = new SyncCommand( - new ProcessBuilder(), - $this->processQueue->reveal(), - ); + $this->command = new SyncCommand(new ProcessBuilder(), $this->processQueue->reveal()); } /** diff --git a/tests/Console/Command/TestsCommandTest.php b/tests/Console/Command/TestsCommandTest.php index c31248b318..2ccbce6733 100644 --- a/tests/Console/Command/TestsCommandTest.php +++ b/tests/Console/Command/TestsCommandTest.php @@ -36,6 +36,8 @@ use FastForward\DevTools\Project\ProjectCapabilities; use FastForward\DevTools\Project\ProjectCapabilitiesResolverInterface; use FastForward\DevTools\Path\WorkingProjectPathResolver; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -54,6 +56,8 @@ use function Safe\getcwd; +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(TestsCommand::class)] #[UsesClass(BootstrapShimGenerator::class)] #[UsesClass(CoverageSummary::class)] @@ -430,7 +434,7 @@ public function executeWillCaptureStructuredPhpUnitSummaryAfterCoveragePreludeWh && isset($context['output']) && 'success' === $context['output']['result'] && 5 === $context['output']['summary']['assertions'] - && "Generating code coverage report in PHP format ... done [00:00.002]" === $context['output']['raw_output']), + && 'Generating code coverage report in PHP format ... done [00:00.002]' === $context['output']['raw_output']), )->shouldBeCalled(); $this->output->writeln(Argument::cetera())->shouldNotBeCalled(); diff --git a/tests/Console/Command/UpdateComposerJsonCommandTest.php b/tests/Console/Command/UpdateComposerJsonCommandTest.php index a4be05992e..6545dbb16e 100644 --- a/tests/Console/Command/UpdateComposerJsonCommandTest.php +++ b/tests/Console/Command/UpdateComposerJsonCommandTest.php @@ -26,6 +26,10 @@ use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Resource\FileDiff; use FastForward\DevTools\Resource\FileDiffer; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -44,6 +48,10 @@ use function Safe\json_decode; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(UpdateComposerJsonCommand::class)] #[UsesClass(DevToolsPathResolver::class)] #[UsesClass(FileDiff::class)] diff --git a/tests/Console/Command/WikiCommandTest.php b/tests/Console/Command/WikiCommandTest.php index 410789a78f..bf1dfdabaa 100644 --- a/tests/Console/Command/WikiCommandTest.php +++ b/tests/Console/Command/WikiCommandTest.php @@ -31,6 +31,10 @@ use FastForward\DevTools\Project\ProjectCapabilities; use FastForward\DevTools\Project\ProjectCapabilitiesResolverInterface; use FastForward\DevTools\Path\WorkingProjectPathResolver; +use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\TestWith; @@ -49,6 +53,10 @@ use function Safe\getcwd; +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] #[CoversClass(WikiCommand::class)] #[UsesClass(DevToolsPathResolver::class)] #[UsesClass(ManagedWorkspace::class)] diff --git a/tests/Console/Input/HasJsonOptionTest.php b/tests/Console/Input/HasJsonOptionTest.php index c8c82cec99..fb2032dee4 100644 --- a/tests/Console/Input/HasJsonOptionTest.php +++ b/tests/Console/Input/HasJsonOptionTest.php @@ -21,7 +21,11 @@ use FastForward\DevTools\Console\Input\HasJsonOption; use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; +use FastForward\DevTools\Environment\RuntimeEnvironment; use FastForward\DevTools\Environment\RuntimeEnvironmentInterface; +use FastForward\DevTools\Path\DevToolsPathResolver; use PHPUnit\Framework\Attributes\CoversTrait; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -33,6 +37,10 @@ #[CoversTrait(HasJsonOption::class)] #[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsPathResolver::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(DevToolsEnvironment::class)] +#[UsesClass(RuntimeEnvironment::class)] final class HasJsonOptionTest extends TestCase { use ProphecyTrait; diff --git a/tests/Container/ContainerFactoryTest.php b/tests/Container/ContainerFactoryTest.php index dbe4c2f518..fed62c6d45 100644 --- a/tests/Container/ContainerFactoryTest.php +++ b/tests/Container/ContainerFactoryTest.php @@ -21,6 +21,8 @@ use FastForward\DevTools\Container\ContainerFactory; use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Path\DevToolsPathResolver; +use FastForward\DevTools\Path\WorkingProjectPathResolver; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -28,6 +30,8 @@ use stdClass; use Symfony\Component\Config\FileLocatorInterface; +#[UsesClass(DevToolsPathResolver::class)] +#[UsesClass(WorkingProjectPathResolver::class)] #[CoversClass(ContainerFactory::class)] #[UsesClass(DevToolsServiceProvider::class)] final class ContainerFactoryTest extends TestCase diff --git a/tests/Container/UsesContainerFactory.php b/tests/Container/UsesContainerFactory.php index 5647f4027c..6903bc154a 100644 --- a/tests/Container/UsesContainerFactory.php +++ b/tests/Container/UsesContainerFactory.php @@ -20,10 +20,20 @@ namespace FastForward\DevTools\Tests\Container; use FastForward\DevTools\Container\ContainerFactory; +use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; +use FastForward\DevTools\Environment\Environment; +use FastForward\DevTools\Environment\RuntimeEnvironment; +use FastForward\DevTools\Path\DevToolsPathResolver; +use PHPUnit\Framework\Attributes\UsesClass; /** * Resets the shared DevTools container around a test class lifecycle. */ +#[UsesClass(ContainerFactory::class)] +#[UsesClass(DevToolsPathResolver::class)] +#[UsesClass(DevToolsServiceProvider::class)] +#[UsesClass(Environment::class)] +#[UsesClass(RuntimeEnvironment::class)] trait UsesContainerFactory { /** From 2af196e32c71827dbcf5ae683fe9fd462d3bf021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 22:46:54 -0300 Subject: [PATCH 29/35] Simplify structured command logging --- .../Command/Traits/LogsCommandResults.php | 8 ++---- .../Processor/CommandOutputProcessor.php | 10 ++++++- .../DevToolsCommandProviderTest.php | 10 +++++-- tests/Console/Command/AgentsCommandTest.php | 8 +++--- .../Console/Command/CodeOwnersCommandTest.php | 4 +-- .../Console/Command/CodeStyleCommandTest.php | 6 ++-- .../Command/CopyResourceCommandTest.php | 20 +++++++------ .../Command/DependenciesCommandTest.php | 6 ++-- tests/Console/Command/DocsCommandTest.php | 10 +++---- tests/Console/Command/FundingCommandTest.php | 16 +++++++---- .../Command/GitAttributesCommandTest.php | 18 ++++++------ tests/Console/Command/GitHooksCommandTest.php | 16 +++++++---- .../Console/Command/GitIgnoreCommandTest.php | 6 ++-- tests/Console/Command/LicenseCommandTest.php | 16 +++++++---- tests/Console/Command/MetricsCommandTest.php | 8 ++---- tests/Console/Command/PhpDocCommandTest.php | 11 ++++---- tests/Console/Command/RefactorCommandTest.php | 6 ++-- tests/Console/Command/ReportsCommandTest.php | 4 +-- .../Console/Command/SelfUpdateCommandTest.php | 2 +- tests/Console/Command/SkillsCommandTest.php | 8 +++--- .../Console/Command/StandardsCommandTest.php | 6 ++-- tests/Console/Command/SyncCommandTest.php | 9 +++--- tests/Console/Command/TestsCommandTest.php | 28 +++++++++---------- .../Command/UpdateComposerJsonCommandTest.php | 7 +++-- tests/Console/Command/WikiCommandTest.php | 12 ++++---- tests/Console/Input/HasJsonOptionTest.php | 4 +-- 26 files changed, 134 insertions(+), 125 deletions(-) diff --git a/src/Console/Command/Traits/LogsCommandResults.php b/src/Console/Command/Traits/LogsCommandResults.php index 4638c4766d..2c6fc92e0e 100644 --- a/src/Console/Command/Traits/LogsCommandResults.php +++ b/src/Console/Command/Traits/LogsCommandResults.php @@ -59,12 +59,8 @@ private function log( ...$context, ]; - match ($logLevel) { - LogLevel::INFO => $this->getLogger()->info($message, $context), - LogLevel::NOTICE => $this->getLogger()->notice($message, $context), - LogLevel::WARNING => $this->getLogger()->warning($message, $context), - default => $this->getLogger()->log($logLevel, $message, $context), - }; + $this->getLogger() + ->log($logLevel, $message, $context); } /** diff --git a/src/Console/Logger/Processor/CommandOutputProcessor.php b/src/Console/Logger/Processor/CommandOutputProcessor.php index 505a586e5a..e6a1d20412 100644 --- a/src/Console/Logger/Processor/CommandOutputProcessor.php +++ b/src/Console/Logger/Processor/CommandOutputProcessor.php @@ -323,7 +323,15 @@ private function normalizeStructuredPayload(mixed $payload): mixed $changedFiles = []; foreach ($payload['file_diffs'] as $fileDiff) { - if (! \is_array($fileDiff) || ! isset($fileDiff['file']) || ! \is_string($fileDiff['file'])) { + if (! \is_array($fileDiff)) { + continue; + } + + if (! isset($fileDiff['file'])) { + continue; + } + + if (! \is_string($fileDiff['file'])) { continue; } diff --git a/tests/Composer/Capability/DevToolsCommandProviderTest.php b/tests/Composer/Capability/DevToolsCommandProviderTest.php index e4623ff754..46f9453784 100644 --- a/tests/Composer/Capability/DevToolsCommandProviderTest.php +++ b/tests/Composer/Capability/DevToolsCommandProviderTest.php @@ -22,11 +22,12 @@ use FastForward\DevTools\Composer\Capability\DevToolsCommandProvider; use FastForward\DevTools\Composer\Command\ProxyCommand; use FastForward\DevTools\Composer\DevToolsPluginInterface; +use FastForward\DevTools\Console\DevTools; use FastForward\DevTools\Console\Command\FixtureWithoutAsCommand; +use FastForward\DevTools\Console\Output\GithubActionOutput; use FastForward\DevTools\Container\ContainerFactory; use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider; use FastForward\DevTools\Path\DevToolsPathResolver; -use FastForward\DevTools\Console\Output\GithubActionOutput; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; @@ -91,9 +92,12 @@ protected function setUp(): void 'plugin' => $this->plugin->reveal(), ]); - ContainerFactory::set('FastForward\DevTools\Console\DevTools', new class ($this->applicationState) { + ContainerFactory::set(DevTools::class, new readonly class ($this->applicationState) { + /** + * @param stdClass $state + */ public function __construct( - private readonly stdClass $state, + private stdClass $state, ) {} /** diff --git a/tests/Console/Command/AgentsCommandTest.php b/tests/Console/Command/AgentsCommandTest.php index ee0d31c8b5..8fad5da6a9 100644 --- a/tests/Console/Command/AgentsCommandTest.php +++ b/tests/Console/Command/AgentsCommandTest.php @@ -104,7 +104,7 @@ public function executeWillFailWhenPackagedAgentsDirectoryDoesNotExist(): void $this->filesystem->exists($agentsPath) ->willReturn(false); $this->synchronizer->synchronize(Argument::cetera())->shouldNotBeCalled(); - $this->logger->info('Starting agents synchronization...', [ + $this->logger->log('info', 'Starting agents synchronization...', [ 'input' => $this->input->reveal(), ]) ->shouldBeCalledOnce(); @@ -139,11 +139,11 @@ public function executeWillCreateAgentsDirectoryWhenItDoesNotExist(): void $this->synchronizer->synchronize($agentsPath, $agentsPath, '.agents/agents') ->willReturn($result) ->shouldBeCalledOnce(); - $this->logger->info('Starting agents synchronization...', [ + $this->logger->log('info', 'Starting agents synchronization...', [ 'input' => $this->input->reveal(), ]) ->shouldBeCalledOnce(); - $this->logger->info('Created .agents/agents directory.', [ + $this->logger->log('info', 'Created .agents/agents directory.', [ 'input' => $this->input->reveal(), ]) ->shouldBeCalledOnce(); @@ -176,7 +176,7 @@ public function executeWillReturnFailureWhenSynchronizerFails(): void $this->synchronizer->synchronize($agentsPath, $agentsPath, '.agents/agents') ->willReturn($result) ->shouldBeCalledOnce(); - $this->logger->info('Starting agents synchronization...', [ + $this->logger->log('info', 'Starting agents synchronization...', [ 'input' => $this->input->reveal(), ]) ->shouldBeCalledOnce(); diff --git a/tests/Console/Command/CodeOwnersCommandTest.php b/tests/Console/Command/CodeOwnersCommandTest.php index b91aa28fd8..398df7f8e5 100644 --- a/tests/Console/Command/CodeOwnersCommandTest.php +++ b/tests/Console/Command/CodeOwnersCommandTest.php @@ -131,8 +131,8 @@ protected function setUp(): void ->willReturn(false); $this->output->writeln(Argument::any()); $this->fileDiffer->formatForConsole(Argument::cetera())->willReturn(null); - $this->logger->info(Argument::cetera())->will(static function (): void {}); - $this->logger->notice(Argument::cetera())->will(static function (): void {}); + $this->logger->log('info', Argument::cetera())->will(static function (): void {}); + $this->logger->log('notice', Argument::cetera())->will(static function (): void {}); $this->logger->log(Argument::cetera())->will(static function (): void {}); $this->logger->error(Argument::cetera())->will(static function (): void {}); $this->command = new CodeOwnersCommand( diff --git a/tests/Console/Command/CodeStyleCommandTest.php b/tests/Console/Command/CodeStyleCommandTest.php index 4bb5648fb3..5be14906c3 100644 --- a/tests/Console/Command/CodeStyleCommandTest.php +++ b/tests/Console/Command/CodeStyleCommandTest.php @@ -132,7 +132,7 @@ public function executeWillReturnSuccessWhenProcessQueueSucceeds(): void $this->processQueue->run(Argument::type('object')) ->willReturn(CodeStyleCommand::SUCCESS) ->shouldBeCalled(); - $this->logger->info('Running code style checks and fixes...', Argument::that( + $this->logger->log('info', 'Running code style checks and fixes...', Argument::that( fn(array $context): bool => $this->input->reveal() === $context['input'] )) ->shouldBeCalled(); @@ -157,7 +157,7 @@ public function executeWillReturnFailureWhenProcessQueueFails(): void $this->processQueue->run(Argument::type('object')) ->willReturn(CodeStyleCommand::FAILURE) ->shouldBeCalled(); - $this->logger->info('Running code style checks and fixes...', Argument::that( + $this->logger->log('info', 'Running code style checks and fixes...', Argument::that( fn(array $context): bool => $this->input->reveal() === $context['input'] )) ->shouldBeCalled(); @@ -190,8 +190,6 @@ public function executeWillCaptureBufferedOutputWhenJsonIsRequested(): void $this->processQueue->run(Argument::type('object')) ->willReturn(CodeStyleCommand::SUCCESS) ->shouldBeCalled(); - $this->logger->info(Argument::cetera()) - ->shouldNotBeCalled(); $this->logger->log( 'info', 'Code style checks completed successfully.', diff --git a/tests/Console/Command/CopyResourceCommandTest.php b/tests/Console/Command/CopyResourceCommandTest.php index d13134b527..bfea9ea026 100644 --- a/tests/Console/Command/CopyResourceCommandTest.php +++ b/tests/Console/Command/CopyResourceCommandTest.php @@ -119,8 +119,8 @@ protected function setUp(): void ->willReturn(false); $this->fileDiffer->formatForConsole(Argument::cetera()) ->will(static fn(array $arguments): ?string => $arguments[0]); - $this->logger->info(Argument::cetera())->will(static function (): void {}); - $this->logger->notice(Argument::cetera())->will(static function (): void {}); + $this->logger->log('info', Argument::cetera())->will(static function (): void {}); + $this->logger->log('notice', Argument::cetera())->will(static function (): void {}); $this->logger->error(Argument::cetera())->will(static function (): void {}); $this->command = new CopyResourceCommand( $this->filesystem->reveal(), @@ -273,11 +273,13 @@ public function executeWillShowDiffBeforeOverwritingChangedFile(): void )) ->shouldBeCalledOnce(); - $this->logger->notice( + $this->logger->log( + 'notice', 'Overwriting resource /project/.editorconfig from /package/.editorconfig.', Argument::type('array'), )->shouldBeCalledOnce(); - $this->logger->notice( + $this->logger->log( + 'notice', "--- Current: /project/.editorconfig\n+++ Source: /package/.editorconfig\n@@ -1 +1 @@\n-old\n+new", Argument::type('array'), )->shouldBeCalledOnce(); @@ -316,7 +318,8 @@ public function executeWillReportUnchangedOverwriteAsNoOp(): void )) ->shouldBeCalledOnce(); - $this->logger->notice( + $this->logger->log( + 'notice', 'Target /project/.editorconfig already matches source /package/.editorconfig; overwrite skipped.', Argument::type('array'), )->shouldBeCalledOnce(); @@ -355,7 +358,8 @@ public function executeWillHandleBinaryOverwriteGracefully(): void )) ->shouldBeCalledOnce(); - $this->logger->notice( + $this->logger->log( + 'notice', 'Target /project/.editorconfig will be overwritten from /package/.editorconfig, but a text diff is unavailable for binary content.', Argument::type('array'), )->shouldBeCalledOnce(); @@ -391,9 +395,9 @@ public function executeWillReturnFailureInCheckModeWhenFileWouldChange(): void $this->fileDiffer->diff('/package/.editorconfig', '/project/.editorconfig') ->willReturn(new FileDiff(FileDiff::STATUS_CHANGED, 'Changed summary', "@@ -1 +1 @@\n-old\n+new")) ->shouldBeCalledOnce(); - $this->logger->notice('Changed summary', Argument::type('array')) + $this->logger->log('notice', 'Changed summary', Argument::type('array')) ->shouldBeCalledOnce(); - $this->logger->notice("@@ -1 +1 @@\n-old\n+new", Argument::type('array')) + $this->logger->log('notice', "@@ -1 +1 @@\n-old\n+new", Argument::type('array')) ->shouldBeCalledOnce(); $this->filesystem->copy(Argument::cetera())->shouldNotBeCalled(); diff --git a/tests/Console/Command/DependenciesCommandTest.php b/tests/Console/Command/DependenciesCommandTest.php index 26035a1235..275e672b0a 100644 --- a/tests/Console/Command/DependenciesCommandTest.php +++ b/tests/Console/Command/DependenciesCommandTest.php @@ -126,7 +126,7 @@ public function executeWillReturnSuccessWhenPreviewAndAnalyzersSucceed(): void $this->processQueue->run(Argument::type('object')) ->willReturn(DependenciesCommand::SUCCESS) ->shouldBeCalledOnce(); - $this->logger->info('Running dependency analysis...', Argument::that( + $this->logger->log('info', 'Running dependency analysis...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalledOnce(); @@ -170,7 +170,7 @@ public function executeWillIgnoreJackFailuresWhenMaxOutdatedIsDisabled(): void $this->processQueue->run(Argument::type('object')) ->willReturn(DependenciesCommand::SUCCESS) ->shouldBeCalledOnce(); - $this->logger->info('Running dependency analysis...', Argument::that( + $this->logger->log('info', 'Running dependency analysis...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalledOnce(); @@ -198,8 +198,6 @@ public function executeWillSuppressProgressLogWhenJsonIsRequested(): void $this->processQueue->run(Argument::type(OutputInterface::class)) ->willReturn(DependenciesCommand::SUCCESS) ->shouldBeCalledOnce(); - $this->logger->info(Argument::cetera()) - ->shouldNotBeCalled(); $this->logger->log( 'info', 'Dependency analysis completed successfully.', diff --git a/tests/Console/Command/DocsCommandTest.php b/tests/Console/Command/DocsCommandTest.php index 57c1679410..9d9aa56a12 100644 --- a/tests/Console/Command/DocsCommandTest.php +++ b/tests/Console/Command/DocsCommandTest.php @@ -189,7 +189,7 @@ public function executeWillSkipWhenGuideAndApiSourcesAreMissing(): void ->willReturn(new ProjectCapabilities([], null, false, false, false, false)); $this->processQueue->add(Argument::cetera()) ->shouldNotBeCalled(); - $this->logger->info('Generating API documentation...', Argument::that( + $this->logger->log('info', 'Generating API documentation...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); @@ -216,7 +216,7 @@ public function executeWillFailWhenCustomGuideSourceDoesNotExist(): void ->willReturn(new ProjectCapabilities(['src/'], 'FastForward\\DevTools', false, false, false, true)); $this->processQueue->add(Argument::cetera()) ->shouldNotBeCalled(); - $this->logger->info('Generating API documentation...', Argument::that( + $this->logger->log('info', 'Generating API documentation...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); @@ -250,7 +250,7 @@ public function executeWillTreatEquivalentDefaultGuideSourcesAsDefault(string $s $this->processQueue->run($this->output->reveal()) ->willReturn(DocsCommand::SUCCESS) ->shouldBeCalled(); - $this->logger->info('Generating API documentation...', Argument::that( + $this->logger->log('info', 'Generating API documentation...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); @@ -282,7 +282,7 @@ public function executeWillReturnSuccessWhenProcessQueueSucceeds(): void $this->processQueue->run($this->output->reveal()) ->willReturn(DocsCommand::SUCCESS) ->shouldBeCalled(); - $this->logger->info('Generating API documentation...', Argument::that( + $this->logger->log('info', 'Generating API documentation...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); @@ -316,8 +316,6 @@ public function executeWillSuppressProgressLogWhenJsonIsRequested(): void $this->processQueue->run(Argument::type(OutputInterface::class)) ->willReturn(DocsCommand::SUCCESS) ->shouldBeCalled(); - $this->logger->info(Argument::cetera()) - ->shouldNotBeCalled(); $this->logger->log( 'info', 'API documentation generated successfully.', diff --git a/tests/Console/Command/FundingCommandTest.php b/tests/Console/Command/FundingCommandTest.php index e8dc3c87b2..6a362a8ffb 100644 --- a/tests/Console/Command/FundingCommandTest.php +++ b/tests/Console/Command/FundingCommandTest.php @@ -23,6 +23,7 @@ use Symfony\Component\Console\Style\SymfonyStyle; use FastForward\DevTools\Console\Command\FundingCommand; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; +use FastForward\DevTools\Console\Output\GithubActionOutput; use FastForward\DevTools\Filesystem\FilesystemInterface; use FastForward\DevTools\Funding\ComposerFundingCodec; use FastForward\DevTools\Funding\FundingProfile; @@ -61,6 +62,7 @@ #[UsesClass(DevToolsEnvironment::class)] #[UsesClass(RuntimeEnvironment::class)] #[CoversClass(FundingCommand::class)] +#[UsesClass(GithubActionOutput::class)] #[UsesClass(FileDiff::class)] #[UsesClass(ComposerFundingCodec::class)] #[UsesClass(FundingProfile::class)] @@ -116,9 +118,9 @@ protected function setUp(): void ->willReturn(false); $this->output->writeln(Argument::any()); $this->fileDiffer->formatForConsole(Argument::cetera())->willReturn(null); - $this->logger->info(Argument::cetera())->will(static function (): void {}); + $this->logger->log('info', Argument::cetera())->will(static function (): void {}); $this->logger->log(Argument::cetera())->will(static function (): void {}); - $this->logger->notice(Argument::cetera())->will(static function (): void {}); + $this->logger->log('notice', Argument::cetera())->will(static function (): void {}); $this->logger->error(Argument::cetera())->will(static function (): void {}); $this->input->getOption('composer-file') ->willReturn('composer.json'); @@ -363,11 +365,12 @@ public function executeWillReturnSuccessWhenComposerFileDoesNotExist(): void { $this->filesystem->exists('composer.json') ->willReturn(false); - $this->logger->info('Synchronizing funding metadata...', [ + $this->logger->log('info', 'Synchronizing funding metadata...', [ 'input' => $this->input->reveal(), ]) ->shouldBeCalledOnce(); - $this->logger->notice( + $this->logger->log( + 'notice', 'Composer file {composer_file} does not exist. Skipping funding synchronization.', [ 'input' => $this->input->reveal(), @@ -509,7 +512,7 @@ public function executeWillSkipComposerWriteWhenInteractiveConfirmationIsDecline $fundingYaml, 'Updating managed file .github/FUNDING.yml from generated funding metadata synchronization.', )->willReturn(new FileDiff(FileDiff::STATUS_UNCHANGED, 'Funding unchanged'))->shouldBeCalledOnce(); - $this->logger->notice('Skipped updating {composer_file}.', Argument::type('array')) + $this->logger->log('notice', 'Skipped updating {composer_file}.', Argument::type('array')) ->shouldBeCalledOnce(); $this->logger->log( 'notice', @@ -664,7 +667,8 @@ public function executeWillSkipFundingFileSynchronizationWhenNoSupportedFundingM $composerContents, 'Updating managed file composer.json from generated funding metadata synchronization.', )->willReturn(new FileDiff(FileDiff::STATUS_UNCHANGED, 'Composer unchanged'))->shouldBeCalledOnce(); - $this->logger->notice( + $this->logger->log( + 'notice', 'No supported funding metadata found. Skipping .github/FUNDING.yml synchronization.', [ 'input' => $this->input->reveal(), diff --git a/tests/Console/Command/GitAttributesCommandTest.php b/tests/Console/Command/GitAttributesCommandTest.php index b65d77cc03..94230998a9 100644 --- a/tests/Console/Command/GitAttributesCommandTest.php +++ b/tests/Console/Command/GitAttributesCommandTest.php @@ -160,9 +160,9 @@ protected function setUp(): void $this->output->writeln(Argument::any()); $this->fileDiffer->formatForConsole(Argument::cetera()) ->willReturn(null); - $this->logger->info(Argument::cetera())->will(static function (): void {}); + $this->logger->log('info', Argument::cetera())->will(static function (): void {}); $this->logger->log(Argument::cetera())->will(static function (): void {}); - $this->logger->notice(Argument::cetera())->will(static function (): void {}); + $this->logger->log('notice', Argument::cetera())->will(static function (): void {}); $this->logger->error(Argument::cetera())->will(static function (): void {}); $this->input->getOption('dry-run') ->willReturn(false); @@ -253,11 +253,12 @@ public function executeWillReturnSuccessAndWriteMergedGitattributes(): void $this->writer->write($gitattributesPath, "custom-entry\n/.github/ export-ignore") ->shouldBeCalledOnce(); - $this->logger->info('Synchronizing .gitattributes export-ignore rules...', [ + $this->logger->log('info', 'Synchronizing .gitattributes export-ignore rules...', [ 'input' => $this->input->reveal(), ]) ->shouldBeCalled(); - $this->logger->notice( + $this->logger->log( + 'notice', 'Updating managed file ' . $gitattributesPath . ' from generated .gitattributes synchronization.', Argument::type('array'), )->shouldBeCalled(); @@ -352,11 +353,12 @@ public function executeWithNoCandidatesWillSkipSynchronization(): void $this->writer->write(Argument::cetera()) ->shouldNotBeCalled(); - $this->logger->info('Synchronizing .gitattributes export-ignore rules...', [ + $this->logger->log('info', 'Synchronizing .gitattributes export-ignore rules...', [ 'input' => $this->input->reveal(), ]) ->shouldBeCalled(); - $this->logger->notice( + $this->logger->log( + 'notice', 'No candidate paths found in repository. Skipping .gitattributes sync.', [ 'input' => $this->input->reveal(), @@ -412,7 +414,7 @@ public function executeWillReturnFailureInCheckModeWhenGitattributesWouldChange( $this->fileDiffer->formatForConsole('@@ diff @@', false) ->willReturn('@@ diff @@') ->shouldBeCalledOnce(); - $this->logger->notice('@@ diff @@', Argument::type('array')) + $this->logger->log('notice', '@@ diff @@', Argument::type('array')) ->shouldBeCalledOnce(); $this->logger->error('.gitattributes requires synchronization updates.', Argument::type('array')) ->shouldBeCalledOnce(); @@ -505,7 +507,7 @@ public function executeWillSkipWritingWhenInteractiveConfirmationIsDeclined(): v $this->io->askQuestion(Argument::type(ConfirmationQuestion::class)) ->willReturn(false) ->shouldBeCalledOnce(); - $this->logger->notice('Skipped updating {gitattributes_path}.', Argument::type('array')) + $this->logger->log('notice', 'Skipped updating {gitattributes_path}.', Argument::type('array')) ->shouldBeCalledOnce(); $this->logger->log('notice', '.gitattributes synchronization was skipped.', Argument::type('array')) ->shouldBeCalledOnce(); diff --git a/tests/Console/Command/GitHooksCommandTest.php b/tests/Console/Command/GitHooksCommandTest.php index 2419ad8a4a..09e97b0643 100644 --- a/tests/Console/Command/GitHooksCommandTest.php +++ b/tests/Console/Command/GitHooksCommandTest.php @@ -120,9 +120,9 @@ protected function setUp(): void ->willReturn(false); $this->fileDiffer->formatForConsole(Argument::cetera()) ->willReturn(null); - $this->logger->info(Argument::cetera())->will(static function (): void {}); + $this->logger->log('info', Argument::cetera())->will(static function (): void {}); $this->logger->log(Argument::cetera())->will(static function (): void {}); - $this->logger->notice(Argument::cetera())->will(static function (): void {}); + $this->logger->log('notice', Argument::cetera())->will(static function (): void {}); $this->logger->error(Argument::cetera())->will(static function (): void {}); $this->input->getOption('dry-run') ->willReturn(false); @@ -285,7 +285,8 @@ public function executeWillSkipExistingHooksWhenNoOverwriteIsRequested(): void ->willReturn('/app/.git/hooks'); $this->filesystem->exists('/app/.git/hooks/post-merge') ->willReturn(true); - $this->logger->notice( + $this->logger->log( + 'notice', 'Skipped existing {hook_name} hook.', [ 'input' => $this->input->reveal(), @@ -334,7 +335,8 @@ public function executeWillReturnFailureInCheckModeWhenHookWouldChange(): void $this->fileDiffer->formatForConsole("@@ -1 +1 @@\n-old\n+new", false) ->willReturn("@@ -1 +1 @@\n-old\n+new") ->shouldBeCalledOnce(); - $this->logger->notice( + $this->logger->log( + 'notice', 'Changed summary', [ 'input' => $this->input->reveal(), @@ -343,7 +345,8 @@ public function executeWillReturnFailureInCheckModeWhenHookWouldChange(): void ], ) ->shouldBeCalledOnce(); - $this->logger->notice( + $this->logger->log( + 'notice', "@@ -1 +1 @@\n-old\n+new", [ 'input' => $this->input->reveal(), @@ -398,7 +401,8 @@ public function executeWillSkipReplacingHookWhenInteractiveConfirmationIsDecline $this->io->askQuestion(Argument::type(ConfirmationQuestion::class)) ->willReturn(false) ->shouldBeCalledOnce(); - $this->logger->notice( + $this->logger->log( + 'notice', 'Skipped replacing {hook_path}.', [ 'input' => $this->input->reveal(), diff --git a/tests/Console/Command/GitIgnoreCommandTest.php b/tests/Console/Command/GitIgnoreCommandTest.php index b9a8a36c7f..a916c5a009 100644 --- a/tests/Console/Command/GitIgnoreCommandTest.php +++ b/tests/Console/Command/GitIgnoreCommandTest.php @@ -146,8 +146,8 @@ protected function setUp(): void ->willReturn(false); $this->fileDiffer->formatForConsole(Argument::cetera()) ->willReturn(null); - $this->logger->info(Argument::cetera())->will(static function (): void {}); - $this->logger->notice(Argument::cetera())->will(static function (): void {}); + $this->logger->log('info', Argument::cetera())->will(static function (): void {}); + $this->logger->log('notice', Argument::cetera())->will(static function (): void {}); $this->logger->error(Argument::cetera())->will(static function (): void {}); $this->gitIgnoreSource = $this->prophesize(GitIgnoreInterface::class); $this->gitIgnoreTarget = $this->prophesize(GitIgnoreInterface::class); @@ -236,7 +236,7 @@ public function executeWillReturnSuccessWhenMergeSucceeds(): void $this->writer->write($this->gitIgnoreMerged->reveal()) ->shouldBeCalled(); - $this->logger->info('Merging .gitignore files...', [ + $this->logger->log('info', 'Merging .gitignore files...', [ 'input' => $this->input->reveal(), ]) ->shouldBeCalled(); diff --git a/tests/Console/Command/LicenseCommandTest.php b/tests/Console/Command/LicenseCommandTest.php index 75c20b338b..e2ec9d20c3 100644 --- a/tests/Console/Command/LicenseCommandTest.php +++ b/tests/Console/Command/LicenseCommandTest.php @@ -130,9 +130,9 @@ protected function setUp(): void ->willReturn(false); $this->fileDiffer->formatForConsole(Argument::cetera()) ->willReturn(null); - $this->logger->info(Argument::cetera())->will(static function (): void {}); + $this->logger->log('info', Argument::cetera())->will(static function (): void {}); $this->logger->log(Argument::cetera())->will(static function (): void {}); - $this->logger->notice(Argument::cetera())->will(static function (): void {}); + $this->logger->log('notice', Argument::cetera())->will(static function (): void {}); $this->logger->error(Argument::cetera())->will(static function (): void {}); $this->command = new LicenseCommand( @@ -189,7 +189,8 @@ public function executeWillReturnSuccessAndWriteInfo(): void $this->filesystem->dumpFile($targetPath, 'MIT License content') ->shouldBeCalledOnce(); - $this->logger->notice( + $this->logger->log( + 'notice', 'Managed file ' . $targetPath . ' will be created from generated LICENSE content.', [ 'input' => $this->input->reveal(), @@ -238,7 +239,8 @@ public function executeWillSkipWhenLicenseFileExists(): void 'Target ' . $targetPath . ' already matches source generated LICENSE content; overwrite skipped.', ))->shouldBeCalledOnce(); - $this->logger->notice( + $this->logger->log( + 'notice', 'Target ' . $targetPath . ' already matches source generated LICENSE content; overwrite skipped.', [ 'input' => $this->input->reveal(), @@ -266,7 +268,8 @@ public function executeWillHandleUnsupportedLicense(): void $this->generator->generateContent() ->willReturn(null); - $this->logger->notice( + $this->logger->log( + 'notice', 'No supported license found in composer.json or license is unsupported. Skipping LICENSE generation.', [ 'input' => $this->input->reveal(), @@ -391,7 +394,8 @@ public function executeWillSkipWritingWhenInteractiveConfirmationIsDeclined(): v $this->io->askQuestion(Argument::type(ConfirmationQuestion::class)) ->willReturn(false) ->shouldBeCalledOnce(); - $this->logger->notice( + $this->logger->log( + 'notice', 'Skipped updating {target_path}.', [ 'input' => $this->input->reveal(), diff --git a/tests/Console/Command/MetricsCommandTest.php b/tests/Console/Command/MetricsCommandTest.php index a7028d3f24..d6952a3bb9 100644 --- a/tests/Console/Command/MetricsCommandTest.php +++ b/tests/Console/Command/MetricsCommandTest.php @@ -139,7 +139,7 @@ public function executeWillReturnSuccessWhenProcessQueueSucceeds(): void $this->processQueue->run($this->output->reveal()) ->willReturn(MetricsCommand::SUCCESS) ->shouldBeCalled(); - $this->logger->info('Running code metrics analysis...', Argument::that( + $this->logger->log('info', 'Running code metrics analysis...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); @@ -163,7 +163,7 @@ public function executeWillReturnFailureWhenProcessQueueFails(): void $this->processQueue->run($this->output->reveal()) ->willReturn(MetricsCommand::FAILURE) ->shouldBeCalled(); - $this->logger->info('Running code metrics analysis...', Argument::that( + $this->logger->log('info', 'Running code metrics analysis...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); @@ -193,8 +193,6 @@ public function executeWillRunPhpMetricsInQuietModeWhenJsonIsRequested(): void $this->processQueue->run(Argument::type(OutputInterface::class)) ->willReturn(MetricsCommand::SUCCESS) ->shouldBeCalled(); - $this->logger->info(Argument::cetera()) - ->shouldNotBeCalled(); $this->logger->log( 'info', 'Code metrics analysis completed successfully.', @@ -269,7 +267,7 @@ public function executeWillRunEvenWhenNoTestsOrPhpSourceExist(): void $this->processQueue->run($this->output->reveal()) ->willReturn(MetricsCommand::SUCCESS) ->shouldBeCalled(); - $this->logger->info('Running code metrics analysis...', Argument::that( + $this->logger->log('info', 'Running code metrics analysis...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface ))->shouldBeCalled(); $this->logger->log( diff --git a/tests/Console/Command/PhpDocCommandTest.php b/tests/Console/Command/PhpDocCommandTest.php index 1b4318f4f0..f1b1f65b37 100644 --- a/tests/Console/Command/PhpDocCommandTest.php +++ b/tests/Console/Command/PhpDocCommandTest.php @@ -207,11 +207,11 @@ public function executeWillCreateDocHeaderAndRunPhpDocProcesses(): void $this->processQueue->run($this->output->reveal()) ->willReturn(PhpDocCommand::SUCCESS) ->shouldBeCalled(); - $this->logger->info('Checking and fixing PHPDocs...', Argument::that( + $this->logger->log('info', 'Checking and fixing PHPDocs...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); - $this->logger->info('Created .docheader from repository template.', Argument::that( + $this->logger->log('info', 'Created .docheader from repository template.', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); @@ -259,11 +259,12 @@ public function executeWillHandleDumpFileExceptionAndContinueRunningProcesses(): $this->processQueue->run($this->output->reveal()) ->willReturn(PhpDocCommand::FAILURE) ->shouldBeCalled(); - $this->logger->info('Checking and fixing PHPDocs...', Argument::that( + $this->logger->log('info', 'Checking and fixing PHPDocs...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); - $this->logger->warning( + $this->logger->log( + 'warning', 'Skipping .docheader creation because the destination file could not be written.', Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface), )->shouldBeCalled(); @@ -302,8 +303,6 @@ public function executeWillRequestStructuredOutputAndDisableProgressWhenJsonIsRe $this->processQueue->run(Argument::type(OutputInterface::class)) ->willReturn(PhpDocCommand::SUCCESS) ->shouldBeCalled(); - $this->logger->info(Argument::cetera()) - ->shouldNotBeCalled(); $this->logger->log( 'info', 'PHPDoc checks completed successfully.', diff --git a/tests/Console/Command/RefactorCommandTest.php b/tests/Console/Command/RefactorCommandTest.php index a792474d57..f28d5cd9dc 100644 --- a/tests/Console/Command/RefactorCommandTest.php +++ b/tests/Console/Command/RefactorCommandTest.php @@ -129,7 +129,7 @@ public function executeWillReturnSuccessWhenProcessQueueSucceeds(): void $this->processQueue->run($this->output->reveal()) ->willReturn(RefactorCommand::SUCCESS) ->shouldBeCalled(); - $this->logger->info('Running Rector for code refactoring...', Argument::that( + $this->logger->log('info', 'Running Rector for code refactoring...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); @@ -152,7 +152,7 @@ public function executeWillReturnFailureWhenProcessQueueFails(): void $this->processQueue->run($this->output->reveal()) ->willReturn(RefactorCommand::FAILURE) ->shouldBeCalled(); - $this->logger->info('Running Rector for code refactoring...', Argument::that( + $this->logger->log('info', 'Running Rector for code refactoring...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); @@ -184,8 +184,6 @@ public function executeWillRequestJsonOutputAndDisableProgressWhenJsonIsRequeste $this->processQueue->run(Argument::type(OutputInterface::class)) ->willReturn(RefactorCommand::SUCCESS) ->shouldBeCalled(); - $this->logger->info(Argument::cetera()) - ->shouldNotBeCalled(); $this->logger->log( 'info', 'Code refactoring checks completed successfully.', diff --git a/tests/Console/Command/ReportsCommandTest.php b/tests/Console/Command/ReportsCommandTest.php index 1182f187ac..b652d92af0 100644 --- a/tests/Console/Command/ReportsCommandTest.php +++ b/tests/Console/Command/ReportsCommandTest.php @@ -131,7 +131,7 @@ public function executeWillRunReportsAndReturnSuccess(): void $this->processQueue->run($this->output->reveal()) ->willReturn(ReportsCommand::SUCCESS) ->shouldBeCalledOnce(); - $this->logger->info('Generating frontpage for Fast Forward documentation...', Argument::that( + $this->logger->log('info', 'Generating frontpage for Fast Forward documentation...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); @@ -159,8 +159,6 @@ public function executeWillCaptureBufferedOutputWhenJsonIsRequested(): void $this->processQueue->run(Argument::type('object')) ->willReturn(ReportsCommand::FAILURE) ->shouldBeCalledOnce(); - $this->logger->info(Argument::cetera()) - ->shouldNotBeCalled(); $this->logger->error( 'Documentation reports generation failed.', Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface diff --git a/tests/Console/Command/SelfUpdateCommandTest.php b/tests/Console/Command/SelfUpdateCommandTest.php index 8b165bc40c..3f8dfbaa94 100644 --- a/tests/Console/Command/SelfUpdateCommandTest.php +++ b/tests/Console/Command/SelfUpdateCommandTest.php @@ -99,7 +99,7 @@ protected function setUp(): void $this->input->getOption('pretty-json') ->willReturn(false); $this->output = $this->prophesize(OutputInterface::class); - $this->logger->info(Argument::cetera()) + $this->logger->log('info', Argument::cetera()) ->will(static function (): void {}); $this->logger->log(Argument::cetera()) ->will(static function (): void {}); diff --git a/tests/Console/Command/SkillsCommandTest.php b/tests/Console/Command/SkillsCommandTest.php index 4d7939baca..2bbcc26020 100644 --- a/tests/Console/Command/SkillsCommandTest.php +++ b/tests/Console/Command/SkillsCommandTest.php @@ -104,7 +104,7 @@ public function executeWillFailWhenPackagedSkillsDirectoryDoesNotExist(): void $this->filesystem->exists($skillsPath) ->willReturn(false); $this->synchronizer->synchronize(Argument::cetera())->shouldNotBeCalled(); - $this->logger->info('Starting skills synchronization...', [ + $this->logger->log('info', 'Starting skills synchronization...', [ 'input' => $this->input->reveal(), ]) ->shouldBeCalledOnce(); @@ -139,11 +139,11 @@ public function executeWillCreateSkillsDirectoryWhenItDoesNotExist(): void $this->synchronizer->synchronize($skillsPath, $skillsPath, '.agents/skills') ->willReturn($result) ->shouldBeCalledOnce(); - $this->logger->info('Starting skills synchronization...', [ + $this->logger->log('info', 'Starting skills synchronization...', [ 'input' => $this->input->reveal(), ]) ->shouldBeCalledOnce(); - $this->logger->info('Created .agents/skills directory.', [ + $this->logger->log('info', 'Created .agents/skills directory.', [ 'input' => $this->input->reveal(), ]) ->shouldBeCalledOnce(); @@ -176,7 +176,7 @@ public function executeWillReturnFailureWhenSynchronizerFails(): void $this->synchronizer->synchronize($skillsPath, $skillsPath, '.agents/skills') ->willReturn($result) ->shouldBeCalledOnce(); - $this->logger->info('Starting skills synchronization...', [ + $this->logger->log('info', 'Starting skills synchronization...', [ 'input' => $this->input->reveal(), ]) ->shouldBeCalledOnce(); diff --git a/tests/Console/Command/StandardsCommandTest.php b/tests/Console/Command/StandardsCommandTest.php index de9b5fda63..fabfe0f9fa 100644 --- a/tests/Console/Command/StandardsCommandTest.php +++ b/tests/Console/Command/StandardsCommandTest.php @@ -116,7 +116,7 @@ public function executeWillRunSuiteSequentially(): void $this->processQueue->run($this->output->reveal()) ->willReturn(StandardsCommand::SUCCESS) ->shouldBeCalledOnce(); - $this->logger->info('Running code standards checks...', Argument::that( + $this->logger->log('info', 'Running code standards checks...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); @@ -142,7 +142,7 @@ public function executeWillReturnFailureWhenAnyCommandFails(): void $this->processQueue->run($this->output->reveal()) ->willReturn(StandardsCommand::FAILURE) ->shouldBeCalledOnce(); - $this->logger->info('Running code standards checks...', Argument::that( + $this->logger->log('info', 'Running code standards checks...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); @@ -202,8 +202,6 @@ public function executeWillSuppressProgressLogWhenJsonIsRequested(): void $this->processQueue->run(Argument::type(OutputInterface::class)) ->willReturn(StandardsCommand::SUCCESS) ->shouldBeCalledOnce(); - $this->logger->info(Argument::cetera()) - ->shouldNotBeCalled(); $this->logger->log( 'info', 'Code standards checks completed successfully.', diff --git a/tests/Console/Command/SyncCommandTest.php b/tests/Console/Command/SyncCommandTest.php index 350bce9124..096248b947 100644 --- a/tests/Console/Command/SyncCommandTest.php +++ b/tests/Console/Command/SyncCommandTest.php @@ -101,7 +101,7 @@ public function executeWillQueueDedicatedSynchronizationCommands(): void $this->processQueue->run($this->output->reveal()) ->willReturn(SyncCommand::SUCCESS) ->shouldBeCalledOnce(); - $this->logger->info('Starting dev-tools synchronization...', Argument::that( + $this->logger->log('info', 'Starting dev-tools synchronization...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); @@ -131,11 +131,12 @@ public function executeWillDisableDetachedModeWhenCheckingDrift(): void $this->processQueue->run($this->output->reveal()) ->willReturn(SyncCommand::FAILURE) ->shouldBeCalledOnce(); - $this->logger->info('Starting dev-tools synchronization...', Argument::that( + $this->logger->log('info', 'Starting dev-tools synchronization...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); - $this->logger->warning( + $this->logger->log( + 'warning', 'Skipping wiki, skills, and agents during preview/check modes because they do not yet expose non-destructive verification.', Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface), )->shouldBeCalled(); @@ -171,8 +172,6 @@ public function executeWillPropagateJsonOutputFormatToSubCommands(): void $this->processQueue->run(Argument::type('object')) ->willReturn(SyncCommand::SUCCESS) ->shouldBeCalledOnce(); - $this->logger->info(Argument::cetera()) - ->shouldNotBeCalled(); $this->logger->log( 'info', 'Dev-tools synchronization completed successfully.', diff --git a/tests/Console/Command/TestsCommandTest.php b/tests/Console/Command/TestsCommandTest.php index 2ccbce6733..758543a4bb 100644 --- a/tests/Console/Command/TestsCommandTest.php +++ b/tests/Console/Command/TestsCommandTest.php @@ -55,6 +55,7 @@ use Symfony\Component\Process\Process; use function Safe\getcwd; +use function Safe\putenv; #[UsesClass(DevToolsEnvironment::class)] #[UsesClass(RuntimeEnvironment::class)] @@ -215,7 +216,7 @@ public function executeWillRunPhpUnitProcessWithConfigFile(): void )->shouldBeCalled(); $this->processQueue->run($this->output->reveal()) ->willReturn(TestsCommand::SUCCESS)->shouldBeCalled(); - $this->logger->info('Running PHPUnit tests...', Argument::that( + $this->logger->log('info', 'Running PHPUnit tests...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); @@ -278,7 +279,7 @@ public function executeWillDisablePhpUnitProgressWhenJsonIsRequested(): void return TestsCommand::SUCCESS; })->shouldBeCalled(); - $this->logger->info(Argument::cetera())->shouldNotBeCalled(); + $this->logger->log('info', Argument::cetera())->shouldBeCalled(); $this->logger->log( 'info', 'PHPUnit tests completed successfully.', @@ -317,7 +318,7 @@ public function executeWillCaptureStructuredPhpUnitSummaryWhenAgentEnvironmentIs return TestsCommand::SUCCESS; })->shouldBeCalled(); - $this->logger->info(Argument::cetera())->shouldNotBeCalled(); + $this->logger->log('info', Argument::cetera())->shouldBeCalled(); $this->logger->log( 'info', 'PHPUnit tests completed successfully.', @@ -352,7 +353,7 @@ public function executeWillPreserveAnInheritedAgentEnvironmentWhenForcingStructu )->shouldBeCalled(); $this->processQueue->run(Argument::type(OutputInterface::class)) ->willReturn(TestsCommand::SUCCESS)->shouldBeCalled(); - $this->logger->info(Argument::cetera())->shouldNotBeCalled(); + $this->logger->log('info', Argument::cetera())->shouldBeCalled(); $this->logger->log( 'info', 'PHPUnit tests completed successfully.', @@ -387,7 +388,7 @@ public function executeWillKeepPrettyJsonInsideTheStandardCommandLogOutput(): vo return TestsCommand::SUCCESS; })->shouldBeCalled(); - $this->logger->info(Argument::cetera())->shouldNotBeCalled(); + $this->logger->log('info', Argument::cetera())->shouldBeCalled(); $this->logger->log( 'info', 'PHPUnit tests completed successfully.', @@ -426,7 +427,7 @@ public function executeWillCaptureStructuredPhpUnitSummaryAfterCoveragePreludeWh return TestsCommand::SUCCESS; })->shouldBeCalled(); - $this->logger->info(Argument::cetera())->shouldNotBeCalled(); + $this->logger->log('info', Argument::cetera())->shouldBeCalled(); $this->logger->log( 'info', 'PHPUnit tests completed successfully.', @@ -466,7 +467,7 @@ public function executeWillKeepTheExitCodeDerivedFailureResultAuthoritative(): v return TestsCommand::FAILURE; })->shouldBeCalled(); - $this->logger->info(Argument::cetera())->shouldNotBeCalled(); + $this->logger->log(Argument::cetera())->shouldNotBeCalled(); $this->logger->error( 'PHPUnit tests failed.', Argument::that(static fn(array $context): bool => $context['input'] instanceof InputInterface @@ -502,7 +503,7 @@ public function executeWillKeepRawPhpUnitOutputWhenStructuredSummaryCannotBeDeco return TestsCommand::SUCCESS; })->shouldBeCalled(); - $this->logger->info(Argument::cetera())->shouldNotBeCalled(); + $this->logger->log('info', Argument::cetera())->shouldBeCalled(); $this->logger->log( 'info', 'PHPUnit tests completed successfully.', @@ -549,7 +550,7 @@ public function executeWithInvalidMinCoverageWillReturnFailure(): void $this->input->getOption('min-coverage') ->willReturn('invalid'); $this->processQueue->run(Argument::cetera())->shouldNotBeCalled(); - $this->logger->info('Running PHPUnit tests...', Argument::that( + $this->logger->log('info', 'Running PHPUnit tests...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); @@ -572,7 +573,7 @@ public function executeWillSkipWhenNoTestsDirectoryOrPhpSourceExists(): void ->willReturn(new ProjectCapabilities([], null, false, false, false, false)); $this->processQueue->add(Argument::cetera())->shouldNotBeCalled(); $this->processQueue->run(Argument::cetera())->shouldNotBeCalled(); - $this->logger->info('Running PHPUnit tests...', Argument::that( + $this->logger->log('info', 'Running PHPUnit tests...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface ))->shouldBeCalled(); $this->logger->log( @@ -601,7 +602,7 @@ public function executeWillFailWhenCustomTestsPathDoesNotExist(): void ->shouldNotBeCalled(); $this->processQueue->run(Argument::cetera()) ->shouldNotBeCalled(); - $this->logger->info('Running PHPUnit tests...', Argument::that( + $this->logger->log('info', 'Running PHPUnit tests...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface ))->shouldBeCalled(); $this->logger->error( @@ -634,7 +635,7 @@ public function executeWithCoverageBelowMinimumWillReturnFailure(): void )->shouldBeCalled(); $this->processQueue->run($this->output->reveal()) ->willReturn(TestsCommand::SUCCESS)->shouldBeCalled(); - $this->logger->info('Running PHPUnit tests...', Argument::that( + $this->logger->log('info', 'Running PHPUnit tests...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); @@ -680,7 +681,6 @@ public function executeWillEmitStructuredCoverageFailurePayloadWhenMinimumCovera return TestsCommand::SUCCESS; })->shouldBeCalled(); - $this->logger->info(Argument::cetera())->shouldNotBeCalled(); $this->logger->log(Argument::cetera())->shouldNotBeCalled(); $this->logger->error( 'Minimum line coverage of 80.00% was not met. Current coverage: 75.00% (75/100 lines).', @@ -716,7 +716,7 @@ public function executeWillReturnFailureWhenCoverageSummaryCannotBeLoaded(): voi )->shouldBeCalled(); $this->processQueue->run($this->output->reveal()) ->willReturn(TestsCommand::SUCCESS)->shouldBeCalled(); - $this->logger->info('Running PHPUnit tests...', Argument::that( + $this->logger->log('info', 'Running PHPUnit tests...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); diff --git a/tests/Console/Command/UpdateComposerJsonCommandTest.php b/tests/Console/Command/UpdateComposerJsonCommandTest.php index 6545dbb16e..66455a5288 100644 --- a/tests/Console/Command/UpdateComposerJsonCommandTest.php +++ b/tests/Console/Command/UpdateComposerJsonCommandTest.php @@ -102,8 +102,8 @@ protected function setUp(): void ->willReturn(false); $this->fileDiffer->formatForConsole(Argument::cetera()) ->willReturn(null); - $this->logger->info(Argument::cetera())->will(static function (): void {}); - $this->logger->notice(Argument::cetera())->will(static function (): void {}); + $this->logger->log('info', Argument::cetera())->will(static function (): void {}); + $this->logger->log('notice', Argument::cetera())->will(static function (): void {}); $this->logger->log(Argument::cetera())->will(static function (): void {}); $this->logger->error(Argument::cetera())->will(static function (): void {}); $this->input->getOption('dry-run') @@ -457,7 +457,8 @@ public function executeWillReturnFailureInCheckModeWhenComposerJsonWouldChange() $this->fileDiffer->formatForConsole('@@ diff @@', false) ->willReturn('@@ diff @@') ->shouldBeCalledOnce(); - $this->logger->notice( + $this->logger->log( + 'notice', '@@ diff @@', [ 'input' => $this->input->reveal(), diff --git a/tests/Console/Command/WikiCommandTest.php b/tests/Console/Command/WikiCommandTest.php index bf1dfdabaa..8e7d354f24 100644 --- a/tests/Console/Command/WikiCommandTest.php +++ b/tests/Console/Command/WikiCommandTest.php @@ -175,7 +175,7 @@ public function executeWillReturnSuccessWhenProcessQueueSucceeds(): void $this->processQueue->run($this->output->reveal()) ->willReturn(WikiCommand::SUCCESS) ->shouldBeCalled(); - $this->logger->info('Generating wiki documentation...', Argument::that( + $this->logger->log('info', 'Generating wiki documentation...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface )) ->shouldBeCalled(); @@ -222,8 +222,6 @@ public function executeWillSuppressProgressLogWhenJsonIsRequested(): void $this->processQueue->run(Argument::type(OutputInterface::class)) ->willReturn(WikiCommand::SUCCESS) ->shouldBeCalled(); - $this->logger->info(Argument::cetera()) - ->shouldNotBeCalled(); $this->logger->log( 'info', 'Wiki documentation generated successfully.', @@ -269,7 +267,7 @@ public function executeWillSkipWhenWikiTargetDoesNotExist(): void ->willReturn(new ProjectCapabilities([], null, false, false, false, false)); $this->processQueue->add(Argument::cetera()) ->shouldNotBeCalled(); - $this->logger->info('Generating wiki documentation...', Argument::that( + $this->logger->log('info', 'Generating wiki documentation...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface ))->shouldBeCalled(); $this->logger->log( @@ -296,7 +294,7 @@ public function executeWillTreatEquivalentDefaultWikiTargetsAsDefault(string $ta ->willReturn(new ProjectCapabilities([], null, false, false, false, false)); $this->processQueue->add(Argument::cetera()) ->shouldNotBeCalled(); - $this->logger->info('Generating wiki documentation...', Argument::that( + $this->logger->log('info', 'Generating wiki documentation...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface ))->shouldBeCalled(); $this->logger->log( @@ -333,7 +331,7 @@ public function executeWillGenerateWhenCustomWikiTargetDoesNotExist(): void $this->processQueue->run($this->output->reveal()) ->willReturn(WikiCommand::SUCCESS) ->shouldBeCalled(); - $this->logger->info('Generating wiki documentation...', Argument::that( + $this->logger->log('info', 'Generating wiki documentation...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface ))->shouldBeCalled(); $this->logger->log( @@ -361,7 +359,7 @@ public function executeWillSkipWhenNoApiDirectoriesAreDetected(): void ->willReturn(new ProjectCapabilities([], null, false, false, true, false)); $this->processQueue->add(Argument::cetera()) ->shouldNotBeCalled(); - $this->logger->info('Generating wiki documentation...', Argument::that( + $this->logger->log('info', 'Generating wiki documentation...', Argument::that( static fn(array $context): bool => $context['input'] instanceof InputInterface ))->shouldBeCalled(); $this->logger->log( diff --git a/tests/Console/Input/HasJsonOptionTest.php b/tests/Console/Input/HasJsonOptionTest.php index fb2032dee4..2b09fce6cf 100644 --- a/tests/Console/Input/HasJsonOptionTest.php +++ b/tests/Console/Input/HasJsonOptionTest.php @@ -108,14 +108,14 @@ public function isJsonOutputWillUseRuntimeEnvironmentWhenAvailable(): void $input->getOption('json') ->willReturn(false); - $command = new class ($runtimeEnvironment->reveal()) { + $command = new readonly class ($runtimeEnvironment->reveal()) { use HasJsonOption; /** * @param RuntimeEnvironmentInterface $runtimeEnvironment */ public function __construct( - private readonly RuntimeEnvironmentInterface $runtimeEnvironment, + private RuntimeEnvironmentInterface $runtimeEnvironment, ) {} /** From 4dcbf46f62c34113f0fa3c04ba6f0133a700196c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 22:51:03 -0300 Subject: [PATCH 30/35] Remove redundant service provider alias --- .../DevToolsServiceProvider.php | 27 ------------------- 1 file changed, 27 deletions(-) delete mode 100644 src/ServiceProvider/DevToolsServiceProvider.php diff --git a/src/ServiceProvider/DevToolsServiceProvider.php b/src/ServiceProvider/DevToolsServiceProvider.php deleted file mode 100644 index ca7232fa6f..0000000000 --- a/src/ServiceProvider/DevToolsServiceProvider.php +++ /dev/null @@ -1,27 +0,0 @@ - - * @license https://opensource.org/licenses/MIT MIT License - * - * @see https://github.com/php-fast-forward/ - * @see https://github.com/php-fast-forward/dev-tools - * @see https://github.com/php-fast-forward/dev-tools/issues - * @see https://php-fast-forward.github.io/dev-tools/ - * @see https://datatracker.ietf.org/doc/html/rfc2119 - */ - -namespace FastForward\DevTools\ServiceProvider; - -use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider as ContainerDevToolsServiceProvider; - -/** - * @deprecated use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider instead - */ -final class DevToolsServiceProvider extends ContainerDevToolsServiceProvider {} From ddc5e80a7315abe772336500cdb3d112f45a2028 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 2 May 2026 01:52:01 +0000 Subject: [PATCH 31/35] Update wiki submodule pointer for PR #329 --- .github/wiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/wiki b/.github/wiki index ca356029e6..989c09cc04 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit ca356029e6b59e5c223dbc73969ce1ff4bb9fd28 +Subproject commit 989c09cc04a27bd9236f56f743796d712ed3ee12 From 516a8f87f959b13df41607a67f21929d657f06c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 22:52:53 -0300 Subject: [PATCH 32/35] Use DevTools test double in command provider test --- .../DevToolsCommandProviderTest.php | 31 ++++++------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/tests/Composer/Capability/DevToolsCommandProviderTest.php b/tests/Composer/Capability/DevToolsCommandProviderTest.php index 46f9453784..c7bf82a998 100644 --- a/tests/Composer/Capability/DevToolsCommandProviderTest.php +++ b/tests/Composer/Capability/DevToolsCommandProviderTest.php @@ -34,7 +34,6 @@ use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; -use stdClass; #[UsesClass(GithubActionOutput::class)] #[CoversClass(DevToolsCommandProvider::class)] @@ -48,13 +47,16 @@ final class DevToolsCommandProviderTest extends TestCase private ObjectProphecy $plugin; + /** + * @var ObjectProphecy + */ + private ObjectProphecy $devTools; + /** * @var array */ private array $applicationCommands = []; - private stdClass $applicationState; - private DevToolsCommandProvider $commandProvider; /** @@ -63,9 +65,8 @@ final class DevToolsCommandProviderTest extends TestCase protected function setUp(): void { ContainerFactory::reset(); - $this->applicationState = new stdClass(); - $this->applicationState->commands = &$this->applicationCommands; $this->plugin = $this->prophesize(DevToolsPluginInterface::class); + $this->devTools = $this->prophesize(DevTools::class); $this->plugin->isRegisteredCommand(null) ->willReturn(false); @@ -92,22 +93,10 @@ protected function setUp(): void 'plugin' => $this->plugin->reveal(), ]); - ContainerFactory::set(DevTools::class, new readonly class ($this->applicationState) { - /** - * @param stdClass $state - */ - public function __construct( - private stdClass $state, - ) {} - - /** - * @return array - */ - public function all(): array - { - return $this->state->commands; - } - }); + $testCase = $this; + $this->devTools->all() + ->will(static fn(): array => $testCase->applicationCommands); + ContainerFactory::set(DevTools::class, $this->devTools->reveal()); } /** From f803c0eb901cbeaa623cdc2f1a7f15ae6926100c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 22:55:14 -0300 Subject: [PATCH 33/35] Use PHPUnit lifecycle attributes for container reset --- tests/Container/UsesContainerFactory.php | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/tests/Container/UsesContainerFactory.php b/tests/Container/UsesContainerFactory.php index 6903bc154a..13cad9f121 100644 --- a/tests/Container/UsesContainerFactory.php +++ b/tests/Container/UsesContainerFactory.php @@ -24,6 +24,8 @@ use FastForward\DevTools\Environment\Environment; use FastForward\DevTools\Environment\RuntimeEnvironment; use FastForward\DevTools\Path\DevToolsPathResolver; +use PHPUnit\Framework\Attributes\AfterClass; +use PHPUnit\Framework\Attributes\BeforeClass; use PHPUnit\Framework\Attributes\UsesClass; /** @@ -39,15 +41,9 @@ trait UsesContainerFactory /** * @return void */ - public static function setUpBeforeClass(): void - { - ContainerFactory::reset(); - } - - /** - * @return void - */ - public static function tearDownAfterClass(): void + #[BeforeClass] + #[AfterClass] + public static function resetSharedContainer(): void { ContainerFactory::reset(); } From 0caeac3ab25dd26d556a652ce72da639c84619d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 22:59:48 -0300 Subject: [PATCH 34/35] Refresh README and testing docs --- README.md | 21 +++++++++++++++++++++ docs/advanced/phpunit-extension.rst | 5 +++++ docs/commands/docs.rst | 7 ++++++- docs/commands/reports.rst | 3 +++ docs/commands/tests.rst | 14 ++++++++++++++ docs/commands/wiki.rst | 4 ++++ docs/getting-started/quickstart.rst | 22 ++++++++++++++-------- docs/usage/testing-and-coverage.rst | 13 ++++++++++++- 8 files changed, 79 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 322c741f7a..f4304feba6 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ across Fast Forward libraries. - Synchronizes packaged skills and project-agent prompts into consumer `.agents/skills` and `.agents/agents` directories using safe link-based updates +- Supports guide-only and automation-only repositories by skipping PHPUnit or + wiki generation gracefully when no runnable PHP surface exists - Works both as a Composer plugin and as a local binary - Preserves local overrides through consumer-first configuration resolution @@ -71,6 +73,8 @@ You can also run individual commands for specific development tasks: ```bash # Run PHPUnit tests composer dev-tools tests +composer dev-tools tests --json +composer dev-tools tests --pretty-json # Analyze missing, unused, misplaced, and outdated Composer dependencies composer dependencies @@ -120,6 +124,7 @@ composer phpdoc # Generate HTML API documentation using phpDocumentor composer docs +composer docs --source=docs/user-guide # Generate Markdown documentation for the wiki composer wiki @@ -180,6 +185,14 @@ The `metrics` command ships with `phpmetrics/phpmetrics` as a direct dependency of `fast-forward/dev-tools`, so consumer repositories can generate metrics reports without extra setup. +Guide-only repositories and workflow-only repositories can still use the +packaged command surface. When no runnable PHPUnit surface exists, `tests` +returns a controlled warning instead of failing. When a repository ships +guides without PSR-4 source paths, `docs` builds the guide site without trying +to synthesize API pages. When `.github/wiki` is absent and no wiki has been +initialized yet, `wiki` now skips generation with a warning instead of failing +the whole automation run. + The changelog commands manage Keep a Changelog 1.1.0 files without requiring extra tooling in the consumer repository. `changelog:entry` bootstraps a missing changelog file on first use, `changelog:check` enforces meaningful @@ -215,6 +228,14 @@ takes precedence over terminal-only color. Where orchestrated tools can expose structured subprocess results safely, DevTools prefers adding stable fields to the JSON context rather than coloring otherwise strict JSON output. +For the `tests` command, structured runs now capture the bundled PHPUnit +agent-reporter payload inside `context.output` while preserving the normal +DevTools JSON envelope. That means the top-level command still emits one final +document, and consumers can inspect stable nested keys such as +`context.output.result`, `context.output.summary`, optional +`context.output.details`, and `context.output.coverage` when minimum-coverage +validation is active. + Progress output is disabled by default on the commands that support transient rendering, and `--progress` re-enables it for human-readable terminal runs. When `--json` or `--pretty-json` is active on commands that orchestrate other diff --git a/docs/advanced/phpunit-extension.rst b/docs/advanced/phpunit-extension.rst index 42eac8af71..b0c9d18378 100644 --- a/docs/advanced/phpunit-extension.rst +++ b/docs/advanced/phpunit-extension.rst @@ -19,6 +19,9 @@ Runtime Chain builds a summary notification and sends it when the run finishes. 5. ``Ergebnis\PHPUnit\AgentReporter\Extension`` replaces PHPUnit's normal output with a compact JSON report when an agent runtime is detected. +6. In structured DevTools runs, ``tests`` forces the same reporter path for + the PHPUnit subprocess so the final nested payload remains deterministic + even when the surrounding process would not naturally look agent-driven. Why This Helps Consumer Projects -------------------------------- @@ -29,6 +32,8 @@ Why This Helps Consumer Projects scrollback; - agent-driven runs consume far less terminal context while still keeping failure details and PHPUnit exit semantics intact; +- DevTools can preserve a single top-level JSON document while nesting the + compact PHPUnit summary under ``context.output`` for bot-friendly parsing; - event counts are available to the notification layer without adding ad-hoc test code. diff --git a/docs/commands/docs.rst b/docs/commands/docs.rst index f50855e8b3..351cf4c98f 100644 --- a/docs/commands/docs.rst +++ b/docs/commands/docs.rst @@ -103,9 +103,14 @@ Exit Codes Behavior --------- -- ``docs/`` must exist unless you pass another ``--source`` directory. +- The default ``docs`` source directory is optional. When it is absent, the + command still generates API pages if PSR-4 source paths are available. +- A custom explicit ``--source`` path must exist; otherwise the command fails + fast. - API pages are built from the PSR-4 paths declared in ``composer.json``. - Guide pages are built from the selected source directory. +- Repositories without PSR-4 source paths can still generate a guides-only + site from the selected source directory. - Cache stays enabled by default; omit both flags to keep the command default, pass ``--cache`` to force it on, and pass ``--no-cache`` to force it off. - When ``--cache-dir`` is omitted, phpDocumentor keeps its default cache diff --git a/docs/commands/reports.rst b/docs/commands/reports.rst index 1dd5d23383..262c41eced 100644 --- a/docs/commands/reports.rst +++ b/docs/commands/reports.rst @@ -123,6 +123,9 @@ Behavior - When ``--json`` or ``--pretty-json`` is active, it forwards JSON mode to the ``docs``, ``tests``, and ``metrics`` subprocesses and suppresses transient progress output where those tools support it. +- Nested ``docs`` and ``tests`` stages can now skip gracefully with warnings in + guide-only or automation-only repositories, while ``reports`` still returns a + successful aggregate result for the stages that were actually generated. - Passes ``--junit /junit.xml`` to the metrics step. - Used by the ``standards`` command as the final phase. - This is the reporting stage used by GitHub Pages. diff --git a/docs/commands/tests.rst b/docs/commands/tests.rst index 23eda8128a..b81039d1e7 100644 --- a/docs/commands/tests.rst +++ b/docs/commands/tests.rst @@ -113,6 +113,13 @@ Run with minimum coverage enforcement: composer tests --min-coverage=80 +Run with structured JSON output: + +.. code-block:: bash + + composer tests --json + composer tests --pretty-json + Run without cache: .. code-block:: bash @@ -150,6 +157,10 @@ Behavior --------- - Local ``phpunit.xml`` is preferred over the packaged default. +- When the default ``tests`` path is absent and the project exposes no + testable PHP source files, the command exits successfully with a warning + instead of failing the whole automation flow. +- A custom explicit tests path that does not exist still fails fast. - Coverage filters are automatically applied to all PSR-4 paths from composer.json. - Multiple coverage formats are generated: HTML, Testdox HTML, Clover XML, and PHP. - Cache stays enabled by default; omit both flags to keep the command default, @@ -167,6 +178,9 @@ Behavior command stores that payload inside ``output`` while keeping the standard DevTools JSON envelope. ``--json`` and ``--pretty-json`` therefore expose the same structured result, with formatting as the only difference. +- the command forces the bundled PHPUnit agent-reporter extension for + structured runs so the nested payload stays compact and stable even when the + surrounding runtime would not normally look like an agent. - in structured mode, the command suppresses intermediary ``Running...`` log records so the output stream contains a single final JSON document. - when structured capture is active but PHPUnit does not emit parseable JSON, diff --git a/docs/commands/wiki.rst b/docs/commands/wiki.rst index 3fe6e88615..f78defe30c 100644 --- a/docs/commands/wiki.rst +++ b/docs/commands/wiki.rst @@ -90,6 +90,10 @@ Behavior --------- - Default output directory is ``.github/wiki``. +- When the default target does not exist yet and ``--init`` is not requested, + the command skips with a controlled warning instead of failing. +- Repositories without PHP API surface also skip wiki generation with a + warning, because the current wiki renderer only emits API pages. - Cache stays enabled by default; omit both flags to keep the command default, pass ``--cache`` to force it on, and pass ``--no-cache`` to force it off. - When ``--cache-dir`` is omitted, phpDocumentor keeps its default cache diff --git a/docs/getting-started/quickstart.rst b/docs/getting-started/quickstart.rst index a57da512e8..71240525a4 100644 --- a/docs/getting-started/quickstart.rst +++ b/docs/getting-started/quickstart.rst @@ -4,16 +4,16 @@ Quickstart This walkthrough is the fastest way to get a new library into a healthy state. 1. Install the package. -2. Create a minimal guide directory. +2. Create a guide directory if the repository will publish guides. 3. Synchronize shared automation, packaged skills, and packaged agents. 4. Run the focused commands once. 5. Run the full suite before opening a pull request. -Create the Minimum Guide ------------------------- +Optional Guide Setup +-------------------- -The ``docs`` command fails early when ``docs/`` does not exist. A tiny -starting page is enough for the first successful run. +If the repository will publish guides, a tiny starting page is enough for the +first successful ``docs`` run. Create the directory: @@ -30,10 +30,14 @@ Create ``docs/index.rst`` with content such as: Welcome to the project documentation. +Repositories that only ship PHP code can skip this step and still generate API +documentation. Repositories that only ship guides can also use the same +``docs`` command even without PSR-4 source paths. + Run the First Commands ---------------------- -Once the package is installed and the guide directory exists, run: +Once the package is installed, run: .. code-block:: bash @@ -57,9 +61,11 @@ What Each Command Proves safely into ``.agents/agents`` without copying files into the consumer repository. - ``composer tests`` proves the packaged or local PHPUnit - configuration can execute the current test suite. + configuration can execute the current test suite, or skip gracefully when + the repository intentionally has no runnable PHPUnit surface yet. - ``composer docs`` proves the PSR-4 source paths and the guide - directory are usable by phpDocumentor. + directory are usable by phpDocumentor, whichever of those surfaces the + repository actually provides. - ``composer dev-tools`` proves the complete pipeline can run in the expected order. diff --git a/docs/usage/testing-and-coverage.rst b/docs/usage/testing-and-coverage.rst index 646a2c6ca7..c4623d1d85 100644 --- a/docs/usage/testing-and-coverage.rst +++ b/docs/usage/testing-and-coverage.rst @@ -17,6 +17,8 @@ When you run ``tests``, DevTools: explicit force-off flag for the current run; - uses the selected workspace ``cache/phpunit`` directory only when caching stays enabled; +- skips the command with a warning when the repository has no default + ``tests`` directory and no testable PHP source surface; - can generate HTML coverage, Testdox, Clover, and raw PHP coverage output when ``--coverage`` is provided. @@ -29,6 +31,8 @@ Useful Examples composer tests -- --filter=PluginTest composer tests --coverage=.dev-tools/coverage composer tests --no-cache --bootstrap=tests/bootstrap.php + composer tests --json + composer tests --pretty-json Coverage Outputs ---------------- @@ -59,6 +63,10 @@ The packaged ``phpunit.xml`` registers: default verbose terminal output with a compact JSON summary when an agent runtime is detected. +When ``tests`` itself runs in structured mode, DevTools also forces the +reporter path for the PHPUnit subprocess so the nested payload remains compact +and parseable for bots and agent workflows. + Programmatic Coverage Access ----------------------------- @@ -83,7 +91,10 @@ When to Override Locally Create your own ``phpunit.xml`` in the consumer project when you need a different bootstrap file, extra extensions, or alternative strictness flags. -DevTools will prefer the local file automatically. +DevTools will prefer the local file automatically, but consumer projects that +replace the packaged configuration must re-register both bundled extensions if +they still want desktop notifications, BypassFinals support, and compact +agent-oriented JSON output. .. note:: From 77632b6b132a6d9b4a97b60b6528953726ad721b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Fri, 1 May 2026 23:00:48 -0300 Subject: [PATCH 35/35] Refresh changelog entry for structured output work --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dfc98a235..2b9e752c9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Keep `tests` JSON output parseable by exposing the PHPUnit agent-reporter payload under `output` in structured runs, including a raw-output fallback when PHPUnit writes extra text before the final JSON (#248) +- Keep structured DevTools command output parseable and agent-friendly by exposing the PHPUnit agent-reporter payload under nested `output`, suppressing intermediary progress logs in JSON modes, and normalizing orchestrated subprocess payloads before the final JSON envelope is emitted (#248) - Register ``ergebnis/phpunit-agent-reporter`` in the packaged ``phpunit.xml`` so AI agents receive compact PHPUnit JSON summaries without changing consumer overrides manually (#327) - Let `wiki`, `docs`, `tests`, `metrics`, and `reports` skip gracefully for guide-only repositories while keeping wiki/report workflows and published preview links aligned with the artifacts that were actually generated (#325)