From bb99e0874465b73e26a14ff338a92c7d9ad91699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Say=C3=A3o=20Lobato=20Abreu?= Date: Tue, 28 Apr 2026 18:04:12 -0300 Subject: [PATCH 1/2] Normalize DevTools workspace directory handling --- CHANGELOG.md | 1 + README.md | 11 ++++ docs/commands/docs.rst | 6 +- docs/commands/metrics.rst | 7 ++- docs/commands/phpdoc.rst | 3 +- docs/commands/reports.rst | 9 ++- docs/commands/self-update.rst | 8 +++ docs/commands/tests.rst | 3 +- docs/commands/wiki.rst | 3 +- docs/configuration/tooling-defaults.rst | 11 +++- docs/running/reports.rst | 18 ++++-- docs/running/specialized-commands.rst | 11 +++- docs/usage/documentation-workflows.rst | 16 +++-- docs/usage/testing-and-coverage.rst | 3 +- src/Console/Command/MetricsCommand.php | 33 +++++++++- src/Console/DevTools.php | 27 ++++++++ src/Path/ManagedWorkspace.php | 63 +++++++++++++++++-- src/Path/WorkingProjectPathResolver.php | 24 ++++++- tests/Console/Command/MetricsCommandTest.php | 48 +++++++++++++- tests/Console/DevToolsTest.php | 50 +++++++++++++++ tests/Path/ManagedWorkspaceTest.php | 45 +++++++++++++ tests/Path/WorkingProjectPathResolverTest.php | 62 ++++++++++++++++++ 22 files changed, 425 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95ef9925f6..2420afefbf 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 ### Added +- Add a configurable DevTools generated artifact workspace through `--workspace-dir` and `FAST_FORWARD_WORKSPACE_DIR`, keeping explicit output/cache command options authoritative (#274) - Add a standalone DevTools `self-update` command plus global `--working-dir` and `--auto-update` binary options for local or global installations (#272) ## [1.23.0] - 2026-04-26 diff --git a/README.md b/README.md index 3a6c284ce1..1be24515c5 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,9 @@ composer dev-tools:fix # Run the standalone binary from another project directory vendor/bin/dev-tools --working-dir=/path/to/project tests +# Store generated reports and caches outside the default .dev-tools workspace +vendor/bin/dev-tools --workspace-dir=.artifacts reports + # Update the installed DevTools package vendor/bin/dev-tools self-update ``` @@ -124,6 +127,7 @@ composer wiki # Generate documentation frontpage and related reports composer reports composer reports --target=.dev-tools --coverage=.dev-tools/coverage +FAST_FORWARD_WORKSPACE_DIR=.artifacts composer reports # Synchronize packaged agent skills into .agents/skills composer skills @@ -296,6 +300,13 @@ skills they depend on. | `composer dev-tools:sync` | Updates scripts, CODEOWNERS, funding metadata, workflow stubs, `.editorconfig`, `.gitignore`, `.gitattributes`, wiki setup, packaged skills, and packaged agents. | | `vendor/bin/dev-tools self-update` / `composer dev-tools:self-update` | Updates the local DevTools package, or the Composer global installation when the active binary is globally installed. | +`--working-dir`/`-d` changes the project root used by the standalone binary. +`--workspace-dir`/`-w` changes where generated DevTools artifacts and caches are +written when command-specific paths are omitted. Composer executions can use +`FAST_FORWARD_WORKSPACE_DIR=.artifacts composer reports` for the same workspace +policy, while explicit options such as `--target`, `--coverage`, `--metrics`, +and `--cache-dir` continue to take precedence. + ## 🔌 Integration DevTools integrates with consumer repositories in two ways. The Composer plugin diff --git a/docs/commands/docs.rst b/docs/commands/docs.rst index c3bc38f05c..f50855e8b3 100644 --- a/docs/commands/docs.rst +++ b/docs/commands/docs.rst @@ -28,7 +28,8 @@ Options ``--target, -t`` (optional) Path to the output directory for the generated HTML documentation. - Default: ``.dev-tools``. + Default: selected workspace directory, ``.dev-tools`` unless + ``--workspace-dir`` or ``FAST_FORWARD_WORKSPACE_DIR`` is configured. ``--source, -s`` (optional) Path to the source directory for the guide documentation. @@ -40,7 +41,8 @@ Options ``--cache-dir`` (optional) Path to the cache directory for phpDocumentor. - Default: ``.dev-tools/cache/phpdoc``. + Default: selected workspace cache directory, usually + ``.dev-tools/cache/phpdoc``. ``--cache`` Force phpDocumentor caching on for this run. diff --git a/docs/commands/metrics.rst b/docs/commands/metrics.rst index c7ca5794bf..d3cb43d006 100644 --- a/docs/commands/metrics.rst +++ b/docs/commands/metrics.rst @@ -33,12 +33,15 @@ Options Comma-separated directories that should be excluded from analysis. Default: - ``vendor,tmp,cache,spec,build,.dev-tools,backup,resources``. + ``vendor,tmp,cache,spec,build,.dev-tools,backup,resources`` plus a custom + relative workspace directory when ``--workspace-dir`` or + ``FAST_FORWARD_WORKSPACE_DIR`` selects one. ``--target=`` Output directory for the generated metrics reports. - Default: ``.dev-tools/metrics``. + Default: selected workspace ``metrics`` directory, usually + ``.dev-tools/metrics``. The command writes: diff --git a/docs/commands/phpdoc.rst b/docs/commands/phpdoc.rst index 47f1c28c27..cb5edb370e 100644 --- a/docs/commands/phpdoc.rst +++ b/docs/commands/phpdoc.rst @@ -37,7 +37,8 @@ Options Automatically fix PHPDoc issues. Without this option, runs in dry-run mode. ``--cache-dir`` (optional) - Path to the cache directory for PHP-CS-Fixer. Default: ``.dev-tools/cache/php-cs-fixer``. + Path to the cache directory for PHP-CS-Fixer. Default: selected workspace + cache directory, usually ``.dev-tools/cache/php-cs-fixer``. ``--cache`` Force PHP-CS-Fixer caching on for this run. diff --git a/docs/commands/reports.rst b/docs/commands/reports.rst index d340f9329e..1dd5d23383 100644 --- a/docs/commands/reports.rst +++ b/docs/commands/reports.rst @@ -30,15 +30,18 @@ Options ``--target`` (optional) The target directory for the generated documentation. - Default: ``.dev-tools``. + Default: selected workspace directory, ``.dev-tools`` unless + ``--workspace-dir`` or ``FAST_FORWARD_WORKSPACE_DIR`` is configured. ``--coverage, -c`` (optional) The target directory for the generated test coverage report. - Default: ``.dev-tools/coverage``. + Default: selected workspace ``coverage`` directory, usually + ``.dev-tools/coverage``. ``--metrics`` (optional) The target directory for the generated metrics report. - Default: ``.dev-tools/metrics``. + Default: selected workspace ``metrics`` directory, usually + ``.dev-tools/metrics``. ``--cache-dir`` (optional) Base cache directory for nested ``docs`` and ``tests`` caches. diff --git a/docs/commands/self-update.rst b/docs/commands/self-update.rst index 31f1ae78ee..1b7a44b8c5 100644 --- a/docs/commands/self-update.rst +++ b/docs/commands/self-update.rst @@ -26,6 +26,7 @@ options before the command name: .. code-block:: bash vendor/bin/dev-tools --working-dir=/path/to/project tests + vendor/bin/dev-tools --workspace-dir=.artifacts reports vendor/bin/dev-tools --auto-update tests ``--working-dir`` (or ``-d``) switches the process directory before resolving @@ -33,6 +34,13 @@ paths, managed files, or command defaults. This lets a globally installed binary operate on another project without first changing shell directories. Composer executions can use Composer's own ``--working-dir``/``-d`` option. +``--workspace-dir`` (or ``-w``) changes where generated DevTools artifacts and +caches are written when command-specific paths are omitted. It does not change +the project root selected by ``--working-dir``. Composer plugin executions can use +``FAST_FORWARD_WORKSPACE_DIR=.artifacts`` to apply the same workspace policy. +Explicit command options such as ``--target``, ``--coverage``, ``--metrics``, +and ``--cache-dir`` continue to take precedence over the workspace default. + ``--auto-update`` runs the self-update flow before the requested command. The same behavior MAY be enabled with ``FAST_FORWARD_AUTO_UPDATE=1``. When the active ``dev-tools`` binary is already installed globally, auto-update also diff --git a/docs/commands/tests.rst b/docs/commands/tests.rst index 1b5766e0f9..6a25cf21e1 100644 --- a/docs/commands/tests.rst +++ b/docs/commands/tests.rst @@ -39,7 +39,8 @@ Options Path to the bootstrap file. Default: ``./vendor/autoload.php``. ``--cache-dir`` (optional) - Path to the PHPUnit cache directory. Default: ``.dev-tools/cache/phpunit``. + Path to the PHPUnit cache directory. Default: selected workspace cache + directory, usually ``.dev-tools/cache/phpunit``. ``--cache`` Force PHPUnit result caching on for this run. diff --git a/docs/commands/wiki.rst b/docs/commands/wiki.rst index efbdf53032..3fe6e88615 100644 --- a/docs/commands/wiki.rst +++ b/docs/commands/wiki.rst @@ -27,7 +27,8 @@ Options ``--cache-dir`` (optional) Path to the cache directory for phpDocumentor. - Default: ``.dev-tools/cache/phpdoc``. + Default: selected workspace cache directory, usually + ``.dev-tools/cache/phpdoc``. ``--cache`` Force phpDocumentor caching on for this run. diff --git a/docs/configuration/tooling-defaults.rst b/docs/configuration/tooling-defaults.rst index 950ae541b6..4e9fe6a783 100644 --- a/docs/configuration/tooling-defaults.rst +++ b/docs/configuration/tooling-defaults.rst @@ -40,7 +40,8 @@ create them on day one. Generated and Cache Directories ------------------------------- -- ``.dev-tools/`` contains generated documentation and report output. +- ``.dev-tools/`` contains generated documentation and report output by + default. - ``.dev-tools/coverage/`` contains HTML coverage, Testdox, Clover, and raw coverage data. - ``.dev-tools/metrics/`` contains PhpMetrics HTML output plus the generated @@ -56,6 +57,14 @@ Generated and Cache Directories ``.dev-tools/cache/php-cs-fixer/.php-cs-fixer.cache`` store repository-local tool caches. +The standalone binary accepts ``--workspace-dir`` to replace the generated +artifact root used by those defaults. Composer executions can set +``FAST_FORWARD_WORKSPACE_DIR`` for the same behavior. ``--working-dir`` still +selects the project root, while ``--workspace-dir`` selects where generated +artifacts and caches are written. Explicit command options such as +``--target``, ``--coverage``, ``--metrics``, and ``--cache-dir`` override the +workspace default. + Local Versus Packaged Files --------------------------- diff --git a/docs/running/reports.rst b/docs/running/reports.rst index 63891d0398..a4b0a27cba 100644 --- a/docs/running/reports.rst +++ b/docs/running/reports.rst @@ -10,19 +10,25 @@ What the Command Runs ``reports`` executes the following steps: -1. ``docs --target .dev-tools`` -2. ``tests --coverage .dev-tools/coverage --coverage-summary`` -3. ``metrics --target .dev-tools/metrics --junit .dev-tools/coverage/junit.xml`` +1. ``docs`` with the selected workspace target +2. ``tests --coverage`` with the selected workspace coverage target +3. ``metrics --target`` with the selected workspace metrics target and JUnit + input from the selected workspace coverage directory + +With the default workspace, those steps resolve to ``docs --target .dev-tools``, +``tests --coverage .dev-tools/coverage --coverage-summary``, and +``metrics --target .dev-tools/metrics --junit .dev-tools/coverage/junit.xml``. Outputs ------- After a successful run you should expect: -- the documentation site rooted at ``.dev-tools/``; +- the documentation site rooted at the selected workspace, usually + ``.dev-tools/``; - guide pages generated from the local ``docs/`` source; -- coverage reports inside ``.dev-tools/coverage/``; -- PhpMetrics output inside ``.dev-tools/metrics/``; +- coverage reports inside the selected workspace ``coverage`` directory; +- PhpMetrics output inside the selected workspace ``metrics`` directory; - ``.dev-tools/coverage/testdox.html`` and ``.dev-tools/coverage/clover.xml`` for human and CI consumption; - ``.dev-tools/metrics/report.json`` and ``.dev-tools/metrics/report-summary.json`` for diff --git a/docs/running/specialized-commands.rst b/docs/running/specialized-commands.rst index ef78641f17..ff52d8121b 100644 --- a/docs/running/specialized-commands.rst +++ b/docs/running/specialized-commands.rst @@ -116,6 +116,7 @@ Analyzes code metrics with PhpMetrics. composer metrics composer metrics --target=.dev-tools/metrics + FAST_FORWARD_WORKSPACE_DIR=.artifacts composer metrics composer --working-dir=packages/example metrics Important details: @@ -128,6 +129,8 @@ Important details: - ``--target`` stores the HTML report plus ``report.json`` and ``report-summary.json`` in the same directory for CI artifacts or manual review; +- when ``--target`` is omitted, metrics output is written under the selected + DevTools workspace, usually ``.dev-tools/metrics``; - ``--json`` and ``--pretty-json`` keep the top-level DevTools response structured while forwarding JSON or quieter modes to the wrapped tools where available; @@ -262,9 +265,11 @@ Runs the documentation and test-report pipeline used by GitHub Pages. Important details: -- it calls ``docs --target .dev-tools``; -- it calls ``tests --coverage .dev-tools/coverage --coverage-summary``; -- it calls ``metrics --target .dev-tools/metrics --junit .dev-tools/coverage/junit.xml``; +- it calls ``docs``, ``tests --coverage``, and ``metrics`` with defaults rooted + in the selected DevTools workspace; +- with the default workspace, that is equivalent to ``docs --target .dev-tools``, + ``tests --coverage .dev-tools/coverage --coverage-summary``, and + ``metrics --target .dev-tools/metrics --junit .dev-tools/coverage/junit.xml``; - ``docs`` remains detached, while ``tests`` and ``metrics`` run in sequence so PhpMetrics can reuse the JUnit report generated by PHPUnit; - cache stays enabled by default for the nested ``docs`` and ``tests`` steps; diff --git a/docs/usage/documentation-workflows.rst b/docs/usage/documentation-workflows.rst index 0c40fbf6a1..570c828674 100644 --- a/docs/usage/documentation-workflows.rst +++ b/docs/usage/documentation-workflows.rst @@ -55,19 +55,23 @@ What Each Command Is For - ``docs`` builds the HTML documentation site. It fails early if the source guide directory does not exist. - ``wiki`` builds Markdown API pages intended for ``.github/wiki``. -- ``reports`` runs ``docs --target .dev-tools`` and - ``tests --coverage .dev-tools/coverage`` and then +- ``reports`` runs ``docs``, ``tests --coverage``, and ``metrics`` using the + selected DevTools workspace defaults. With the default workspace that means + ``docs --target .dev-tools``, ``tests --coverage .dev-tools/coverage``, and ``metrics --target .dev-tools/metrics --junit .dev-tools/coverage/junit.xml``. + When ``--workspace-dir`` or ``FAST_FORWARD_WORKSPACE_DIR`` selects another + workspace, those nested defaults move together unless an explicit command + option overrides them. Outputs to Expect ----------------- - an HTML site rooted at the target directory chosen for ``docs``; - guide pages generated from ``docs/``; -- coverage data under ``.dev-tools/coverage`` when ``reports`` or - ``tests --coverage`` is used; -- metrics data under ``.dev-tools/metrics`` when ``reports`` or ``metrics`` is - used; +- coverage data under the selected workspace ``coverage`` directory when + ``reports`` or ``tests --coverage`` is used; +- metrics data under the selected workspace ``metrics`` directory when + ``reports`` or ``metrics`` is used; - Markdown API pages under ``.github/wiki`` when ``wiki`` is used. Troubleshooting diff --git a/docs/usage/testing-and-coverage.rst b/docs/usage/testing-and-coverage.rst index 0013e86467..18d204416c 100644 --- a/docs/usage/testing-and-coverage.rst +++ b/docs/usage/testing-and-coverage.rst @@ -15,7 +15,8 @@ When you run ``tests``, DevTools: - keeps PHPUnit result caching enabled by default; - treats ``--cache`` as an explicit force-on flag and ``--no-cache`` as an explicit force-off flag for the current run; -- uses ``.dev-tools/cache/phpunit`` only when caching stays enabled; +- uses the selected workspace ``cache/phpunit`` directory only when caching + stays enabled; - can generate HTML coverage, Testdox, Clover, and raw PHP coverage output when ``--coverage`` is provided. diff --git a/src/Console/Command/MetricsCommand.php b/src/Console/Command/MetricsCommand.php index 434dbf620e..6b7dc54f61 100644 --- a/src/Console/Command/MetricsCommand.php +++ b/src/Console/Command/MetricsCommand.php @@ -59,6 +59,20 @@ final class MetricsCommand extends Command */ private const int PHP_DEFAULT_SOCKET_TIMEOUT = 1; + /** + * @var list the directories PhpMetrics SHOULD skip by default + */ + private const array DEFAULT_EXCLUDED_DIRECTORIES = [ + 'vendor', + 'tmp', + 'cache', + 'spec', + 'build', + ManagedWorkspace::WORKSPACE_ROOT, + 'backup', + 'resources', + ]; + /** * @param ProcessBuilderInterface $processBuilder the builder used to assemble the PhpMetrics process * @param ProcessQueueInterface $processQueue the queue used to execute the PhpMetrics process @@ -89,7 +103,7 @@ protected function configure(): void name: 'exclude', mode: InputOption::VALUE_OPTIONAL, description: 'Comma-separated directories that SHOULD be excluded from analysis.', - default: 'vendor,tmp,cache,spec,build,.dev-tools,backup,resources', + default: implode(',', $this->getDefaultExcludedDirectories()), ) ->addOption( name: 'target', @@ -164,4 +178,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int 'output' => $processOutput, ]); } + + /** + * Returns the default PhpMetrics directory exclusion list. + * + * @return list + */ + private function getDefaultExcludedDirectories(): array + { + $directories = self::DEFAULT_EXCLUDED_DIRECTORIES; + $workspaceRoot = ManagedWorkspace::getProjectRelativeWorkspaceRoot(); + + if (null !== $workspaceRoot && ! \in_array($workspaceRoot, $directories, true)) { + $directories[] = $workspaceRoot; + } + + return $directories; + } } diff --git a/src/Console/DevTools.php b/src/Console/DevTools.php index b52cee8d89..14edf2df7a 100644 --- a/src/Console/DevTools.php +++ b/src/Console/DevTools.php @@ -22,6 +22,7 @@ use FastForward\DevTools\Console\Command\SelfUpdateCommand; use Override; use FastForward\DevTools\Environment\EnvironmentInterface; +use FastForward\DevTools\Path\ManagedWorkspace; use FastForward\DevTools\SelfUpdate\SelfUpdateRunnerInterface; use FastForward\DevTools\SelfUpdate\SelfUpdateScopeResolverInterface; use FastForward\DevTools\SelfUpdate\VersionCheckNotifierInterface; @@ -38,6 +39,8 @@ use Symfony\Component\Console\Output\OutputInterface; use Throwable; +use function Safe\putenv; + /** * Wraps the fast-forward console tooling suite conceptually as an isolated application instance. * Extending the base application, it MUST provide default command injections safely. @@ -118,6 +121,13 @@ protected function getDefaultInputDefinition(): InputDefinition description: 'Update fast-forward/dev-tools before running the requested command.', )); + $definition->addOption(new InputOption( + name: 'workspace-dir', + shortcut: 'w', + mode: InputOption::VALUE_REQUIRED, + description: 'Store generated DevTools artifacts in the given directory.', + )); + return $definition; } @@ -134,6 +144,7 @@ public function doRun(InputInterface $input, OutputInterface $output): int { try { $this->workingDirectorySwitcher->switchTo($this->getWorkingDirectoryOption($input)); + $this->configureWorkspaceDirectory($input); } catch (Throwable $throwable) { $output->writeln(\sprintf('%s', $throwable->getMessage())); @@ -183,6 +194,22 @@ private function getWorkingDirectoryOption(InputInterface $input): ?string return \is_string($workingDirectory) ? $workingDirectory : null; } + /** + * Applies the configured workspace directory before resolving command defaults. + * + * @param InputInterface $input the application input + */ + private function configureWorkspaceDirectory(InputInterface $input): void + { + $workspaceDirectory = $input->getParameterOption('--workspace-dir', null, true); + + if (! \is_string($workspaceDirectory) || '' === $workspaceDirectory) { + return; + } + + putenv(ManagedWorkspace::ENV_WORKSPACE_DIR . '=' . $workspaceDirectory); + } + /** * Runs an explicit automatic update without letting failures block the requested command. * diff --git a/src/Path/ManagedWorkspace.php b/src/Path/ManagedWorkspace.php index 53582a06e8..e733d1ea8a 100644 --- a/src/Path/ManagedWorkspace.php +++ b/src/Path/ManagedWorkspace.php @@ -26,6 +26,11 @@ */ final class ManagedWorkspace { + /** + * @var string the environment variable used to override the generated artifact workspace + */ + public const string ENV_WORKSPACE_DIR = 'FAST_FORWARD_WORKSPACE_DIR'; + /** * @var string the output segment used for coverage artifacts */ @@ -59,7 +64,7 @@ final class ManagedWorkspace /** * @var string the repository-local root directory for generated artifacts */ - private const string WORKSPACE_ROOT = '.dev-tools'; + public const string WORKSPACE_ROOT = '.dev-tools'; /** * @var string the repository-local root directory for generated tool caches @@ -78,9 +83,7 @@ final class ManagedWorkspace */ public static function getOutputDirectory(string $path = '', string $baseDir = ''): string { - $baseDir = '' === $baseDir - ? self::WORKSPACE_ROOT - : Path::join($baseDir, self::WORKSPACE_ROOT); + $baseDir = self::getWorkspaceRoot($baseDir); return '' === $path ? $baseDir @@ -105,4 +108,56 @@ public static function getCacheDirectory(string $path = '', string $baseDir = '' ? $baseDir : Path::join($baseDir, $path); } + + /** + * Returns the configured workspace root. + * + * Relative workspace paths stay relative when no base directory is provided. + * When a base directory is provided, relative workspaces are materialized + * under that base directory while absolute workspaces are used as-is. + * + * @param string $baseDir the optional repository root used to resolve a relative workspace + */ + public static function getWorkspaceRoot(string $baseDir = ''): string + { + $workspaceRoot = getenv(self::ENV_WORKSPACE_DIR); + + if (false === $workspaceRoot || '' === $workspaceRoot) { + $workspaceRoot = self::WORKSPACE_ROOT; + } + + if ('' === $baseDir || Path::isAbsolute($workspaceRoot)) { + return $workspaceRoot; + } + + return Path::join($baseDir, $workspaceRoot); + } + + /** + * Returns the workspace root as a project-relative directory when tooling + * should skip generated artifacts during source scans. + * + * @param string $baseDir the optional repository root used to relativize absolute workspace paths + */ + public static function getProjectRelativeWorkspaceRoot(string $baseDir = ''): ?string + { + $workspaceRoot = self::getWorkspaceRoot(); + + if (! Path::isAbsolute($workspaceRoot)) { + return $workspaceRoot; + } + + if ('' === $baseDir) { + return null; + } + + $baseDir = Path::canonicalize($baseDir); + $workspaceRoot = Path::canonicalize($workspaceRoot); + + if ($baseDir === $workspaceRoot || ! str_starts_with($workspaceRoot, $baseDir . '/')) { + return null; + } + + return Path::makeRelative($workspaceRoot, $baseDir); + } } diff --git a/src/Path/WorkingProjectPathResolver.php b/src/Path/WorkingProjectPathResolver.php index cd42589412..6145bbfd80 100644 --- a/src/Path/WorkingProjectPathResolver.php +++ b/src/Path/WorkingProjectPathResolver.php @@ -71,7 +71,7 @@ public static function getToolingExcludedDirectories(string $baseDir = ''): arra { $directories = []; - foreach (self::TOOLING_EXCLUDED_DIRECTORIES as $excludedDirectory) { + foreach (self::getToolingExcludedDirectoryNames($baseDir) as $excludedDirectory) { $directories[] = Path::join($baseDir, $excludedDirectory); } @@ -88,11 +88,12 @@ public static function getToolingExcludedDirectories(string $baseDir = ''): arra public static function getToolingSourcePaths(string $baseDir = ''): array { $workingDirectory = '' === $baseDir ? getcwd() : $baseDir; + $excludedDirectories = self::getToolingExcludedDirectoryNames($workingDirectory); $finder = Finder::create() ->files() ->name('*.php') ->in($workingDirectory) - ->exclude(self::TOOLING_EXCLUDED_DIRECTORIES) + ->exclude($excludedDirectories) ->sortByName(); $paths = []; @@ -109,4 +110,23 @@ public static function getToolingSourcePaths(string $baseDir = ''): array return $paths; } + + /** + * Returns repository-relative directories ignored by tooling. + * + * @param string $baseDir the optional repository base directory used to relativize a custom workspace + * + * @return list + */ + private static function getToolingExcludedDirectoryNames(string $baseDir = ''): array + { + $directories = self::TOOLING_EXCLUDED_DIRECTORIES; + $workspaceRoot = ManagedWorkspace::getProjectRelativeWorkspaceRoot($baseDir); + + if (null !== $workspaceRoot && ! \in_array($workspaceRoot, $directories, true)) { + $directories[] = $workspaceRoot; + } + + return $directories; + } } diff --git a/tests/Console/Command/MetricsCommandTest.php b/tests/Console/Command/MetricsCommandTest.php index 1737dde21f..3e2a91cb12 100644 --- a/tests/Console/Command/MetricsCommandTest.php +++ b/tests/Console/Command/MetricsCommandTest.php @@ -39,6 +39,8 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; +use function Safe\putenv; + #[CoversClass(MetricsCommand::class)] #[UsesClass(ManagedWorkspace::class)] #[UsesTrait(LogsCommandResults::class)] @@ -98,9 +100,6 @@ protected function setUp(): void && str_starts_with((string) $command[1], '-derror_reporting=') && '-ddefault_socket_timeout=1' === $command[2] && 'vendor/bin/phpmetrics' === $command[3]))->willReturn($this->process->reveal()); - $this->processQueue->add($this->process->reveal(), Argument::cetera()) - ->shouldBeCalled(); - $this->command = new MetricsCommand( $this->processBuilder->reveal(), $this->processQueue->reveal(), @@ -108,12 +107,21 @@ protected function setUp(): void ); } + /** + * @return void + */ + protected function tearDown(): void + { + putenv(ManagedWorkspace::ENV_WORKSPACE_DIR); + } + /** * @return void */ #[Test] public function executeWillReturnSuccessWhenProcessQueueSucceeds(): void { + $this->expectProcessQueued(); $this->processQueue->run($this->output->reveal()) ->willReturn(MetricsCommand::SUCCESS) ->shouldBeCalled(); @@ -137,6 +145,7 @@ public function executeWillReturnSuccessWhenProcessQueueSucceeds(): void #[Test] public function executeWillReturnFailureWhenProcessQueueFails(): void { + $this->expectProcessQueued(); $this->processQueue->run($this->output->reveal()) ->willReturn(MetricsCommand::FAILURE) ->shouldBeCalled(); @@ -159,6 +168,7 @@ public function executeWillReturnFailureWhenProcessQueueFails(): void #[Test] public function executeWillRunPhpMetricsInQuietModeWhenJsonIsRequested(): void { + $this->expectProcessQueued(); $this->input->getOption('json') ->willReturn(true); $this->input->getOption('pretty-json') @@ -179,6 +189,7 @@ public function executeWillRunPhpMetricsInQuietModeWhenJsonIsRequested(): void #[Test] public function executeWillNotRunPhpMetricsInQuietModeWhenProgressIsRequested(): void { + $this->expectProcessQueued(); $this->input->getOption('progress') ->willReturn(true); $this->processBuilder->withArgument('--quiet') @@ -190,6 +201,28 @@ public function executeWillNotRunPhpMetricsInQuietModeWhenProgressIsRequested(): self::assertSame(MetricsCommand::SUCCESS, $this->executeCommand()); } + /** + * @return void + */ + #[Test] + public function configureWillExcludeCustomRelativeWorkspaceByDefault(): void + { + putenv(ManagedWorkspace::ENV_WORKSPACE_DIR . '=.artifacts'); + + $command = new MetricsCommand( + $this->processBuilder->reveal(), + $this->processQueue->reveal(), + $this->logger->reveal(), + ); + + self::assertSame( + 'vendor,tmp,cache,spec,build,.dev-tools,backup,resources,.artifacts', + $command->getDefinition() + ->getOption('exclude') + ->getDefault() + ); + } + /** * @return int */ @@ -198,4 +231,13 @@ private function executeCommand(): int return (new ReflectionMethod($this->command, 'execute')) ->invoke($this->command, $this->input->reveal(), $this->output->reveal()); } + + /** + * @return void + */ + private function expectProcessQueued(): void + { + $this->processQueue->add(Argument::type(Process::class), Argument::cetera()) + ->shouldBeCalled(); + } } diff --git a/tests/Console/DevToolsTest.php b/tests/Console/DevToolsTest.php index 384d0cd222..15f453e5f4 100644 --- a/tests/Console/DevToolsTest.php +++ b/tests/Console/DevToolsTest.php @@ -28,6 +28,7 @@ use FastForward\DevTools\Environment\RuntimeEnvironment; use FastForward\DevTools\Filesystem\FinderFactory; use FastForward\DevTools\Path\DevToolsPathResolver; +use FastForward\DevTools\Path\ManagedWorkspace; use FastForward\DevTools\Path\WorkingProjectPathResolver; use FastForward\DevTools\Process\ColorPreservingProcessEnvironmentConfigurator; use FastForward\DevTools\Process\CompositeProcessEnvironmentConfigurator; @@ -64,8 +65,11 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use function Safe\putenv; + #[CoversClass(DevTools::class)] #[UsesClass(DevToolsPathResolver::class)] +#[UsesClass(ManagedWorkspace::class)] #[UsesClass(DevToolsCommandLoader::class)] #[UsesClass(FinderFactory::class)] #[UsesClass(DevToolsServiceProvider::class)] @@ -121,6 +125,8 @@ final class DevToolsTest extends TestCase private DevTools $devTools; + private string|false $originalWorkspaceDirectoryEnv; + /** * @return void */ @@ -137,9 +143,25 @@ protected function setUp(): void $this->selfUpdateRunner = $this->prophesize(SelfUpdateRunnerInterface::class); $this->selfUpdateScopeResolver = $this->prophesize(SelfUpdateScopeResolverInterface::class); $this->environment = $this->prophesize(EnvironmentInterface::class); + $this->originalWorkspaceDirectoryEnv = getenv(ManagedWorkspace::ENV_WORKSPACE_DIR); $this->devTools = $this->createDevTools(); } + /** + * @return void + */ + #[Override] + protected function tearDown(): void + { + if (false === $this->originalWorkspaceDirectoryEnv) { + putenv(ManagedWorkspace::ENV_WORKSPACE_DIR); + + return; + } + + putenv(ManagedWorkspace::ENV_WORKSPACE_DIR . '=' . $this->originalWorkspaceDirectoryEnv); + } + /** * @return void */ @@ -190,6 +212,8 @@ public function constructorWillRegisterGlobalRuntimeOptions(): void self::assertTrue($definition->hasOption('working-dir')); self::assertSame('d', $definition->getOption('working-dir')->getShortcut()); + self::assertTrue($definition->hasOption('workspace-dir')); + self::assertSame('w', $definition->getOption('workspace-dir')->getShortcut()); self::assertTrue($definition->hasOption('auto-update')); } @@ -301,6 +325,21 @@ public function runAutoUpdateWhenRequestedWillUpdateGlobalInstallationWhenCurren $this->invokeRunAutoUpdateWhenRequested($input->reveal(), $output->reveal()); } + /** + * @return void + */ + #[Test] + public function configureWorkspaceDirectoryWillExposeWorkspaceDirectoryToManagedWorkspace(): void + { + $input = $this->prophesize(InputInterface::class); + $input->getParameterOption('--workspace-dir', null, true) + ->willReturn('.artifacts'); + + $this->invokeConfigureWorkspaceDirectory($input->reveal()); + + self::assertSame('.artifacts', ManagedWorkspace::getWorkspaceRoot()); + } + /** * @return DevTools */ @@ -354,4 +393,15 @@ private function invokeRunAutoUpdateWhenRequested(InputInterface $input, OutputI $reflectionMethod = new ReflectionMethod($this->devTools, 'runAutoUpdateWhenRequested'); $reflectionMethod->invoke($this->devTools, $input, $output); } + + /** + * @param InputInterface $input + * + * @return void + */ + private function invokeConfigureWorkspaceDirectory(InputInterface $input): void + { + $reflectionMethod = new ReflectionMethod($this->devTools, 'configureWorkspaceDirectory'); + $reflectionMethod->invoke($this->devTools, $input); + } } diff --git a/tests/Path/ManagedWorkspaceTest.php b/tests/Path/ManagedWorkspaceTest.php index bd83bf2584..ab50a6763c 100644 --- a/tests/Path/ManagedWorkspaceTest.php +++ b/tests/Path/ManagedWorkspaceTest.php @@ -24,9 +24,19 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; +use function Safe\putenv; + #[CoversClass(ManagedWorkspace::class)] final class ManagedWorkspaceTest extends TestCase { + /** + * @return void + */ + protected function tearDown(): void + { + putenv(ManagedWorkspace::ENV_WORKSPACE_DIR); + } + /** * @return void */ @@ -63,4 +73,39 @@ public function itWillNormalizePathSeparatorsWhenJoiningManagedPaths(): void self::assertSame('tmp/.dev-tools/metrics', ManagedWorkspace::getOutputDirectory('/metrics', 'tmp/')); self::assertSame('tmp/.dev-tools/cache/phpunit', ManagedWorkspace::getCacheDirectory('/phpunit', 'tmp/')); } + + /** + * @return void + */ + #[Test] + public function itWillUseConfiguredRelativeWorkspaceRoot(): void + { + putenv(ManagedWorkspace::ENV_WORKSPACE_DIR . '=.artifacts'); + + self::assertSame('.artifacts', ManagedWorkspace::getWorkspaceRoot()); + self::assertSame('.artifacts/coverage', ManagedWorkspace::getOutputDirectory(ManagedWorkspace::COVERAGE)); + self::assertSame( + 'tmp/.artifacts/cache/phpunit', + ManagedWorkspace::getCacheDirectory(ManagedWorkspace::PHPUNIT, 'tmp') + ); + } + + /** + * @return void + */ + #[Test] + public function itWillUseConfiguredAbsoluteWorkspaceRoot(): void + { + putenv(ManagedWorkspace::ENV_WORKSPACE_DIR . '=/tmp/dev-tools-artifacts'); + + self::assertSame('/tmp/dev-tools-artifacts', ManagedWorkspace::getWorkspaceRoot()); + self::assertSame( + '/tmp/dev-tools-artifacts/metrics', + ManagedWorkspace::getOutputDirectory(ManagedWorkspace::METRICS, 'tmp') + ); + self::assertSame( + '/tmp/dev-tools-artifacts/cache/rector', + ManagedWorkspace::getCacheDirectory(ManagedWorkspace::RECTOR, 'tmp') + ); + } } diff --git a/tests/Path/WorkingProjectPathResolverTest.php b/tests/Path/WorkingProjectPathResolverTest.php index 7494048b3d..9ce9c5dfd9 100644 --- a/tests/Path/WorkingProjectPathResolverTest.php +++ b/tests/Path/WorkingProjectPathResolverTest.php @@ -32,6 +32,7 @@ use function Safe\file_put_contents; use function Safe\getcwd; use function Safe\mkdir; +use function Safe\putenv; use function Safe\realpath; use function uniqid; @@ -39,6 +40,14 @@ #[UsesClass(ManagedWorkspace::class)] final class WorkingProjectPathResolverTest extends TestCase { + /** + * @return void + */ + protected function tearDown(): void + { + putenv(ManagedWorkspace::ENV_WORKSPACE_DIR); + } + /** * @return void */ @@ -63,6 +72,33 @@ public function itWillExposeCanonicalRepositoryRootPaths(): void ); } + /** + * @return void + */ + #[Test] + public function itWillIncludeCustomRelativeWorkspaceInToolingSkipPatterns(): void + { + putenv(ManagedWorkspace::ENV_WORKSPACE_DIR . '=.artifacts'); + + self::assertSame( + [ + 'repo/.dev-tools', + 'repo/backup', + 'repo/cache', + 'repo/public', + 'repo/resources', + 'repo/tmp', + 'repo/vendor', + 'repo/*/vendor', + 'repo/*/vendor/*', + 'repo/**/vendor', + 'repo/**/vendor/*', + 'repo/.artifacts', + ], + WorkingProjectPathResolver::getToolingExcludedDirectories('repo') + ); + } + /** * @return void */ @@ -145,6 +181,32 @@ public function itWillExposeToolingSourcePathsIgnoringExcludedDirectories(): voi } } + /** + * @return void + */ + #[Test] + public function itWillIgnoreCustomWorkspaceWhenResolvingToolingSourcePaths(): void + { + $fixtureDirectory = \dirname(__DIR__, 2) . '/backup/dev-tools-path-resolver-' . uniqid(); + + putenv(ManagedWorkspace::ENV_WORKSPACE_DIR . '=.artifacts'); + + mkdir($fixtureDirectory . '/src', recursive: true); + mkdir($fixtureDirectory . '/.artifacts/cache', recursive: true); + + file_put_contents($fixtureDirectory . '/src/Example.php', ' Date: Tue, 28 Apr 2026 21:06:14 +0000 Subject: [PATCH 2/2] Update wiki submodule pointer for PR #275 --- .github/wiki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/wiki b/.github/wiki index 20540e53c7..f1a8bc31ab 160000 --- a/.github/wiki +++ b/.github/wiki @@ -1 +1 @@ -Subproject commit 20540e53c7df86308a195c7b1fdb75b31571ea13 +Subproject commit f1a8bc31ab0d8e09146703773fceb77157c5d2ba