Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
60 / 60
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
TestsCommand
100.00% covered (success)
100.00%
60 / 60
100.00% covered (success)
100.00%
3 / 3
6
100.00% covered (success)
100.00%
1 / 1
 configure
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
4
 resolvePath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5/**
6 * This file is part of fast-forward/dev-tools.
7 *
8 * This source file is subject to the license bundled
9 * with this source code in the file LICENSE.
10 *
11 * @copyright Copyright (c) 2026 Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
12 * @license   https://opensource.org/licenses/MIT MIT License
13 *
14 * @see       https://github.com/php-fast-forward/dev-tools
15 * @see       https://github.com/php-fast-forward
16 * @see       https://datatracker.ietf.org/doc/html/rfc2119
17 */
18
19namespace FastForward\DevTools\Command;
20
21use Symfony\Component\Console\Input\InputArgument;
22use Symfony\Component\Console\Input\InputInterface;
23use Symfony\Component\Console\Input\InputOption;
24use Symfony\Component\Console\Output\OutputInterface;
25use Symfony\Component\Process\Process;
26
27/**
28 * Facilitates the execution of the PHPUnit testing framework.
29 * This class MUST NOT be overridden and SHALL configure testing parameters dynamically.
30 */
31final class TestsCommand extends AbstractCommand
32{
33    /**
34     * @var string identifies the local configuration file for PHPUnit processes
35     */
36    public const string CONFIG = 'phpunit.xml';
37
38    /**
39     * Configures the testing command input constraints.
40     *
41     * The method MUST specify valid arguments for testing paths, caching directories,
42     * bootstrap scripts, and coverage instructions. It SHALL align with robust testing standards.
43     *
44     * @return void
45     */
46    protected function configure(): void
47    {
48        $this
49            ->setName('tests')
50            ->setDescription('Runs PHPUnit tests.')
51            ->setHelp('This command runs PHPUnit to execute your tests.')
52            ->addArgument(
53                name: 'path',
54                mode: InputArgument::OPTIONAL,
55                description: 'Path to the tests directory.',
56                default: './tests',
57            )
58            ->addOption(
59                name: 'bootstrap',
60                shortcut: 'b',
61                mode: InputOption::VALUE_OPTIONAL,
62                description: 'Path to the bootstrap file.',
63                default: './vendor/autoload.php',
64            )
65            ->addOption(
66                name: 'cache-dir',
67                mode: InputOption::VALUE_OPTIONAL,
68                description: 'Path to the PHPUnit cache directory.',
69                default: './tmp/cache/phpunit',
70            )
71            ->addOption(
72                name: 'no-cache',
73                mode: InputOption::VALUE_NONE,
74                description: 'Whether to disable PHPUnit caching.',
75            )
76            ->addOption(
77                name: 'coverage',
78                shortcut: 'c',
79                mode: InputOption::VALUE_OPTIONAL,
80                description: 'Whether to generate code coverage reports.',
81            );
82    }
83
84    /**
85     * Triggers the PHPUnit engine based on resolved paths and settings.
86     *
87     * The method MUST assemble the necessary commands to initiate PHPUnit securely.
88     * It SHOULD optionally construct advanced configuration arguments such as caching and coverage.
89     *
90     * @param InputInterface $input the runtime instruction set from the CLI
91     * @param OutputInterface $output the console feedback relay
92     *
93     * @return int the status integer describing the termination code
94     */
95    protected function execute(InputInterface $input, OutputInterface $output): int
96    {
97        $output->writeln('<info>Running PHPUnit tests...</info>');
98
99        $arguments = [
100            $this->getAbsolutePath('vendor/bin/phpunit'),
101            '--configuration=' . parent::getConfigFile(self::CONFIG),
102            '--bootstrap=' . $this->resolvePath($input, 'bootstrap'),
103            '--display-deprecations',
104            '--display-phpunit-deprecations',
105            '--display-incomplete',
106            '--display-skipped',
107        ];
108
109        if (! $input->getOption('no-cache')) {
110            $arguments[] = '--cache-directory=' . $this->resolvePath($input, 'cache-dir');
111        }
112
113        if ($input->getOption('coverage')) {
114            $output->writeln(
115                '<info>Generating code coverage reports on path: ' . $this->resolvePath($input, 'coverage') . '</info>'
116            );
117
118            foreach ($this->getPsr4Namespaces() as $path) {
119                $arguments[] = '--coverage-filter=' . $this->getAbsolutePath($path);
120            }
121
122            $arguments[] = '--coverage-text';
123            $arguments[] = '--coverage-html=' . $this->resolvePath($input, 'coverage');
124            $arguments[] = '--testdox-html=' . $this->resolvePath($input, 'coverage') . '/testdox.html';
125            $arguments[] = '--coverage-clover=' . $this->resolvePath($input, 'coverage') . '/clover.xml';
126            $arguments[] = '--coverage-php=' . $this->resolvePath($input, 'coverage') . '/coverage.php';
127        }
128
129        $command = new Process([...$arguments, $input->getArgument('path')]);
130
131        return parent::runProcess($command, $output);
132    }
133
134    /**
135     * Safely constructs an absolute path tied to a defined capability option.
136     *
137     * The method MUST compute absolute properties based on the supplied input parameters.
138     * It SHALL strictly return a securely bounded path string.
139     *
140     * @param InputInterface $input the raw parameter definitions
141     * @param string $option the requested option key to resolve
142     *
143     * @return string validated absolute path string
144     */
145    private function resolvePath(InputInterface $input, string $option): string
146    {
147        return $this->getAbsolutePath($input->getOption($option));
148    }
149}