Repository: bmitch/churn-php (php)

Issue:
Run in parallel
With a large number of files the results take a while to come back. Need some way to run the files in parallel.

* http://symfony.com/doc/current/components/process.html
* http://blog.servergrove.com/2014/04/16/symfony2-components-overview-process/

If we used something like the above then we'd need to probably create another command that simple takes the file path and returns the results.


Discussion:
* Gather all the files needed for processing. Could be a collection of files.
* While there are files to run:
   * Launch a process to get the commit count.
      * Store results in a class that contains filename and output.
      * This class can go into a collection of CommitCountProcessResults.
   * Launch a process to get the cyclomatic complexity.
      * Store results in a class that contains filename and output.
      * This class can go into a collection of CyclomaticComplexityProcessResults.
* Take the two collections and create the results.

Seems like this can be really slowed down if the repo has lots of commits. Perhaps we just limit it to the last X commits?

Patch:
diff --git a/.gitignore b/.gitignore
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,5 @@ build
 composer.lock
 docs
 vendor
-coverage.xml
\ No newline at end of file
+coverage.xml
+.idea
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
--- a/.travis.yml
+++ b/.travis.yml
@@ -10,6 +10,7 @@ before_script:
   - travis_retry composer install --no-interaction --prefer-source --dev
 
 script:
+ - vendor/bin/parallel-lint src tests
  - vendor/bin/phpcs --config-set ignore_warnings_on_exit 1
  - vendor/bin/phpcs --standard=psr2 src
  - if [[ ${TRAVIS_PHP_VERSION:0:3} == "7.0" ]]; then vendor/bin/phpcs --standard=phpcs.xml src --ignore=tests/Sniffs; fi
@@ -18,6 +19,7 @@ script:
  - vendor/bin/phpmd src text codesize,unusedcode,naming
  - vendor/bin/phploc src
  - vendor/bin/phpcpd src
+ - php churn run src
 
 after_success:
  - bash <(curl -s https://codecov.io/bash)
\ No newline at end of file
diff --git a/CyclomaticComplexityAssessorRunner b/CyclomaticComplexityAssessorRunner
new file mode 100644
--- /dev/null
+++ b/CyclomaticComplexityAssessorRunner
@@ -0,0 +1,9 @@
+#!/usr/bin/env php
+<?php
+require_once 'vendor/autoload.php';
+
+use Churn\Assessors\CyclomaticComplexity\CyclomaticComplexityAssessor;
+
+$file = $argv[1];
+$assessor = new CyclomaticComplexityAssessor();
+echo $assessor->assess($file);
\ No newline at end of file
diff --git a/churn b/churn
old mode 100644
new mode 100755
diff --git a/composer.json b/composer.json
--- a/composer.json
+++ b/composer.json
@@ -20,7 +20,8 @@
     "require": {
         "php": ">=7.0.0",
         "symfony/console": "^3.3",
-        "tightenco/collect": "^5.4"
+        "tightenco/collect": "^5.4",
+        "symfony/process": "^3.3"
     },
     "require-dev": {
         "phpunit/phpunit": "^5.7",
@@ -51,11 +52,16 @@
             "vendor/bin/phpcs --standard=psr2 src -spn",
             "vendor/bin/phpcs --standard=phpcs.xml src -spn",
             "vendor/bin/phpcs --standard=codor.xml src -spn",
-            "vendor/bin/phpunit --debug --coverage-clover=coverage.xml",
             "vendor/bin/phpmd src text codesize,unusedcode,naming",
             "vendor/bin/phploc src",
             "vendor/bin/phpcpd src",
-            "vendor/bin/phpunit"
+            "vendor/bin/phpunit --debug --coverage-clover=coverage.xml",
+            "churn run src"
+        ],
+        "fix": [
+            "vendor/bin/phpcbf -n --standard=phpcs.xml src/",
+            "vendor/bin/phpcbf --standard=codor.xml src -spn",
+            "vendor/bin/phpcbf --standard=psr2 src -sp"
         ]
     }
 }
diff --git a/src/Assessors/GitCommitCount/GitCommitCountAssessor.php b/src/Assessors/GitCommitCount/GitCommitCountAssessor.php
deleted file mode 100644
--- a/src/Assessors/GitCommitCount/GitCommitCountAssessor.php
+++ /dev/null
@@ -1,59 +0,0 @@
-<?php declare(strict_types = 1);
-
-namespace Churn\Assessors\GitCommitCount;
-
-use Churn\Services\CommandService;
-
-class GitCommitCountAssessor
-{
-    /**
-     * The command service.
-     * @var CommandService
-     */
-    protected $commandService;
-
-    /**
-     * Class constructor.
-     * @param CommandService $commandService Command Service.
-     */
-    public function __construct(CommandService $commandService)
-    {
-        $this->commandService = $commandService;
-    }
-
-    /**
-     * See how many commits the file at $filePath has.
-     * @param  string $filePath Path and filename.
-     * @return integer
-     */
-    public function assess(string $filePath): int
-    {
-        $command = $this->buildCommand($filePath);
-
-        $result = $this->commandService->execute($command);
-        if (! isset($result[0])) {
-            return 0;
-        }
-        $result = trim($result[0]);
-        $explodedResult = explode(' ', $result);
-        return (integer) $explodedResult[0];
-    }
-
-    /**
-     * Build the command to get the number of commits for the file at $filePath.
-     * @param  string $filePath Patha nd filename.
-     * @return string
-     */
-    protected function buildCommand(string $filePath): string
-    {
-        $commandTemplate = "git log --name-only --pretty=format: %s | sort | uniq -c | sort -nr";
-
-        $pos =strrpos($filePath, '/');
-        if ($pos) {
-            $folder = substr($filePath, 0, $pos);
-            $commandTemplate = "cd {$folder} && git log --name-only --pretty=format: %s | sort | uniq -c | sort -nr";
-        }
-
-        return sprintf($commandTemplate, $filePath, $filePath);
-    }
-}
diff --git a/src/Commands/ChurnCommand.php b/src/Commands/ChurnCommand.php
--- a/src/Commands/ChurnCommand.php
+++ b/src/Commands/ChurnCommand.php
@@ -2,30 +2,58 @@
 
 namespace Churn\Commands;
 
-use Churn\Services\CommandService;
-use Churn\Assessors\GitCommitCount\GitCommitCountAssessor;
-use Churn\Assessors\CyclomaticComplexity\CyclomaticComplexityAssessor;
+use Churn\Processes\CyclomaticComplexityProcess;
+use Churn\Processes\GitCommitCountProcess;
 use Symfony\Component\Console\Command\Command;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Helper\Table;
-use Churn\Results\ResultsGenerator;
 use Churn\Managers\FileManager;
 use Churn\Results\ResultCollection;
+use Illuminate\Support\Collection;
+use Churn\Results\ResultsParser;
 
 class ChurnCommand extends Command
 {
+    /**
+     * The file manager.
+     * @var FileManager
+     */
+    private $fileManager;
+
+    /**
+     * Th results parser.
+     * @var ResultsParser
+     */
+    private $resultsParser;
+
+    /**
+     * Collection of files to run the processes on.
+     * @var Collection
+     */
+    private $files;
+
+    /**
+     * Collection of processes currently running.
+     * @var Collection
+     */
+    private $runningProcesses;
+
+    /**
+     * Array of completed processes.
+     * @var array
+     */
+    private $completedProcessesArray;
+
     /**
      * Class constructor.
      */
     public function __construct()
     {
         parent::__construct();
-        $commitCountAssessor    = new GitCommitCountAssessor(new CommandService);
-        $complexityAssessor     = new CyclomaticComplexityAssessor();
-        $this->resultsGenerator = new ResultsGenerator($commitCountAssessor, $complexityAssessor);
-        $this->fileManager      = new FileManager;
+        $this->fileManager = new FileManager;
+        $this->resultsParser = new ResultsParser;
     }
 
     /**
@@ -48,12 +76,45 @@ protected function configure()
      */
     protected function execute(InputInterface $input, OutputInterface $output)
     {
-        $path     = $input->getArgument('path');
-        $phpFiles = $this->fileManager->getPhpFiles($path);
-        $results  = $this->resultsGenerator->getResults($phpFiles);
+        $path = $input->getArgument('path');
+        $this->files = $this->fileManager->getPhpFiles($path);
+
+        $this->runningProcesses = new Collection;
+        $this->completedProcessesArray = [];
+        while ($this->files->count() || $this->runningProcesses->count()) {
+            $this->getProcessResults();
+        }
+        $completedProcesses = new Collection($this->completedProcessesArray);
+
+        $results = $this->resultsParser->parse($completedProcesses);
         $this->displayResults($output, $results);
     }
 
+    /**
+     * Gets the output from processes and stores them in the completedProcessArray member.
+     * @return void
+     */
+    private function getProcessResults()
+    {
+        for ($index = $this->runningProcesses->count(); $this->files->count() > 0 && $index < 10; $index++) {
+            $file = $this->files->shift();
+
+            $process = new GitCommitCountProcess($file);
+            $process->start();
+            $this->runningProcesses->put($process->getKey(), $process);
+            $process = new CyclomaticComplexityProcess($file);
+            $process->start();
+            $this->runningProcesses->put($process->getKey(), $process);
+        }
+
+        foreach ($this->runningProcesses as $file => $process) {
+            if ($process->isSuccessful()) {
+                $this->runningProcesses->forget($process->getKey());
+                $this->completedProcessesArray[$process->getFileName()][$process->getType()] = $process;
+            }
+        }
+    }
+
     /**
      * Displays the results in a table.
      * @param  OutputInterface                $output  Output.
@@ -62,9 +123,17 @@ protected function execute(InputInterface $input, OutputInterface $output)
      */
     protected function displayResults(OutputInterface $output, ResultCollection $results)
     {
+        echo "\n
+  ___  _   _  __  __  ____  _  _     ____  _   _  ____ 
+ / __)( )_( )(  )(  )(  _ \( \( )___(  _ \( )_( )(  _ \
+( (__  ) _ (  )(__)(  )   / )  ((___))___/ ) _ (  )___/
+ \___)(_) (_)(______)(_)\_)(_)\_)   (__)  (_) (_)(__)      https://github.com/bmitch/churn-php
+ 
+";
         $table = new Table($output);
         $table->setHeaders(['File', 'Times Changed', 'Complexity', 'Score']);
-        foreach ($results->orderByScoreDesc() as $result) {
+
+        foreach ($results->orderByScoreDesc()->take(10) as $result) {
             $table->addRow($result->toArray());
         }
         $table->render();
diff --git a/src/Managers/FileManager.php b/src/Managers/FileManager.php
--- a/src/Managers/FileManager.php
+++ b/src/Managers/FileManager.php
@@ -2,8 +2,10 @@
 
 namespace Churn\Managers;
 
+use Illuminate\Support\Collection;
 use RecursiveDirectoryIterator;
 use RecursiveIteratorIterator;
+use Churn\Values\File;
 
 class FileManager
 {
@@ -11,19 +13,20 @@ class FileManager
      * Recursively finds all files with the .php extension in the provided
      * $path and returns list as array.
      * @param  string $path Path to look for .php files.
-     * @return array
+     * @return Collection
      */
-    public function getPhpFiles(string $path): array
+    public function getPhpFiles(string $path): Collection
     {
         $directoryIterator = new RecursiveDirectoryIterator($path);
-        $files = [];
+        $files = new Collection;
         foreach (new RecursiveIteratorIterator($directoryIterator) as $file) {
             if ($file->getExtension() !== 'php') {
                 continue;
             }
 
-            $files[] = ['fullPath' => $file->getRealPath(), 'displayPath' => $file->getPathName()];
+            $files->push(new File(['displayPath' => $file->getPathName(), 'fullPath' => $file->getRealPath()]));
         }
+
         return $files;
     }
 }
diff --git a/src/Processes/CyclomaticComplexityProcess.php b/src/Processes/CyclomaticComplexityProcess.php
new file mode 100644
--- /dev/null
+++ b/src/Processes/CyclomaticComplexityProcess.php
@@ -0,0 +1,92 @@
+<?php declare(strict_types = 1);
+
+
+namespace Churn\Processes;
+
+use Churn\Values\File;
+use Symfony\Component\Process\Process;
+
+class CyclomaticComplexityProcess
+{
+    /**
+     * The Symfony Process Component.
+     * @var Process
+     */
+    private $process;
+
+    /**
+     * GitCommitCountProcess constructor.
+     * @param File $file The file the process is being executed on.
+     */
+    public function __construct(File $file)
+    {
+        $this->file = $file;
+    }
+
+    /**
+     * Start the process.
+     * @return void
+     */
+    public function start()
+    {
+        $command = $this->getCommandString();
+        $this->process = new Process($command);
+        $this->process->start();
+    }
+
+    /**
+     * Determines if the process was successful.
+     * @return boolean
+     */
+    public function isSuccessful(): bool
+    {
+        return $this->process->isSuccessful();
+    }
+
+    /**
+     * Gets the output of the process.
+     * @return string
+     */
+    public function getOutput(): string
+    {
+        return $this->process->getOutput();
+    }
+
+    /**
+     * Gets the file name of the file the process
+     * is being executed on.
+     * @return string
+     */
+    public function getFilename(): string
+    {
+        return $this->file->getDisplayPath();
+    }
+
+    /**
+     * Gets a unique key used for storing the process in data structures.
+     * @return string
+     */
+    public function getKey(): string
+    {
+        return 'CyclomaticComplexityProcess' . $this->file->getFullPath();
+    }
+
+    /**
+     * Get the type of this process.
+     * @return string
+     */
+    public function getType(): string
+    {
+        return 'CyclomaticComplexityProcess';
+    }
+
+    /**
+     * The process command.
+     * @return string
+     */
+    private function getCommandString(): string
+    {
+        $rootFolder = __DIR__ . '/../../';
+        return "php {$rootFolder}CyclomaticComplexityAssessorRunner {$this->file->getFullPath()}";
+    }
+}
diff --git a/src/Processes/GitCommitCountProcess.php b/src/Processes/GitCommitCountProcess.php
new file mode 100644
--- /dev/null
+++ b/src/Processes/GitCommitCountProcess.php
@@ -0,0 +1,91 @@
+<?php declare(strict_types = 1);
+
+
+namespace Churn\Processes;
+
+use Churn\Values\File;
+use Symfony\Component\Process\Process;
+
+class GitCommitCountProcess
+{
+    /**
+     * The Symfony Process Component.
+     * @var Process
+     */
+    private $process;
+
+    /**
+     * GitCommitCountProcess constructor.
+     * @param File $file The file the process is being executed on.
+     */
+    public function __construct(File $file)
+    {
+        $this->file = $file;
+    }
+
+    /**
+     * Start the process.
+     * @return void
+     */
+    public function start()
+    {
+        $command = $this->getCommandString();
+        $this->process = new Process($command);
+        $this->process->start();
+    }
+
+    /**
+     * Determines if the process was successful.
+     * @return boolean
+     */
+    public function isSuccessful(): bool
+    {
+        return $this->process->isSuccessful();
+    }
+
+    /**
+     * Gets the output of the process.
+     * @return string
+     */
+    public function getOutput(): string
+    {
+        return $this->process->getOutput();
+    }
+
+    /**
+     * Gets the file name of the file the process
+     * is being executed on.
+     * @return string
+     */
+    public function getFilename(): string
+    {
+        return $this->file->getDisplayPath();
+    }
+
+    /**
+     * Gets a unique key used for storing the process in data structures.
+     * @return string
+     */
+    public function getKey(): string
+    {
+        return 'GitCommitCountProcess' . $this->file->getFullPath();
+    }
+
+    /**
+     * Get the type of this process.
+     * @return string
+     */
+    public function getType(): string
+    {
+        return 'GitCommitCountProcess';
+    }
+
+    /**
+     * The process command.
+     * @return string
+     */
+    private function getCommandString(): string
+    {
+        return 'git -C ' . getcwd() . " log --name-only --pretty=format: " . $this->file->getFullPath(). " | sort | uniq -c | sort -nr";
+    }
+}
diff --git a/src/Results/ResultsGenerator.php b/src/Results/ResultsGenerator.php
deleted file mode 100644
--- a/src/Results/ResultsGenerator.php
+++ /dev/null
@@ -1,64 +0,0 @@
-<?php declare(strict_types = 1);
-
-namespace Churn\Results;
-
-use Churn\Assessors\GitCommitCount\GitCommitCountAssessor;
-use Churn\Assessors\CyclomaticComplexity\CyclomaticComplexityAssessor;
-
-class ResultsGenerator
-{
-
-    /**
-     * The commit count assesor.
-     * @var GitCommitCountAssessor
-     */
-    protected $commitCountAssessor;
-
-    /**
-     * The complexity assesor.
-     * @var CyclomaticComplexityAssessor
-     */
-    protected $complexityAssessor;
-   
-    /**
-     * Class constructor.
-     * @param GitCommitCountAssessor       $commitCountAssessor Git Commit Count Assessor.
-     * @param CyclomaticComplexityAssessor $complexityAssessor  Cyclomatic Complexity Assessor.
-     */
-    public function __construct(GitCommitCountAssessor $commitCountAssessor, CyclomaticComplexityAssessor $complexityAssessor)
-    {
-        $this->commitCountAssessor = $commitCountAssessor;
-        $this->complexityAssessor = $complexityAssessor;
-    }
-
-    /**
-     * Generates a ResultCollection for the provided $fils.
-     * @param  array $files List of files.
-     * @return ResultCollection
-     */
-    public function getResults(array $files): ResultCollection
-    {
-        $results = new ResultCollection;
-        foreach ($files as $file) {
-            $results->push($this->getResultsForFile($file));
-        }
-        return $results;
-    }
-
-    /**
-     * Returns a Result object for the provided $file.
-     * @param  array $file FileData.
-     * @return Result
-     */
-    protected function getResultsForFile(array $file): Result
-    {
-        $commits    = $this->commitCountAssessor->assess($file['fullPath']);
-        $complexity = $this->complexityAssessor->assess($file['fullPath']);
-
-        return new Result([
-            'file'       => $file['displayPath'],
-            'commits'    => $commits,
-            'complexity' => $complexity,
-        ]);
-    }
-}
diff --git a/src/Results/ResultsParser.php b/src/Results/ResultsParser.php
new file mode 100644
--- /dev/null
+++ b/src/Results/ResultsParser.php
@@ -0,0 +1,65 @@
+<?php declare(strict_types = 1);
+
+
+namespace Churn\Results;
+
+use Churn\Processes\GitCommitCountProcess;
+use Illuminate\Support\Collection;
+
+class ResultsParser
+{
+    /**
+     * Collection of results.
+     * @var ResultCollection
+     */
+    private $resultsCollection;
+
+    /**
+     * Turns a collection of completed processes into a
+     * collection of parsed result objects.
+     * @param Collection $completedProcesses Collection of completed processes.
+     * @return ResultCollection
+     */
+    public function parse(Collection $completedProcesses): ResultCollection
+    {
+        $this->resultsCollection = new ResultCollection;
+
+        foreach ($completedProcesses as $file => $processes) {
+            $this->parseCompletedProcessesForFile($file, $processes);
+        }
+
+        return $this->resultsCollection;
+    }
+
+    /**
+     * Parse the list of processes for a file.
+     * @param string $file      The file the processes were executed on.
+     * @param array  $processes The proceses that were executed on the file.
+     * @return void
+     */
+    private function parseCompletedProcessesForFile(string $file, array $processes)
+    {
+        $commits = (integer) $this->parseCommits($processes['GitCommitCountProcess']);
+        $complexity = (integer) $processes['CyclomaticComplexityProcess']->getOutput();
+
+        $result = new Result([
+            'file' => $file,
+            'commits' => $commits,
+            'complexity' => $complexity,
+        ]);
+
+        $this->resultsCollection->push($result);
+    }
+
+    /**
+     * Parse the number of commits on the file from the raw process output.
+     * @param GitCommitCountProcess $process Git Commit Count Process.
+     * @return integer
+     */
+    private function parseCommits(GitCommitCountProcess $process): int
+    {
+        $output = $process->getOutput();
+        preg_match("/([0-9]{1,})/", $output, $matches);
+        return (integer) $matches[1] ?? 0;
+    }
+}
diff --git a/src/Services/CommandService.php b/src/Services/CommandService.php
deleted file mode 100644
--- a/src/Services/CommandService.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php declare(strict_types = 1);
-
-namespace Churn\Services;
-
-class CommandService
-{
-    /**
-     * Run a command line command and return the result.
-     * @param  string $command Command to execute.
-     * @return array
-     */
-    public function execute(string $command): array
-    {
-        exec($command, $output);
-        return $output;
-    }
-}
diff --git a/src/Values/File.php b/src/Values/File.php
new file mode 100644
--- /dev/null
+++ b/src/Values/File.php
@@ -0,0 +1,46 @@
+<?php declare(strict_types = 1);
+
+namespace Churn\Values;
+
+class File
+{
+    /**
+     * The full path of the file.
+     * @var string
+     */
+    private $fullPath;
+
+    /**
+     * The display path of the file.
+     * @var string
+     */
+    private $displayPath;
+
+    /**
+     * File constructor.
+     * @param array $fileData Raw file data.
+     */
+    public function __construct(array $fileData)
+    {
+        $this->fullPath = $fileData['fullPath'];
+        $this->displayPath = $fileData['displayPath'];
+    }
+
+    /**
+     * Get the full path of the file.
+     * @return string
+     */
+    public function getFullPath(): string
+    {
+        return $this->fullPath;
+    }
+
+    /**
+     * Get the display path of the file.
+     * @return string
+     */
+    public function getDisplayPath(): string
+    {
+        return $this->displayPath;
+    }
+}


Test patch:
diff --git a/tests/Integration/Managers/FileManagerTest.php b/tests/Integration/Managers/FileManagerTest.php
new file mode 100644
--- /dev/null
+++ b/tests/Integration/Managers/FileManagerTest.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Churn\Tests\Integration\Managers;
+
+use Churn\Managers\FileManager;
+use Churn\Tests\BaseTestCase;
+use Churn\Values\File;
+use Illuminate\Support\Collection;
+
+class FileManagerTest extends BaseTestCase
+{
+    /**
+     * The class being tested.
+     * @var FileManager
+     */
+    private $fileManager;
+
+    /** @test **/
+    public function it_can_be_instantiated()
+    {
+        $this->assertInstanceOf(FileManager::class, $this->fileManager);
+    }
+
+    /** @test **/
+    public function it_can_recursively_get_the_php_files_in_a_path()
+    {
+        $path = __DIR__ . '';
+        $results = $this->fileManager->getPhpFiles($path);
+        $this->assertInstanceOf(Collection::class, $results);
+        $this->assertCount(1, $results);
+        $this->assertInstanceOf(File::class, $results[0]);
+    }
+
+    public function setUp()
+    {
+        parent::setup();
+
+        $this->fileManager = new FileManager;
+    }
+}
\ No newline at end of file
diff --git a/tests/Unit/Assessors/GitCommitCountAssessor/GitCommitCountAssessorTest.php b/tests/Unit/Assessors/GitCommitCountAssessor/GitCommitCountAssessorTest.php
deleted file mode 100644
--- a/tests/Unit/Assessors/GitCommitCountAssessor/GitCommitCountAssessorTest.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php declare(strict_types = 1);
-
-namespace Churn\Tests\Assessors\CyclomaticComplexity;
-
-use Mockery as m;
-use Churn\Tests\BaseTestCase;
-use Churn\Assessors\GitCommitCount\GitCommitCountAssessor;
-use Churn\Services\CommandService;
-use Churn\Exceptions\CommandServiceException;
-
-class GitCommitCountAssessorTest extends BaseTestCase
-{
-    /** @test */
-    public function when_a_file_has_one_commit_it_returns_one()
-    {
-        $commandService = m::mock(CommandService::class);
-        $commandService->shouldReceive('execute')
-            ->once()
-            ->with("cd src/Assessors/CyclomaticComplexity && git log --name-only --pretty=format: src/Assessors/CyclomaticComplexity/CyclomaticComplexityAssessor.php | sort | uniq -c | sort -nr")
-            ->andReturn(['      1 src/Assessors/CyclomaticComplexity/CyclomaticComplexityAssessor.php']);
-
-        $this->assertSame(1, (new GitCommitCountAssessor($commandService))->assess('src/Assessors/CyclomaticComplexity/CyclomaticComplexityAssessor.php'));
-    }
-
-    /** @test */
-    public function when_a_file_has_four_commits_it_returns_four()
-    {
-        $commandService = m::mock(CommandService::class);
-        $commandService->shouldReceive('execute')
-            ->once()
-            ->with("git log --name-only --pretty=format: README.md | sort | uniq -c | sort -nr")
-            ->andReturn(['      4 README.md']);
-
-        $this->assertSame(4, (new GitCommitCountAssessor($commandService))->assess('README.md'));
-    }
-
-    /** @test */
-    public function it_handles_the_scenario_when_the_provided_file_does_not_exist_or_has_no_commits()
-    {
-        $commandService = m::mock(CommandService::class);
-        $commandService->shouldReceive('execute')
-            ->once()
-            ->with("git log --name-only --pretty=format: doesNotExist.md | sort | uniq -c | sort -nr")
-            ->andReturn([]);
-
-        $this->assertSame(0, (new GitCommitCountAssessor($commandService))->assess('doesNotExist.md'));
-    }
-}
\ No newline at end of file
diff --git a/tests/Unit/Results/ResultsGeneratorTest.php b/tests/Unit/Results/ResultsGeneratorTest.php
deleted file mode 100644
--- a/tests/Unit/Results/ResultsGeneratorTest.php
+++ /dev/null
@@ -1,65 +0,0 @@
-<?php declare(strict_types = 1);
-
-namespace Churn\Tests\Results;
-
-use Churn\Tests\BaseTestCase;
-use Churn\Results\ResultsGenerator;
-use Churn\Results\ResultCollection;
-use Churn\Services\CommandService;
-use Churn\Assessors\GitCommitCount\GitCommitCountAssessor;
-use Churn\Assessors\CyclomaticComplexity\CyclomaticComplexityAssessor;
-use Mockery as m;
-
-class ResultsGeneratorTest extends BaseTestCase
-{
-    /**
-     * The object we're testing.
-     * @var ResultsGenerator
-     */
-    protected $resultsGenerator;
-
-    /**
-     * The results generated by the ResultsGenerator
-     * @var Results
-     */
-    protected $results;
-
-    /** @test */
-    public function it_can_be_created()
-    {
-        $this->assertInstanceOf(ResultsGenerator::class, $this->resultsGenerator);
-    }
-
-    /** @test */
-    public function it_generates_a_ResultCollection_object()
-    {
-        $this->assertInstanceOf(ResultCollection::class, $this->results);
-    }
-
-    public function setup()
-    {
-        parent::setup();
-        $commandService = m::mock(CommandService::class);
-        $commandService->shouldReceive('execute')
-            ->once()
-            ->with("cd /foo/bar && git log --name-only --pretty=format: /foo/bar/Baz.php | sort | uniq -c | sort -nr")
-            ->andReturn(['      5 /foo/bar/Baz.php']);
-
-        $commitCountAssessor = new GitCommitCountAssessor($commandService);
-        $cyclomaticComplexityAssessor = m::mock(CyclomaticComplexityAssessor::class);
-        $cyclomaticComplexityAssessor->shouldReceive('assess')
-            ->once()
-            ->with('/foo/bar/Baz.php')
-            ->andReturn(6);
-
-        $this->resultsGenerator = new ResultsGenerator($commitCountAssessor, $cyclomaticComplexityAssessor);
-
-        $this->results = $this->resultsGenerator->getResults([
-            [
-                "fullPath"    => "/foo/bar/Baz.php",
-                "displayPath" => "src/Assessors/CyclomaticComplexity/CyclomaticComplexityAssessor.php",
-            ]
-        ]);
-    }
-
-}
\ No newline at end of file
diff --git a/tests/Unit/Services/CommandServiceTest.php b/tests/Unit/Services/CommandServiceTest.php
deleted file mode 100644
--- a/tests/Unit/Services/CommandServiceTest.php
+++ /dev/null
@@ -1,17 +0,0 @@
-<?php declare(strict_types = 1);
-
-namespace Churn\Tests\Results;
-
-use Churn\Tests\BaseTestCase;
-use Churn\Services\CommandService;
-
-class CommandServiceTest extends BaseTestCase
-{
-    /** @test */
-    public function it_returns_the_output_of_a_command()
-    {
-        $commandService = new CommandService;
-        $result = $commandService->execute("echo hi");
-        $this->assertSame(["hi"], $result);
-    }
-}
\ No newline at end of file
diff --git a/tests/Unit/Values/FileTest.php b/tests/Unit/Values/FileTest.php
new file mode 100644
--- /dev/null
+++ b/tests/Unit/Values/FileTest.php
@@ -0,0 +1,31 @@
+<?php
+
+
+namespace Churn\Tests\Unit\Values;
+
+
+use Churn\Tests\BaseTestCase;
+use Churn\Values\File;
+
+class FileTest extends BaseTestCase
+{
+    /** @test **/
+    public function it_can_be_instantiated()
+    {
+        $this->assertInstanceOf(File::class, $this->file);
+    }
+
+    /** @test **/
+    public function it_can_return_its_values()
+    {
+        $this->assertSame('foo/bar/baz.php', $this->file->getFullPath());
+        $this->assertSame('bar/baz.php', $this->file->getDisplayPath());
+    }
+
+    public function setUp()
+    {
+        parent::setUp();
+
+        $this->file = new File(['fullPath' => 'foo/bar/baz.php', 'displayPath' => 'bar/baz.php']);
+    }
+}
\ No newline at end of file