Repository: bmitch/churn-php (php)

Issue:
Add a --parallel option
This option would override the `parallelJobs` configuration.

By exposing such option it would be easier to use `churn` in conjunction with `nproc`.
Example:
```sh
./churn run src/ --parallel $(nproc)
```


Discussion:

Patch:
diff --git a/src/Command/RunCommand.php b/src/Command/RunCommand.php
--- a/src/Command/RunCommand.php
+++ b/src/Command/RunCommand.php
@@ -23,6 +23,7 @@
 use Symfony\Component\Console\Input\InputOption;
 use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Console\Output\StreamOutput;
+use Webmozart\Assert\Assert;
 
 /** @SuppressWarnings(PHPMD.CouplingBetweenObjects) */
 class RunCommand extends Command
@@ -78,9 +79,10 @@ protected function configure(): void
     {
         $this->setName('run')
             ->addArgument('paths', InputArgument::IS_ARRAY, 'Path to source to check.')
-            ->addOption('configuration', 'c', InputOption::VALUE_OPTIONAL, 'Path to the configuration file', 'churn.yml')  // @codingStandardsIgnoreLine
+            ->addOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'Path to the configuration file', 'churn.yml') // @codingStandardsIgnoreLine
             ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format to use', 'text')
             ->addOption('output', 'o', InputOption::VALUE_REQUIRED, 'The path where to write the result')
+            ->addOption('parallel', null, InputOption::VALUE_REQUIRED, 'Number of parallel jobs')
             ->addOption('progress', 'p', InputOption::VALUE_NONE, 'Show progress bar')
             ->setDescription('Check files')
             ->setHelp('Checks the churn on the provided path argument(s).');
@@ -95,13 +97,39 @@ protected function configure(): void
     protected function execute(InputInterface $input, OutputInterface $output): int
     {
         $this->displayLogo($input, $output);
-        $config = Loader::fromPath((string) $input->getOption('configuration'));
-        $accumulator = $this->analyze($input, $output, $config);
+        $accumulator = $this->analyze($input, $output, $this->getConfiguration($input));
         $this->writeResult($input, $output, $accumulator);
 
         return 0;
     }
 
+    /**
+     * @param InputInterface $input Input.
+     * @throws InvalidArgumentException If paths argument invalid.
+     */
+    private function getConfiguration(InputInterface $input): Config
+    {
+        $config = Loader::fromPath((string) $input->getOption('configuration'));
+
+        if ([] !== $input->getArgument('paths')) {
+            $config->setDirectoriesToScan($input->getArgument('paths'));
+        }
+
+        if ([] === $config->getDirectoriesToScan()) {
+            throw new InvalidArgumentException(
+                'Provide the directories you want to scan as arguments, ' .
+                'or configure them under "directoriesToScan" in your churn.yml file.'
+            );
+        }
+
+        if (null !== $input->getOption('parallel')) {
+            Assert::integerish($input->getOption('parallel'), 'Amount of parallel jobs should be an integer');
+            $config->setParallelJobs((int) $input->getOption('parallel'));
+        }
+
+        return $config;
+    }
+
     /**
      * Run the actual analysis.
      *
@@ -112,7 +140,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
     private function analyze(InputInterface $input, OutputInterface $output, Config $config): ResultAccumulator
     {
         $filesFinder = (new FileFinder($config->getFileExtensions(), $config->getFilesToIgnore()))
-            ->getPhpFiles($this->getDirectoriesToScan($input, $config->getDirectoriesToScan()));
+            ->getPhpFiles($config->getDirectoriesToScan());
         $accumulator = new ResultAccumulator($config->getFilesToShow(), $config->getMinScoreToShow());
         $this->processHandlerFactory->getProcessHandler($config)->process(
             $filesFinder,
@@ -123,32 +151,6 @@ private function analyze(InputInterface $input, OutputInterface $output, Config
         return $accumulator;
     }
 
-    /**
-     * Get the directories to scan.
-     *
-     * @param InputInterface $input Input Interface.
-     * @param array<string> $dirsConfigured The directories configured to scan.
-     * @throws InvalidArgumentException If paths argument invalid.
-     * @return array<string> When no directories to scan found.
-     */
-    private function getDirectoriesToScan(InputInterface $input, array $dirsConfigured): array
-    {
-        $dirsProvidedAsArgs = (array) $input->getArgument('paths');
-
-        if ([] !== $dirsProvidedAsArgs) {
-            return $dirsProvidedAsArgs;
-        }
-
-        if ([] !== $dirsConfigured) {
-            return $dirsConfigured;
-        }
-
-        throw new InvalidArgumentException(
-            'Provide the directories you want to scan as arguments, ' .
-            'or configure them under "directoriesToScan" in your churn.yml file.'
-        );
-    }
-
     /**
      * @param InputInterface $input Input.
      * @param OutputInterface $output Output.
diff --git a/src/Configuration/Config.php b/src/Configuration/Config.php
--- a/src/Configuration/Config.php
+++ b/src/Configuration/Config.php
@@ -4,8 +4,6 @@
 
 namespace Churn\Configuration;
 
-use Webmozart\Assert\Assert;
-
 class Config
 {
 
@@ -24,15 +22,12 @@ class Config
     private $configuration;
 
     /**
-     * Config constructor.
-     *
      * @param array<mixed> $configuration Raw config data.
-     * @throws \InvalidArgumentException If parameters is badly defined.
      */
     private function __construct(array $configuration = [])
     {
         if (!empty($configuration)) {
-            $this->validateConfigurationValues($configuration);
+            (new Validator())->validateConfigurationValues($configuration);
         }
 
         $this->configuration = $configuration;
@@ -57,7 +52,7 @@ public static function createFromDefaultValues(): Config
     }
 
     /**
-     * Get the names of directories to scan.
+     * Get the paths of directories to scan.
      *
      * @return array<string>
      */
@@ -66,6 +61,14 @@ public function getDirectoriesToScan(): array
         return $this->configuration['directoriesToScan'] ?? self::DIRECTORIES_TO_SCAN;
     }
 
+    /**
+     * @param array<string> $directories Paths of directories to scan.
+     */
+    public function setDirectoriesToScan(array $directories): void
+    {
+        $this->configuration['directoriesToScan'] = $directories;
+    }
+
     /**
      * Get the number of files to display in the results table.
      */
@@ -90,6 +93,14 @@ public function getParallelJobs(): int
         return $this->configuration['parallelJobs'] ?? self::AMOUNT_OF_PARALLEL_JOBS;
     }
 
+    /**
+     * @param integer $parallelJobs Number of parallel jobs.
+     */
+    public function setParallelJobs(int $parallelJobs): void
+    {
+        $this->configuration['parallelJobs'] = $parallelJobs;
+    }
+
     /**
      * Get how far back in the git history to go to count commits.
      */
@@ -99,8 +110,6 @@ public function getCommitsSince(): string
     }
 
     /**
-     * Get the paths to files to ignore when processing.
-     *
      * @return array<string>
      */
     public function getFilesToIgnore(): array
@@ -125,116 +134,4 @@ public function getVCS(): string
     {
         return $this->configuration['vcs'] ?? self::VCS;
     }
-
-    /**
-     * @param array<mixed> $configuration The array containing the configuration values.
-     */
-    private function validateConfigurationValues(array $configuration): void
-    {
-        $this->validateDirectoriesToScan($configuration);
-        $this->validateFilesToShow($configuration);
-        $this->validateMinScoreToShow($configuration);
-        $this->validateParallelJobs($configuration);
-        $this->validateCommitsSince($configuration);
-        $this->validateFilesToIgnore($configuration);
-        $this->validateFileExtensions($configuration);
-        $this->validateVCS($configuration);
-    }
-
-    /**
-     * @param array<mixed> $configuration The array containing the configuration values.
-     */
-    private function validateDirectoriesToScan(array $configuration): void
-    {
-        if (!\array_key_exists('directoriesToScan', $configuration)) {
-            return;
-        }
-
-        Assert::allString($configuration['directoriesToScan'], 'Directories to scan should be an array of strings');
-    }
-
-    /**
-     * @param array<mixed> $configuration The array containing the configuration values.
-     */
-    private function validateFilesToShow(array $configuration): void
-    {
-        if (!\array_key_exists('filesToShow', $configuration)) {
-            return;
-        }
-
-        Assert::integer($configuration['filesToShow'], 'Files to show should be an integer');
-    }
-
-    /**
-     * @param array<mixed> $configuration The array containing the configuration values.
-     */
-    private function validateMinScoreToShow(array $configuration): void
-    {
-        if (!\array_key_exists('minScoreToShow', $configuration)) {
-            return;
-        }
-
-        Assert::numeric($configuration['minScoreToShow'], 'Minimum score to show should be a number');
-    }
-
-    /**
-     * @param array<mixed> $configuration The array containing the configuration values.
-     */
-    private function validateParallelJobs(array $configuration): void
-    {
-        if (!\array_key_exists('parallelJobs', $configuration)) {
-            return;
-        }
-
-        Assert::integer($configuration['parallelJobs'], 'Amount of parallel jobs should be an integer');
-    }
-
-    /**
-     * @param array<mixed> $configuration The array containing the configuration values.
-     * @throws \InvalidArgumentException If date is in a bad format.
-     */
-    private function validateCommitsSince(array $configuration): void
-    {
-        if (!\array_key_exists('commitsSince', $configuration)) {
-            return;
-        }
-
-        Assert::string($configuration['commitsSince'], 'Commits since should be a string');
-    }
-
-    /**
-     * @param array<mixed> $configuration The array containing the configuration values.
-     */
-    private function validateFilesToIgnore(array $configuration): void
-    {
-        if (!\array_key_exists('filesToIgnore', $configuration)) {
-            return;
-        }
-
-        Assert::isArray($configuration['filesToIgnore'], 'Files to ignore should be an array of strings');
-    }
-
-    /**
-     * @param array<mixed> $configuration The array containing the configuration values.
-     */
-    private function validateFileExtensions(array $configuration): void
-    {
-        if (!\array_key_exists('fileExtensions', $configuration)) {
-            return;
-        }
-
-        Assert::isArray($configuration['fileExtensions'], 'File extensions should be an array of strings');
-    }
-
-    /**
-     * @param array<mixed> $configuration The array containing the configuration values.
-     */
-    private function validateVCS(array $configuration): void
-    {
-        if (!\array_key_exists('vcs', $configuration)) {
-            return;
-        }
-
-        Assert::string($configuration['vcs'], 'VCS should be a string');
-    }
 }
diff --git a/src/Configuration/Validator.php b/src/Configuration/Validator.php
new file mode 100644
--- /dev/null
+++ b/src/Configuration/Validator.php
@@ -0,0 +1,122 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Churn\Configuration;
+
+use Webmozart\Assert\Assert;
+
+class Validator
+{
+
+    /**
+     * @param array<mixed> $configuration The array containing the configuration values.
+     */
+    public function validateConfigurationValues(array $configuration): void
+    {
+        $this->validateDirectoriesToScan($configuration);
+        $this->validateFilesToShow($configuration);
+        $this->validateMinScoreToShow($configuration);
+        $this->validateParallelJobs($configuration);
+        $this->validateCommitsSince($configuration);
+        $this->validateFilesToIgnore($configuration);
+        $this->validateFileExtensions($configuration);
+        $this->validateVCS($configuration);
+    }
+
+    /**
+     * @param array<mixed> $configuration The array containing the configuration values.
+     */
+    private function validateDirectoriesToScan(array $configuration): void
+    {
+        if (!\array_key_exists('directoriesToScan', $configuration)) {
+            return;
+        }
+
+        Assert::allString($configuration['directoriesToScan'], 'Directories to scan should be an array of strings');
+    }
+
+    /**
+     * @param array<mixed> $configuration The array containing the configuration values.
+     */
+    private function validateFilesToShow(array $configuration): void
+    {
+        if (!\array_key_exists('filesToShow', $configuration)) {
+            return;
+        }
+
+        Assert::integer($configuration['filesToShow'], 'Files to show should be an integer');
+    }
+
+    /**
+     * @param array<mixed> $configuration The array containing the configuration values.
+     */
+    private function validateMinScoreToShow(array $configuration): void
+    {
+        if (!\array_key_exists('minScoreToShow', $configuration)) {
+            return;
+        }
+
+        Assert::numeric($configuration['minScoreToShow'], 'Minimum score to show should be a number');
+    }
+
+    /**
+     * @param array<mixed> $configuration The array containing the configuration values.
+     */
+    private function validateParallelJobs(array $configuration): void
+    {
+        if (!\array_key_exists('parallelJobs', $configuration)) {
+            return;
+        }
+
+        Assert::integer($configuration['parallelJobs'], 'Amount of parallel jobs should be an integer');
+    }
+
+    /**
+     * @param array<mixed> $configuration The array containing the configuration values.
+     */
+    private function validateCommitsSince(array $configuration): void
+    {
+        if (!\array_key_exists('commitsSince', $configuration)) {
+            return;
+        }
+
+        Assert::string($configuration['commitsSince'], 'Commits since should be a string');
+    }
+
+    /**
+     * @param array<mixed> $configuration The array containing the configuration values.
+     */
+    private function validateFilesToIgnore(array $configuration): void
+    {
+        if (!\array_key_exists('filesToIgnore', $configuration)) {
+            return;
+        }
+
+        Assert::isArray($configuration['filesToIgnore'], 'Files to ignore should be an array of strings');
+    }
+
+    /**
+     * @param array<mixed> $configuration The array containing the configuration values.
+     */
+    private function validateFileExtensions(array $configuration): void
+    {
+        if (!\array_key_exists('fileExtensions', $configuration)) {
+            return;
+        }
+
+        Assert::isArray($configuration['fileExtensions'], 'File extensions should be an array of strings');
+    }
+
+    /**
+     * @param array<mixed> $configuration The array containing the configuration values.
+     */
+    private function validateVCS(array $configuration): void
+    {
+        if (!\array_key_exists('vcs', $configuration)) {
+            return;
+        }
+
+        Assert::string($configuration['vcs'], 'VCS should be a string');
+    }
+}


Test patch:
diff --git a/tests/Integration/Command/RunCommandTest.php b/tests/Integration/Command/RunCommandTest.php
--- a/tests/Integration/Command/RunCommandTest.php
+++ b/tests/Integration/Command/RunCommandTest.php
@@ -39,7 +39,10 @@ protected function tearDown()
     /** @test */
     public function it_displays_the_logo_at_the_beginning_by_default()
     {
-        $exitCode = $this->commandTester->execute(['paths' => [realpath(__DIR__ . '/../..')]]);
+        $exitCode = $this->commandTester->execute([
+            'paths' => [__DIR__],
+            '--parallel' => '1',
+        ]);
         $display = $this->commandTester->getDisplay();
 
         $this->assertEquals(0, $exitCode);
@@ -49,7 +52,7 @@ public function it_displays_the_logo_at_the_beginning_by_default()
     /** @test */
     public function it_can_return_a_json_report()
     {
-        $exitCode = $this->commandTester->execute(['paths' => [realpath(__DIR__ . '/../..')], '--format' => 'json']);
+        $exitCode = $this->commandTester->execute(['paths' => [__DIR__], '--format' => 'json']);
         $data = \json_decode($this->commandTester->getDisplay(), true);
 
         $this->assertEquals(0, $exitCode);
@@ -61,7 +64,7 @@ public function it_can_write_a_json_report()
     {
         $this->tmpFile = \tempnam(\sys_get_temp_dir(), 'churn-test-');
         $exitCode = $this->commandTester->execute([
-            'paths' => [realpath(__DIR__ . '/../..')],
+            'paths' => [\realpath(__DIR__ . '/../../')],
             '--format' => 'json',
             '--output' => $this->tmpFile,
         ]);
@@ -92,7 +95,7 @@ public function it_throws_for_invalid_configuration(): void
     {
         $this->expectException(InvalidArgumentException::class);
         $this->commandTester->execute([
-            'paths' => [realpath(__DIR__ . '/../..')],
+            'paths' => [__DIR__],
             '--configuration' => 'not a valid configuration file',
         ]);
     }