Repository: bmitch/churn-php (php)

Issue:
Use a cache to speed up calculations
I just started to use this library. I have not looked into the source yet so Im sorry if I ask a weird question. 

Would it be possible to use a cache to speed up execution? 
What if the cache contained complexity and changes for every file in my project up until yesterday. Then when I do `churn run src` I will only calculate the change set between yesterday and now. 


Discussion:
Thank you for the suggestion @Nyholm 

It does sound like a good idea but a couple of notes:
* I think most people run this during a CI pipeline which probably would not have access to a cache since every run via the pipeline is essentially fresh. However, if you are running this locally over and over then a cache could be helpful.
* If you are looking for a way to increase the performance you could limit how far back in the git history it looks. See https://github.com/bmitch/churn-php#how-to-configure and the `commitsSince`. Perhaps that will solve your issue?

Are you having poor performance using this tool? If so please feel free to open another issue with details. Thanks!
> which probably would not have access to a cache since every run via the pipeline is essentially fresh

I know both Travis and Gitlab's CI can use caching. I mean we just started to use it here #191.

> If you are looking for a way to increase the performance you could limit how far back in the git history it looks.

I tried that. My project takes about 5 minutes to run on my MacBook Pro 2018. Changing the value of `commitsSince` does not effect the execution time. (Only the result). I tried with 3 years and 1 week. 

My project is not enormous. It is about 4.000 files with history from 5 years. 

--------

Thank you for your reply. 


Patch:
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -69,9 +69,9 @@ docker run --rm -ti -v $PWD:/app dockerizedphp/churn run src
 You may add an optional `churn.yml` file which can be used to configure churn-php. The location of the churn.yml file can be customized using these commands:
 
 ```
-Default: "churn.yml" 
-vendor/bin/churn run -c <path>
-vendor/bin/churn run --configuration[=CONFIGURATION] <path>
+# Default: "churn.yml" 
+vendor/bin/churn run --configuration=config-dir/ <path>
+vendor/bin/churn run --configuration=my-config.yml <path>
 ```
 A sample `churn.yml` file looks like:
 
@@ -108,10 +108,21 @@ fileExtensions:
  - php
  - inc
 
+# This list is used only if there is no argument when running churn.
+# Default: <empty>
+directoriesToScan:
+ - src
+ - tests/
+
 # The version control system used for your project.
 # Accepted values: fossil, git, mercurial, subversion, none
 # Default: git
  vcs: git
+
+# The path of the cache file. It doesn't need to exist before running churn.
+# Disabled if null.
+# Default: null
+ cachePath: .churn.cache
  ```
 
 If a `churn.yml` file is omitted or an individual setting is omitted the default values above will be used.
diff --git a/composer.json b/composer.json
--- a/composer.json
+++ b/composer.json
@@ -17,6 +17,7 @@
         "php": ">=7.1.3",
         "composer/package-versions-deprecated": "^1.11",
         "symfony/console": "^3.4 || ^4.0 || ^5.0",
+        "symfony/filesystem": "^3.4 || ^4.0 || ^5.0",
         "symfony/process": "^3.4 || ^4.0 || ^5.0",
         "symfony/yaml": "^3.4 || ^4.0 || ^5.0",
         "webmozart/assert": "^1.2"
diff --git a/src/Command/RunCommand.php b/src/Command/RunCommand.php
--- a/src/Command/RunCommand.php
+++ b/src/Command/RunCommand.php
@@ -8,10 +8,9 @@
 use Churn\Configuration\Loader;
 use Churn\File\FileFinder;
 use Churn\File\FileHelper;
-use Churn\Process\Observer\OnSuccess;
-use Churn\Process\Observer\OnSuccessAccumulate;
-use Churn\Process\Observer\OnSuccessCollection;
-use Churn\Process\Observer\OnSuccessProgress;
+use Churn\Process\CacheProcessFactory;
+use Churn\Process\ConcreteProcessFactory;
+use Churn\Process\Observer\OnSuccessBuilder;
 use Churn\Process\ProcessFactory;
 use Churn\Process\ProcessHandlerFactory;
 use Churn\Result\ResultAccumulator;
@@ -97,7 +96,7 @@ protected function configure(): void
      */
     protected function execute(InputInterface $input, OutputInterface $output): int
     {
-        $this->displayLogo($input, $output);
+        $this->printLogo($input, $output);
         $accumulator = $this->analyze($input, $output, $this->getConfiguration($input));
         $this->writeResult($input, $output, $accumulator);
 
@@ -140,14 +139,20 @@ private function getConfiguration(InputInterface $input): Config
      */
     private function analyze(InputInterface $input, OutputInterface $output, Config $config): ResultAccumulator
     {
+        $observers = [];
+        if (true === $input->getOption('progress')) {
+            $observers[] = $progressBar = new ProgressBar($output);
+            $progressBar->start();
+        }
         $filesFinder = (new FileFinder($config->getFileExtensions(), $config->getFilesToIgnore()))
             ->getPhpFiles($this->getDirectoriesToScan($input, $config));
-        $accumulator = new ResultAccumulator($config->getFilesToShow(), $config->getMinScoreToShow());
-        $this->processHandlerFactory->getProcessHandler($config)->process(
-            $filesFinder,
-            new ProcessFactory($config->getVCS(), $config->getCommitsSince()),
-            $this->getOnSuccessObserver($input, $output, $accumulator)
-        );
+        $observers[] = $accumulator = new ResultAccumulator($config->getFilesToShow(), $config->getMinScoreToShow());
+        $observers[] = $processFactory = $this->getProcessFactory($config);
+        $observer = (new OnSuccessBuilder())->build($observers);
+        $this->processHandlerFactory->getProcessHandler($config)->process($filesFinder, $processFactory, $observer);
+        if ($processFactory instanceof CacheProcessFactory) {
+            $processFactory->writeCache();
+        }
 
         return $accumulator;
     }
@@ -172,31 +177,28 @@ private function getDirectoriesToScan(InputInterface $input, Config $config): ar
     }
 
     /**
-     * @param InputInterface $input Input.
-     * @param OutputInterface $output Output.
-     * @param ResultAccumulator $accumulator The object accumulating the results.
+     * @param Config $config The configuration object.
      */
-    private function getOnSuccessObserver(
-        InputInterface $input,
-        OutputInterface $output,
-        ResultAccumulator $accumulator
-    ): OnSuccess {
-        $observer = new OnSuccessAccumulate($accumulator);
-
-        if (true === $input->getOption('progress')) {
-            $progressBar = new ProgressBar($output);
-            $progressBar->start();
-            $observer = new OnSuccessCollection($observer, new OnSuccessProgress($progressBar));
+    private function getProcessFactory(Config $config): ProcessFactory
+    {
+        $factory = new ConcreteProcessFactory($config->getVCS(), $config->getCommitsSince());
+
+        if (null !== $config->getCachePath()) {
+            $basePath = $config->getPath()
+                ? \dirname($config->getPath())
+                : \getcwd();
+            $path = $config->getCachePath();
+            $factory = new CacheProcessFactory(FileHelper::toAbsolutePath($path, $basePath), $factory);
         }
 
-        return $observer;
+        return $factory;
     }
 
     /**
      * @param InputInterface $input Input.
      * @param OutputInterface $output Output.
      */
-    private function displayLogo(InputInterface $input, OutputInterface $output): void
+    private function printLogo(InputInterface $input, OutputInterface $output): void
     {
         if ('text' !== $input->getOption('format') && empty($input->getOption('output'))) {
             return;
diff --git a/src/Configuration/Config.php b/src/Configuration/Config.php
--- a/src/Configuration/Config.php
+++ b/src/Configuration/Config.php
@@ -15,6 +15,7 @@ class Config
     private const FILES_TO_IGNORE = [];
     private const FILE_EXTENSIONS_TO_PARSE = ['php'];
     private const VCS = 'git';
+    private const CACHE_PATH = null;
 
     /**
      * @var array<string, mixed>
@@ -154,4 +155,12 @@ public function getVCS(): string
     {
         return $this->configuration['vcs'] ?? self::VCS;
     }
+
+    /**
+     * Get the cache file path.
+     */
+    public function getCachePath(): ?string
+    {
+        return $this->configuration['cachePath'] ?? self::CACHE_PATH;
+    }
 }
diff --git a/src/Configuration/Validator.php b/src/Configuration/Validator.php
--- a/src/Configuration/Validator.php
+++ b/src/Configuration/Validator.php
@@ -22,6 +22,7 @@ public function validateConfigurationValues(array $configuration): void
         $this->validateFilesToIgnore($configuration);
         $this->validateFileExtensions($configuration);
         $this->validateVCS($configuration);
+        $this->validateCachePath($configuration);
     }
 
     /**
@@ -33,6 +34,7 @@ private function validateDirectoriesToScan(array $configuration): void
             return;
         }
 
+        Assert::isArray($configuration['directoriesToScan'], 'Directories to scan should be an array of strings');
         Assert::allString($configuration['directoriesToScan'], 'Directories to scan should be an array of strings');
     }
 
@@ -94,6 +96,7 @@ private function validateFilesToIgnore(array $configuration): void
         }
 
         Assert::isArray($configuration['filesToIgnore'], 'Files to ignore should be an array of strings');
+        Assert::allString($configuration['filesToIgnore'], 'Files to ignore should be an array of strings');
     }
 
     /**
@@ -106,6 +109,7 @@ private function validateFileExtensions(array $configuration): void
         }
 
         Assert::isArray($configuration['fileExtensions'], 'File extensions should be an array of strings');
+        Assert::allString($configuration['fileExtensions'], 'File extensions should be an array of strings');
     }
 
     /**
@@ -119,4 +123,16 @@ private function validateVCS(array $configuration): void
 
         Assert::string($configuration['vcs'], 'VCS should be a string');
     }
+
+    /**
+     * @param array<mixed> $configuration The array containing the configuration values.
+     */
+    private function validateCachePath(array $configuration): void
+    {
+        if (!isset($configuration['cachePath'])) {
+            return;
+        }
+
+        Assert::string($configuration['cachePath'], 'Cache path should be a string');
+    }
 }
diff --git a/src/File/FileHelper.php b/src/File/FileHelper.php
--- a/src/File/FileHelper.php
+++ b/src/File/FileHelper.php
@@ -4,6 +4,9 @@
 
 namespace Churn\File;
 
+use InvalidArgumentException;
+use Symfony\Component\Filesystem\Filesystem;
+
 class FileHelper
 {
 
@@ -14,20 +17,35 @@ class FileHelper
      */
     public static function toAbsolutePath(string $path, string $basePath): string
     {
-        $path = \trim($path);
+        return (new Filesystem())->isAbsolutePath($path)
+            ? $path
+            : $basePath . '/' . $path;
+    }
 
-        if (0 === \strpos($path, '/')) {
-            return $path;
+    /**
+     * Check whether the path is writable and create the missing folders if needed.
+     *
+     * @param string $filePath The file path to check.
+     * @throws InvalidArgumentException If the path is invalid.
+     */
+    public static function ensureFileIsWritable(string $filePath): void
+    {
+        if ('' === $filePath) {
+            throw new InvalidArgumentException('Path cannot be empty');
         }
 
-        if ('\\' === $path[0] || (\strlen($path) >= 3 && \preg_match('#^[A-Z]\:[/\\\]#i', \substr($path, 0, 3)))) {
-            return $path;
+        if (\is_dir($filePath)) {
+            throw new InvalidArgumentException('Path cannot be a folder');
         }
 
-        if (false !== \strpos($path, '://')) {
-            return $path;
+        if (!\is_file($filePath)) {
+            (new Filesystem())->mkdir(\dirname($filePath));
+
+            return;
         }
 
-        return $basePath . '/' . $path;
+        if (!\is_writable($filePath)) {
+            throw new InvalidArgumentException('File is not writable');
+        }
     }
 }
diff --git a/src/Process/CacheProcessFactory.php b/src/Process/CacheProcessFactory.php
new file mode 100644
--- /dev/null
+++ b/src/Process/CacheProcessFactory.php
@@ -0,0 +1,138 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Churn\Process;
+
+use Churn\File\File;
+use Churn\File\FileHelper;
+use Churn\Result\Result;
+use InvalidArgumentException;
+use Throwable;
+
+class CacheProcessFactory implements ProcessFactory
+{
+
+    /**
+     * @var string The cache file path.
+     */
+    private $cachePath;
+
+    /**
+     * @var ProcessFactory Inner process factory.
+     */
+    private $processFactory;
+
+    /**
+     * @var array<string, array<mixed>> The cached data.
+     */
+    private $cache;
+
+    /**
+     * @param string $cachePath The cache file path.
+     * @param ProcessFactory $processFactory Inner process factory.
+     * @throws InvalidArgumentException If the path is invalid.
+     */
+    public function __construct(string $cachePath, ProcessFactory $processFactory)
+    {
+        try {
+            FileHelper::ensureFileIsWritable($cachePath);
+        } catch (Throwable $e) {
+            $message = 'Invalid cache file path: ' . $e->getMessage();
+
+            throw new InvalidArgumentException($message, $e->getCode(), $e);
+        }
+
+        $this->cachePath = $cachePath;
+        $this->processFactory = $processFactory;
+        $this->cache = $this->loadCache($cachePath);
+    }
+
+    /**
+     * @param File $file File that the processes will execute on.
+     * @return iterable<ProcessInterface> The list of processes to execute.
+     */
+    public function createProcesses(File $file): iterable
+    {
+        if (!$this->isCached($file)) {
+            return $this->processFactory->createProcesses($file);
+        }
+
+        $key = $file->getFullPath();
+
+        $countChanges = (int) $this->cache[$key][1];
+        $cyclomaticComplexity = (int) $this->cache[$key][2];
+        $this->cache[$key][3] = true;
+
+        return [new PredefinedProcess($file, $countChanges, $cyclomaticComplexity)];
+    }
+
+    /**
+     * @param Result $result The result to save.
+     */
+    public function addToCache(Result $result): void
+    {
+        $path = $result->getFile()->getFullPath();
+        $this->cache[$path][0] = $this->cache[$path][0] ?? \md5_file($path);
+        $this->cache[$path][1] = $result->getCommits();
+        $this->cache[$path][2] = $result->getComplexity();
+        $this->cache[$path][3] = true;
+    }
+
+    /**
+     * Write the cache in its file.
+     */
+    public function writeCache(): void
+    {
+        $data = [];
+
+        foreach ($this->cache as $path => $values) {
+            if (!$values[3]) {
+                continue;
+            }
+
+            unset($values[3]);
+            $data[] = \implode(',', \array_merge([$path], $values));
+        }
+
+        \file_put_contents($this->cachePath, \implode("\n", $data));
+    }
+
+    /**
+     * @param File $file The file to process.
+     */
+    private function isCached(File $file): bool
+    {
+        $key = $file->getFullPath();
+
+        if (!isset($this->cache[$key])) {
+            return false;
+        }
+
+        $md5 = $this->cache[$key][0];
+        $this->cache[$key][0] = $newMd5 = \md5_file($file->getFullPath());
+
+        return $md5 === $newMd5;
+    }
+
+    /**
+     * @param string $cachePath Cache file path.
+     * @return array<string, array<mixed>>
+     */
+    private function loadCache(string $cachePath): array
+    {
+        if (!\is_file($cachePath)) {
+            return [];
+        }
+
+        $cache = [];
+
+        foreach (\file($cachePath) as $row) {
+            $data = \explode(',', $row);
+            $cache[$data[0]] = \array_slice($data, 1);
+            $cache[$data[0]][3] = false;
+        }
+
+        return $cache;
+    }
+}
diff --git a/src/Process/ChurnProcess.php b/src/Process/ChurnProcess.php
--- a/src/Process/ChurnProcess.php
+++ b/src/Process/ChurnProcess.php
@@ -8,7 +8,7 @@
 use Symfony\Component\Process\Exception\ProcessFailedException;
 use Symfony\Component\Process\Process;
 
-abstract class ChurnProcess
+abstract class ChurnProcess implements ProcessInterface
 {
 
     /**
diff --git a/src/Process/ConcreteProcessFactory.php b/src/Process/ConcreteProcessFactory.php
new file mode 100644
--- /dev/null
+++ b/src/Process/ConcreteProcessFactory.php
@@ -0,0 +1,90 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Churn\Process;
+
+use Churn\File\File;
+use Closure;
+use Phar;
+use Symfony\Component\Process\PhpExecutableFinder;
+use Symfony\Component\Process\Process;
+
+class ConcreteProcessFactory implements ProcessFactory
+{
+
+    /**
+     * Builder of objects implementing ChangesCountInterface.
+     *
+     * @var Closure
+     */
+    private $changesCountProcessBuilder;
+
+    /**
+     * Builder of objects implementing CyclomaticComplexityInterface.
+     *
+     * @var Closure
+     */
+    private $cyclomaticComplexityBuilder;
+
+    /**
+     * Class constructor.
+     *
+     * @param string $vcs Name of the version control system.
+     * @param string $commitsSince String containing the date of when to look at commits since.
+     */
+    public function __construct(string $vcs, string $commitsSince)
+    {
+        $this->changesCountProcessBuilder = $this->getChangesCountProcessBuilder($vcs, $commitsSince);
+        $this->cyclomaticComplexityBuilder = $this->getCyclomaticComplexityProcessBuilder();
+    }
+
+    /**
+     * @param File $file File that the processes will execute on.
+     * @return iterable<ProcessInterface> The list of processes to execute.
+     */
+    public function createProcesses(File $file): iterable
+    {
+        $processes = [];
+        $processes[] = ($this->changesCountProcessBuilder)($file);
+        $processes[] = ($this->cyclomaticComplexityBuilder)($file);
+
+        return $processes;
+    }
+
+    /**
+     * @param string $vcs Name of the version control system.
+     * @param string $commitsSince String containing the date of when to look at commits since.
+     * @throws InvalidArgumentException If VCS is not supported.
+     */
+    private function getChangesCountProcessBuilder(string $vcs, string $commitsSince): Closure
+    {
+        return (new ChangesCountProcessBuilder())->getBuilder($vcs, $commitsSince);
+    }
+
+    /**
+     * Returns a cyclomatic complexity builder.
+     */
+    private function getCyclomaticComplexityProcessBuilder(): Closure
+    {
+        $phpExecutable = (string)(new PhpExecutableFinder())->find();
+        $command = \array_merge([$phpExecutable], $this->getAssessorArguments());
+
+        return static function (File $file) use ($command): CyclomaticComplexityInterface {
+            $command[] = $file->getFullPath();
+            $process = new Process($command);
+
+            return new CyclomaticComplexityProcess($file, $process);
+        };
+    }
+
+    /** @return array<string> */
+    private function getAssessorArguments(): array
+    {
+        if (\is_callable([Phar::class, 'running']) && '' !== Phar::running(false)) {
+            return [Phar::running(false), 'assess-complexity'];
+        }
+
+        return [__DIR__ . '/../../bin/CyclomaticComplexityAssessorRunner'];
+    }
+}
diff --git a/src/Process/Handler/BaseProcessHandler.php b/src/Process/Handler/BaseProcessHandler.php
new file mode 100644
--- /dev/null
+++ b/src/Process/Handler/BaseProcessHandler.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Churn\Process\Handler;
+
+use Churn\Process\ChangesCountInterface;
+use Churn\Process\CyclomaticComplexityInterface;
+use Churn\Process\ProcessInterface;
+use Churn\Result\Result;
+
+abstract class BaseProcessHandler implements ProcessHandler
+{
+
+    /**
+     * @param ProcessInterface $process A successful process.
+     * @param Result $result The result object to hydrate.
+     */
+    protected function saveResult(ProcessInterface $process, Result $result): Result
+    {
+        if ($process instanceof ChangesCountInterface) {
+            $result->setCommits($process->countChanges());
+        }
+
+        if ($process instanceof CyclomaticComplexityInterface) {
+            $result->setComplexity($process->getCyclomaticComplexity());
+        }
+
+        return $result;
+    }
+}
diff --git a/src/Process/Handler/ParallelProcessHandler.php b/src/Process/Handler/ParallelProcessHandler.php
--- a/src/Process/Handler/ParallelProcessHandler.php
+++ b/src/Process/Handler/ParallelProcessHandler.php
@@ -5,15 +5,13 @@
 namespace Churn\Process\Handler;
 
 use Churn\File\File;
-use Churn\Process\ChangesCountInterface;
-use Churn\Process\CyclomaticComplexityInterface;
 use Churn\Process\Observer\OnSuccess;
 use Churn\Process\ProcessFactory;
 use Churn\Process\ProcessInterface;
 use Churn\Result\Result;
 use Generator;
 
-class ParallelProcessHandler implements ProcessHandler
+class ParallelProcessHandler extends BaseProcessHandler
 {
 
     /**
@@ -88,12 +86,7 @@ private function checkRunningProcesses(array &$pool, OnSuccess $onSuccess): void
      */
     private function addToPool(array &$pool, File $file, ProcessFactory $processFactory): void
     {
-        $processes = [
-            $processFactory->createChangesCountProcess($file),
-            $processFactory->createCyclomaticComplexityProcess($file),
-        ];
-
-        foreach ($processes as $i => $process) {
+        foreach ($processFactory->createProcesses($file) as $i => $process) {
             $process->start();
             $pool["$i:" . $file->getDisplayPath()] = $process;
         }
@@ -107,17 +100,9 @@ private function addToPool(array &$pool, File $file, ProcessFactory $processFact
     private function getResult(ProcessInterface $process): Result
     {
         $key = $process->getFile()->getDisplayPath();
-        $result = $this->completedProcesses[$key] = $this->completedProcesses[$key] ?? new Result($key);
-
-        if ($process instanceof ChangesCountInterface) {
-            $result->setCommits($process->countChanges());
-        }
-
-        if ($process instanceof CyclomaticComplexityInterface) {
-            $result->setComplexity($process->getCyclomaticComplexity());
-        }
+        $this->completedProcesses[$key] = $this->completedProcesses[$key] ?? new Result($process->getFile());
 
-        return $result;
+        return $this->saveResult($process, $this->completedProcesses[$key]);
     }
 
     /**
@@ -130,7 +115,7 @@ private function sendEventIfComplete(Result $result, OnSuccess $onSuccess): void
             return;
         }
 
-        unset($this->completedProcesses[$result->getFile()]);
+        unset($this->completedProcesses[$result->getFile()->getDisplayPath()]);
         $onSuccess($result);
     }
 }
diff --git a/src/Process/Handler/SequentialProcessHandler.php b/src/Process/Handler/SequentialProcessHandler.php
--- a/src/Process/Handler/SequentialProcessHandler.php
+++ b/src/Process/Handler/SequentialProcessHandler.php
@@ -4,15 +4,13 @@
 
 namespace Churn\Process\Handler;
 
-use Churn\Process\ChangesCountInterface;
-use Churn\Process\CyclomaticComplexityInterface;
 use Churn\Process\Observer\OnSuccess;
 use Churn\Process\ProcessFactory;
 use Churn\Process\ProcessInterface;
 use Churn\Result\Result;
 use Generator;
 
-class SequentialProcessHandler implements ProcessHandler
+class SequentialProcessHandler extends BaseProcessHandler
 {
 
     /**
@@ -25,13 +23,9 @@ class SequentialProcessHandler implements ProcessHandler
     public function process(Generator $filesFinder, ProcessFactory $processFactory, OnSuccess $onSuccess): void
     {
         foreach ($filesFinder as $file) {
-            $result = new Result($file->getDisplayPath());
-            $processes = [
-                $processFactory->createChangesCountProcess($file),
-                $processFactory->createCyclomaticComplexityProcess($file),
-            ];
+            $result = new Result($file);
 
-            foreach ($processes as $process) {
+            foreach ($processFactory->createProcesses($file) as $process) {
                 $this->executeProcess($process, $result);
             }
 
@@ -45,20 +39,12 @@ public function process(Generator $filesFinder, ProcessFactory $processFactory,
      * @param ProcessInterface $process The process to execute.
      * @param Result $result The result to complete.
      */
-    private function executeProcess(ProcessInterface $process, Result $result): Result
+    private function executeProcess(ProcessInterface $process, Result $result): void
     {
         $process->start();
 
         while (!$process->isSuccessful());
 
-        if ($process instanceof ChangesCountInterface) {
-            $result->setCommits($process->countChanges());
-        }
-
-        if ($process instanceof CyclomaticComplexityInterface) {
-            $result->setComplexity($process->getCyclomaticComplexity());
-        }
-
-        return $result;
+        $this->saveResult($process, $result);
     }
 }
diff --git a/src/Process/Observer/OnSuccessBuilder.php b/src/Process/Observer/OnSuccessBuilder.php
new file mode 100644
--- /dev/null
+++ b/src/Process/Observer/OnSuccessBuilder.php
@@ -0,0 +1,86 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Churn\Process\Observer;
+
+use Churn\Process\CacheProcessFactory;
+use Churn\Result\ResultAccumulator;
+use Symfony\Component\Console\Helper\ProgressBar;
+
+class OnSuccessBuilder
+{
+
+    /**
+     * @param array<mixed> $objects Objects who need to observe OnSuccess events.
+     */
+    public function build(array $objects): OnSuccess
+    {
+        $observers = [];
+
+        foreach ($objects as $object) {
+            $this->addOnSuccess($object, $observers);
+            $this->addOnSuccessProgress($object, $observers);
+            $this->addOnSuccessAccumulate($object, $observers);
+            $this->addOnSuccessCache($object, $observers);
+        }
+
+        if (1 === \count($observers)) {
+            return $observers[0];
+        }
+
+        return new OnSuccessCollection(...$observers);
+    }
+
+    /**
+     * @param mixed $object Object who needs to observe OnSuccess events.
+     * @param array<OnSuccess> $observers Collection of observers.
+     */
+    private function addOnSuccess($object, array &$observers): void
+    {
+        if (!$object instanceof OnSuccess) {
+            return;
+        }
+
+        $observers[] = $object;
+    }
+
+    /**
+     * @param mixed $object Object who needs to observe OnSuccess events.
+     * @param array<OnSuccess> $observers Collection of observers.
+     */
+    private function addOnSuccessProgress($object, array &$observers): void
+    {
+        if (!$object instanceof ProgressBar) {
+            return;
+        }
+
+        $observers[] = new OnSuccessProgress($object);
+    }
+
+    /**
+     * @param mixed $object Object who needs to observe OnSuccess events.
+     * @param array<OnSuccess> $observers Collection of observers.
+     */
+    private function addOnSuccessAccumulate($object, array &$observers): void
+    {
+        if (!$object instanceof ResultAccumulator) {
+            return;
+        }
+
+        $observers[] = new OnSuccessAccumulate($object);
+    }
+
+    /**
+     * @param mixed $object Object who needs to observe OnSuccess events.
+     * @param array<OnSuccess> $observers Collection of observers.
+     */
+    private function addOnSuccessCache($object, array &$observers): void
+    {
+        if (!$object instanceof CacheProcessFactory) {
+            return;
+        }
+
+        $observers[] = new OnSuccessCache($object);
+    }
+}
diff --git a/src/Process/Observer/OnSuccessCache.php b/src/Process/Observer/OnSuccessCache.php
new file mode 100644
--- /dev/null
+++ b/src/Process/Observer/OnSuccessCache.php
@@ -0,0 +1,37 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Churn\Process\Observer;
+
+use Churn\Process\CacheProcessFactory;
+use Churn\Result\Result;
+
+class OnSuccessCache implements OnSuccess
+{
+
+    /**
+     * @var CacheProcessFactory
+     */
+    private $cacheProcessFactory;
+
+    /**
+     * Class constructor.
+     *
+     * @param CacheProcessFactory $cacheProcessFactory The object writing the cache.
+     */
+    public function __construct(CacheProcessFactory $cacheProcessFactory)
+    {
+        $this->cacheProcessFactory = $cacheProcessFactory;
+    }
+
+    /**
+     * Triggers an event when a file is successfully processed.
+     *
+     * @param Result $result The result for a file.
+     */
+    public function __invoke(Result $result): void
+    {
+        $this->cacheProcessFactory->addToCache($result);
+    }
+}
diff --git a/src/Process/PredefinedProcess.php b/src/Process/PredefinedProcess.php
new file mode 100644
--- /dev/null
+++ b/src/Process/PredefinedProcess.php
@@ -0,0 +1,78 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Churn\Process;
+
+use Churn\File\File;
+
+class PredefinedProcess implements ChangesCountInterface, CyclomaticComplexityInterface
+{
+
+    /**
+     * @var File
+     */
+    private $file;
+
+    /**
+     * @var integer
+     */
+    private $countChanges;
+
+    /**
+     * @var integer
+     */
+    private $cyclomaticComplexity;
+
+    /**
+     * @param File $file The file the process is being executed on.
+     * @param integer $countChanges The number of changes of the file.
+     * @param integer $cyclomaticComplexity The complexity of the file.
+     */
+    public function __construct(File $file, int $countChanges, int $cyclomaticComplexity)
+    {
+        $this->file = $file;
+        $this->countChanges = $countChanges;
+        $this->cyclomaticComplexity = $cyclomaticComplexity;
+    }
+
+    /**
+     * Start the process.
+     */
+    public function start(): void
+    {
+        // nothing to do
+    }
+
+    /**
+     * Determines if the process was successful.
+     */
+    public function isSuccessful(): bool
+    {
+        return true;
+    }
+
+    /**
+     * Gets the file the process is being executed on.
+     */
+    public function getFile(): File
+    {
+        return $this->file;
+    }
+
+    /**
+     * Returns the number of changes for a file.
+     */
+    public function countChanges(): int
+    {
+        return $this->countChanges;
+    }
+
+    /**
+     * Returns the cyclomatic complexity of a file.
+     */
+    public function getCyclomaticComplexity(): int
+    {
+        return $this->cyclomaticComplexity;
+    }
+}
diff --git a/src/Process/ProcessFactory.php b/src/Process/ProcessFactory.php
--- a/src/Process/ProcessFactory.php
+++ b/src/Process/ProcessFactory.php
@@ -5,93 +5,13 @@
 namespace Churn\Process;
 
 use Churn\File\File;
-use Closure;
-use Phar;
-use Symfony\Component\Process\PhpExecutableFinder;
-use Symfony\Component\Process\Process;
 
-class ProcessFactory
+interface ProcessFactory
 {
 
     /**
-     * Builder of objects implementing ChangesCountInterface.
-     *
-     * @var Closure
+     * @param File $file File that the processes will execute on.
+     * @return iterable<ProcessInterface> The list of processes to execute.
      */
-    private $changesCountProcessBuilder;
-
-    /**
-     * Builder of objects implementing CyclomaticComplexityInterface.
-     *
-     * @var Closure
-     */
-    private $cyclomaticComplexityBuilder;
-
-    /**
-     * Class constructor.
-     *
-     * @param string $vcs Name of the version control system.
-     * @param string $commitsSince String containing the date of when to look at commits since.
-     */
-    public function __construct(string $vcs, string $commitsSince)
-    {
-        $this->changesCountProcessBuilder = $this->getChangesCountProcessBuilder($vcs, $commitsSince);
-        $this->cyclomaticComplexityBuilder = $this->getCyclomaticComplexityProcessBuilder();
-    }
-
-    /**
-     * Creates a process that will count the number of changes for $file.
-     *
-     * @param File $file File that the process will execute on.
-     */
-    public function createChangesCountProcess(File $file): ChangesCountInterface
-    {
-        return ($this->changesCountProcessBuilder)($file);
-    }
-
-    /**
-     * Creates a Cyclomatic Complexity Process that will run on $file.
-     *
-     * @param File $file File that the process will execute on.
-     */
-    public function createCyclomaticComplexityProcess(File $file): CyclomaticComplexityInterface
-    {
-        return ($this->cyclomaticComplexityBuilder)($file);
-    }
-
-    /**
-     * @param string $vcs Name of the version control system.
-     * @param string $commitsSince String containing the date of when to look at commits since.
-     * @throws InvalidArgumentException If VCS is not supported.
-     */
-    private function getChangesCountProcessBuilder(string $vcs, string $commitsSince): Closure
-    {
-        return (new ChangesCountProcessBuilder())->getBuilder($vcs, $commitsSince);
-    }
-
-    /**
-     * Returns a cyclomatic complexity builder.
-     */
-    private function getCyclomaticComplexityProcessBuilder(): Closure
-    {
-        $phpExecutable = (string)(new PhpExecutableFinder())->find();
-        $command = \array_merge([$phpExecutable], $this->getAssessorArguments());
-
-        return static function (File $file) use ($command): CyclomaticComplexityInterface {
-            $command[] = $file->getFullPath();
-            $process = new Process($command);
-
-            return new CyclomaticComplexityProcess($file, $process);
-        };
-    }
-
-    /** @return array<string> */
-    private function getAssessorArguments(): array
-    {
-        if (\is_callable([Phar::class, 'running']) && '' !== Phar::running(false)) {
-            return [Phar::running(false), 'assess-complexity'];
-        }
-
-        return [__DIR__ . '/../../bin/CyclomaticComplexityAssessorRunner'];
-    }
+    public function createProcesses(File $file): iterable;
 }
diff --git a/src/Result/Result.php b/src/Result/Result.php
--- a/src/Result/Result.php
+++ b/src/Result/Result.php
@@ -4,6 +4,7 @@
 
 namespace Churn\Result;
 
+use Churn\File\File;
 use Webmozart\Assert\Assert;
 
 class Result
@@ -12,7 +13,7 @@ class Result
     /**
      * The file property.
      *
-     * @var string
+     * @var File
      */
     private $file;
 
@@ -31,11 +32,9 @@ class Result
     private $complexity;
 
     /**
-     * Class constructor.
-     *
-     * @param string $file The path of the processed file.
+     * @param File $file The processed file.
      */
-    public function __construct(string $file)
+    public function __construct(File $file)
     {
         $this->file = $file;
         $this->commits = -1;
@@ -43,9 +42,9 @@ public function __construct(string $file)
     }
 
     /**
-     * Get the file path.
+     * Return the file.
      */
-    public function getFile(): string
+    public function getFile(): File
     {
         return $this->file;
     }
diff --git a/src/Result/ResultAccumulator.php b/src/Result/ResultAccumulator.php
--- a/src/Result/ResultAccumulator.php
+++ b/src/Result/ResultAccumulator.php
@@ -106,7 +106,7 @@ public function toArray(): array
             }
 
             $rows[] = [
-                $result->getFile(),
+                $result->getFile()->getDisplayPath(),
                 $result->getCommits(),
                 $result->getComplexity(),
                 $score,


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
@@ -12,6 +12,9 @@
 
 class RunCommandTest extends BaseTestCase
 {
+
+    private const BAR = '0 [>---------------------------]';
+
     /** @var CommandTester */
     private $commandTester;
 
@@ -47,6 +50,24 @@ public function it_displays_the_logo_at_the_beginning_by_default()
 
         $this->assertEquals(0, $exitCode);
         $this->assertEquals(RunCommand::LOGO, substr($display, 0, strlen(RunCommand::LOGO)));
+        // there is no progress bar by default
+        $this->assertFalse(strpos($display, self::BAR), 'The progress bar shouldn\'t be displayed');
+    }
+
+    /** @test */
+    public function it_can_show_a_progress_bar()
+    {
+        $exitCode = $this->commandTester->execute([
+            'paths' => [__DIR__],
+            '--progress' => null,
+        ]);
+        $display = $this->commandTester->getDisplay();
+
+        $this->assertEquals(0, $exitCode);
+        $this->assertEquals(RunCommand::LOGO, substr($display, 0, strlen(RunCommand::LOGO)));
+        // the progress bar must be right after the logo
+        $display = ltrim(substr($display, strlen(RunCommand::LOGO)));
+        $this->assertEquals(self::BAR, substr($display, 0, strlen(self::BAR)));
     }
 
     /** @test */
@@ -106,4 +127,38 @@ public function it_throws_when_no_directory(): void
         $this->expectException(InvalidArgumentException::class);
         $this->commandTester->execute(['paths' => []]);
     }
+
+    /** @test */
+    public function it_can_use_cache(): void
+    {
+        // delete cache if any
+        $cachePath = $this->tmpFile = __DIR__ . '/config/.churn.cache';
+        if (is_file($cachePath)) {
+            unlink($cachePath);
+        }
+        $this->assertFalse(file_exists($cachePath), "File $cachePath shouldn't exist");
+
+        // generate cache
+        $exitCode = $this->commandTester->execute([
+            'paths' => [],
+            '-c' => __DIR__ . '/config/test-cache.yml',
+        ]);
+        $displayBeforeCache = $this->commandTester->getDisplay();
+
+        $this->assertEquals(0, $exitCode);
+        $this->assertTrue(file_exists($cachePath), "File $cachePath should exist");
+        $this->assertGreaterThan(0, filesize($cachePath), 'Cache file is empty');
+
+        // use cache
+        $exitCode = $this->commandTester->execute([
+            'paths' => [],
+            '-c' => __DIR__ . '/config/test-cache.yml',
+        ]);
+        $displayAfterCache = $this->commandTester->getDisplay();
+
+        $this->assertEquals(0, $exitCode);
+        $this->assertEquals($displayBeforeCache, $displayAfterCache);
+        $this->assertTrue(file_exists($cachePath), "File $cachePath should exist");
+        $this->assertGreaterThan(0, filesize($cachePath), 'Cache file is empty');
+    }
 }
diff --git a/tests/Integration/Command/config/.gitignore b/tests/Integration/Command/config/.gitignore
new file mode 100644
--- /dev/null
+++ b/tests/Integration/Command/config/.gitignore
@@ -0,0 +1,5 @@
+# Ignore everything, mostly files generated by tests
+*
+# except this file and the configuration files
+!.gitignore
+!*.yml
\ No newline at end of file
diff --git a/tests/Integration/Command/config/test-cache.yml b/tests/Integration/Command/config/test-cache.yml
new file mode 100644
--- /dev/null
+++ b/tests/Integration/Command/config/test-cache.yml
@@ -0,0 +1,3 @@
+directoriesToScan:
+  - "../"
+cachePath: .churn.cache
\ No newline at end of file
diff --git a/tests/Unit/Configuration/ConfigTest.php b/tests/Unit/Configuration/ConfigTest.php
--- a/tests/Unit/Configuration/ConfigTest.php
+++ b/tests/Unit/Configuration/ConfigTest.php
@@ -10,14 +10,33 @@
 
 class ConfigTest extends BaseTestCase
 {
-    /** @test */
-    public function it_throws_exception_if_badly_instantiated()
+    /**
+     * @test
+     * @dataProvider provide_invalid_values
+     */
+    public function it_throws_exception_if_badly_instantiated(array $config, string $expectedMessage)
     {
         $this->expectException(InvalidArgumentException::class);
-        $this->assertInstanceOf(
-            Config::class,
-            Config::create(['fileExtensions' => 5])
-        );
+        $this->expectExceptionMessage($expectedMessage);
+        Config::create($config);
+    }
+
+    public function provide_invalid_values(): iterable
+    {
+        yield [
+            ['fileExtensions' => 5],
+            'File extensions should be an array of strings'
+        ];
+
+        yield [
+            ['directoriesToScan' => 'not an array'],
+            'Directories to scan should be an array of strings'
+        ];
+
+        yield [
+            ['directoriesToScan' => ['/tmp', 42]],
+            'Directories to scan should be an array of strings'
+        ];
     }
 
     /** @test */
@@ -38,6 +57,7 @@ public function it_can_return_its_default_values_when_instantiated_without_any_p
         $this->assertSame([], $config->getFilesToIgnore());
         $this->assertSame(['php'], $config->getFileExtensions());
         $this->assertSame('git', $config->getVCS());
+        $this->assertSame(null, $config->getCachePath());
     }
 
     /** @test */
@@ -51,6 +71,7 @@ public function it_can_return_its_values_when_instantiated_parameters()
         $filesToIgnore = ['foo.php', 'bar.php', 'baz.php'];
         $fileExtensions = ['php', 'inc'];
         $vcs = 'none';
+        $cachePath = '/tmp/.churn.cache';
 
         $config = Config::create([
             'directoriesToScan' => $directoriesToScan,
@@ -61,6 +82,7 @@ public function it_can_return_its_values_when_instantiated_parameters()
             'filesToIgnore' => $filesToIgnore,
             'fileExtensions' => $fileExtensions,
             'vcs' => $vcs,
+            'cachePath' => $cachePath,
         ]);
 
         $this->assertSame($directoriesToScan, $config->getDirectoriesToScan());
@@ -71,6 +93,7 @@ public function it_can_return_its_values_when_instantiated_parameters()
         $this->assertSame($filesToIgnore, $config->getFilesToIgnore());
         $this->assertSame($fileExtensions, $config->getFileExtensions());
         $this->assertSame($vcs, $config->getVCS());
+        $this->assertSame($cachePath, $config->getCachePath());
     }
 
     /** @test */
diff --git a/tests/Unit/File/FileHelperTest.php b/tests/Unit/File/FileHelperTest.php
--- a/tests/Unit/File/FileHelperTest.php
+++ b/tests/Unit/File/FileHelperTest.php
@@ -27,6 +27,5 @@ public function provide_paths(): iterable
         yield ['d:\\foo', '/path', 'd:\\foo'];
         yield ['E:/foo', '/path', 'E:/foo'];
         yield ['f:/foo', '/path', 'f:/foo'];
-        yield ['://foo', '/path', '://foo'];
     }
 }
diff --git a/tests/Unit/Process/CacheProcessFactoryTest.php b/tests/Unit/Process/CacheProcessFactoryTest.php
new file mode 100644
--- /dev/null
+++ b/tests/Unit/Process/CacheProcessFactoryTest.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Churn\Tests\Unit\Process;
+
+use Churn\Process\CacheProcessFactory;
+use Churn\Process\ProcessFactory;
+use Churn\Tests\BaseTestCase;
+use InvalidArgumentException;
+use Mockery as m;
+
+class CacheProcessFactoryTest extends BaseTestCase
+{
+    /**
+     * @test
+     * @dataProvider provide_invalid_paths
+     */
+    public function it_throws_for_invalid_cache_path(string $cachePath): void
+    {
+        $factory = m::mock(ProcessFactory::class);
+        $this->expectException(InvalidArgumentException::class);
+        new CacheProcessFactory($cachePath, $factory);
+    }
+
+    public function provide_invalid_paths(): iterable
+    {
+        yield [''];
+        yield [__DIR__];
+    }
+}
diff --git a/tests/Unit/Process/ConcreteProcessFactoryTest.php b/tests/Unit/Process/ConcreteProcessFactoryTest.php
new file mode 100644
--- /dev/null
+++ b/tests/Unit/Process/ConcreteProcessFactoryTest.php
@@ -0,0 +1,91 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Churn\Tests\Unit\Process;
+
+use Churn\Configuration\Config;
+use Churn\File\File;
+use Churn\Process\ChangesCountInterface;
+use Churn\Process\CyclomaticComplexityInterface;
+use Churn\Process\ConcreteProcessFactory;
+use Churn\Tests\BaseTestCase;
+use InvalidArgumentException;
+
+class ConcreteProcessFactoryTest extends BaseTestCase
+{
+    /**
+     * @var ConcreteProcessFactory
+     */
+    private $processFactory;
+
+    public function setup()
+    {
+        $config = Config::createFromDefaultValues();
+        $this->processFactory = new ConcreteProcessFactory($config->getVCS(), $config->getCommitsSince());
+    }
+
+    /** @test */
+    public function it_can_be_created()
+    {
+        $this->assertInstanceOf(ConcreteProcessFactory::class, $this->processFactory);
+    }
+
+    private function extractChangesCountProcess(iterable $processes): ?ChangesCountInterface
+    {
+        foreach ($processes as $process) {
+            if ($process instanceof ChangesCountInterface) {
+                return $process;
+            }
+        }
+
+        return null;
+    }
+
+    private function extractCyclomaticComplexityProcess(iterable $processes): ?CyclomaticComplexityInterface
+    {
+        foreach ($processes as $process) {
+            if ($process instanceof CyclomaticComplexityInterface) {
+                return $process;
+            }
+        }
+
+        return null;
+    }
+
+    /** @test */
+    public function it_can_create_a_git_commit_count_process()
+    {
+        $file = new File('foo/bar/baz.php', 'bar/baz.php');
+        $process = $this->extractChangesCountProcess($this->processFactory->createProcesses($file));
+        $this->assertInstanceOf(ChangesCountInterface::class, $process);
+        $this->assertSame($file, $process->getFile());
+    }
+
+    /** @test */
+    public function it_can_create_a_cyclomatic_complexity_process()
+    {
+        $file = new File('foo/bar/baz.php', 'bar/baz.php');
+        $process = $this->extractCyclomaticComplexityProcess($this->processFactory->createProcesses($file));
+        $this->assertInstanceOf(CyclomaticComplexityInterface::class, $process);
+        $this->assertSame($file, $process->getFile());
+    }
+
+    /** @test */
+    public function it_throws_exception_if_VCS_is_not_supported()
+    {
+        $config = Config::createFromDefaultValues();
+        $this->expectException(InvalidArgumentException::class);
+        $this->processFactory = new ConcreteProcessFactory('not a valid VCS', $config->getCommitsSince());
+    }
+
+    /** @test */
+    public function it_always_counts_one_when_there_is_no_VCS()
+    {
+        $file = new File('foo/bar/baz.php', 'bar/baz.php');
+        $this->processFactory = new ConcreteProcessFactory('none', '');
+        $process = $this->extractChangesCountProcess($this->processFactory->createProcesses($file));
+        $this->assertSame($file, $process->getFile());
+        $this->assertEquals(1, $process->countChanges());
+    }
+}
diff --git a/tests/Unit/Process/Handler/ParallelProcessHandlerTest.php b/tests/Unit/Process/Handler/ParallelProcessHandlerTest.php
--- a/tests/Unit/Process/Handler/ParallelProcessHandlerTest.php
+++ b/tests/Unit/Process/Handler/ParallelProcessHandlerTest.php
@@ -10,6 +10,7 @@
 use Churn\Process\CyclomaticComplexityProcess;
 use Churn\Process\Handler\ParallelProcessHandler;
 use Churn\Process\Observer\OnSuccess;
+use Churn\Process\ConcreteProcessFactory;
 use Churn\Process\ProcessFactory;
 use Churn\Tests\BaseTestCase;
 use Generator;
@@ -28,7 +29,7 @@ public function it_doesnt_call_the_observer_when_no_file()
     {
         $processHandler = new ParallelProcessHandler(3);
         $config = Config::createFromDefaultValues();
-        $processFactory = new ProcessFactory($config->getVCS(), $config->getCommitsSince());
+        $processFactory = new ConcreteProcessFactory($config->getVCS(), $config->getCommitsSince());
 
         $observer = m::mock(OnSuccess::class);
         $observer->shouldReceive('__invoke')->never();
@@ -54,8 +55,7 @@ public function it_calls_the_observer_for_one_file()
         $process2->shouldReceive('getCyclomaticComplexity')->andReturn(2);
 
         $processFactory = m::mock(ProcessFactory::class);
-        $processFactory->shouldReceive('createChangesCountProcess')->andReturn($process1);
-        $processFactory->shouldReceive('createCyclomaticComplexityProcess')->andReturn($process2);
+        $processFactory->shouldReceive('createProcesses')->andReturn([$process1, $process2]);
 
         $observer = m::mock(OnSuccess::class);
         $observer->shouldReceive('__invoke')->once();
diff --git a/tests/Unit/Process/Handler/SequentialProcessHandlerTest.php b/tests/Unit/Process/Handler/SequentialProcessHandlerTest.php
--- a/tests/Unit/Process/Handler/SequentialProcessHandlerTest.php
+++ b/tests/Unit/Process/Handler/SequentialProcessHandlerTest.php
@@ -36,8 +36,7 @@ public function it_calls_the_observer_for_one_file()
         $process2->shouldReceive('getCyclomaticComplexity')->andReturn(2);
 
         $processFactory = m::mock(ProcessFactory::class);
-        $processFactory->shouldReceive('createChangesCountProcess')->andReturn($process1);
-        $processFactory->shouldReceive('createCyclomaticComplexityProcess')->andReturn($process2);
+        $processFactory->shouldReceive('createProcesses')->andReturn([$process1, $process2]);
 
         $observer = m::mock(OnSuccess::class);
         $observer->shouldReceive('__invoke')->once();
diff --git a/tests/Unit/Process/ProcessFactoryTest.php b/tests/Unit/Process/ProcessFactoryTest.php
deleted file mode 100644
--- a/tests/Unit/Process/ProcessFactoryTest.php
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace Churn\Tests\Unit\Process;
-
-use Churn\Configuration\Config;
-use Churn\File\File;
-use Churn\Process\ChangesCountInterface;
-use Churn\Process\CyclomaticComplexityInterface;
-use Churn\Process\ProcessFactory;
-use Churn\Tests\BaseTestCase;
-use InvalidArgumentException;
-
-class ProcessFactoryTest extends BaseTestCase
-{
-    /**
-     * @var ProcessFactory
-     */
-    private $processFactory;
-
-    /** @test */
-    public function it_can_be_created()
-    {
-        $this->assertInstanceOf(ProcessFactory::class, $this->processFactory);
-    }
-
-    /** @test */
-    public function it_can_create_a_git_commit_count_process()
-    {
-        $file = new File('foo/bar/baz.php', 'bar/baz.php');
-        $result = $this->processFactory->createChangesCountProcess($file);
-        $this->assertInstanceOf(ChangesCountInterface::class, $result);
-        $this->assertSame($file, $result->getFile());
-    }
-
-    /** @test */
-    public function it_can_create_a_cyclomatic_complexity_process()
-    {
-        $file = new File('foo/bar/baz.php', 'bar/baz.php');
-        $result = $this->processFactory->createCyclomaticComplexityProcess($file);
-        $this->assertInstanceOf(CyclomaticComplexityInterface::class, $result);
-        $this->assertSame($file, $result->getFile());
-    }
-
-    /** @test */
-    public function it_throws_exception_if_VCS_is_not_supported()
-    {
-        $config = Config::createFromDefaultValues();
-        $this->expectException(InvalidArgumentException::class);
-        $this->processFactory = new ProcessFactory('not a valid VCS', $config->getCommitsSince());
-    }
-
-    /** @test */
-    public function it_always_counts_one_when_there_is_no_VCS()
-    {
-        $file = new File('foo/bar/baz.php', 'bar/baz.php');
-        $this->processFactory = new ProcessFactory('none', '');
-        $result = $this->processFactory->createChangesCountProcess($file);
-        $this->assertSame($file, $result->getFile());
-        $this->assertEquals(1, $result->countChanges());
-    }
-
-    public function setup()
-    {
-        $config = Config::createFromDefaultValues();
-        $this->processFactory = new ProcessFactory($config->getVCS(), $config->getCommitsSince());
-    }
-}
diff --git a/tests/Unit/Process/ProcessHandlerFactoryTest.php b/tests/Unit/Process/ProcessHandlerFactoryTest.php
--- a/tests/Unit/Process/ProcessHandlerFactoryTest.php
+++ b/tests/Unit/Process/ProcessHandlerFactoryTest.php
@@ -21,7 +21,7 @@ public function it_can_be_instantiated()
 
     /**
      * @test
-     * @dataProvider provideConfigWithCorrespondingProcessHandler
+     * @dataProvider provide_config_with_process_handler
      */
     public function it_returns_the_right_process_handler(Config $config, string $expectedClassName)
     {
@@ -30,7 +30,7 @@ public function it_returns_the_right_process_handler(Config $config, string $exp
         $this->assertEquals($expectedClassName, get_class($processHandler));
     }
 
-    public function provideConfigWithCorrespondingProcessHandler(): iterable
+    public function provide_config_with_process_handler(): iterable
     {
         $config = m::mock(Config::class);
         $config->shouldReceive('getParallelJobs')->andReturn(0);
diff --git a/tests/Unit/Result/ResultAccumulatorTest.php b/tests/Unit/Result/ResultAccumulatorTest.php
--- a/tests/Unit/Result/ResultAccumulatorTest.php
+++ b/tests/Unit/Result/ResultAccumulatorTest.php
@@ -4,6 +4,7 @@
 
 namespace Churn\Tests\Result;
 
+use Churn\File\File;
 use Churn\Result\Result;
 use Churn\Result\ResultAccumulator;
 use Churn\Tests\BaseTestCase;
@@ -17,7 +18,7 @@ private function mockResult(int $commits, int $complexity, string $file, float $
         $result->shouldReceive('getCommits')->andReturn($commits);
         $result->shouldReceive('getComplexity')->andReturn($complexity);
         $result->shouldReceive('getPriority')->andReturn($commits * $complexity);
-        $result->shouldReceive('getFile')->andReturn($file);
+        $result->shouldReceive('getFile')->andReturn(new File("/$file", $file));
         $result->shouldReceive('getScore')->andReturn($score);
 
         return $result;
diff --git a/tests/Unit/Result/ResultTest.php b/tests/Unit/Result/ResultTest.php
--- a/tests/Unit/Result/ResultTest.php
+++ b/tests/Unit/Result/ResultTest.php
@@ -4,6 +4,7 @@
 
 namespace Churn\Tests\Result;
 
+use Churn\File\File;
 use Churn\Result\Result;
 use Churn\Tests\BaseTestCase;
 
@@ -15,6 +16,13 @@ class ResultTest extends BaseTestCase
      */
     protected $result;
 
+    public function setup()
+    {
+        $this->result = new Result(new File('/filename.php', 'filename.php'));
+        $this->result->setCommits(5);
+        $this->result->setComplexity(7);
+    }
+
     /** @test */
     public function it_can_be_created()
     {
@@ -24,7 +32,7 @@ public function it_can_be_created()
     /** @test */
     public function it_can_return_the_file()
     {
-        $this->assertSame('filename.php', $this->result->getFile());
+        $this->assertSame('filename.php', $this->result->getFile()->getDisplayPath());
     }
 
     /** @test */
@@ -53,11 +61,4 @@ public function it_can_calculate_the_score()
 
         $this->assertEquals(0.417, $this->result->getScore($maxCommits, $maxComplexity));
     }
-
-    public function setup()
-    {
-        $this->result = new Result('filename.php');
-        $this->result->setCommits(5);
-        $this->result->setComplexity(7);
-    }
 }