diff --git a/.travis.yml b/.travis.yml index 47253ed97..a4644ea41 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,6 +22,7 @@ branches: only: - master - develop + - support/2 - wip env: diff --git a/src/Console/Helper/BorderlessTable.php b/src/Console/Helper/BorderlessTable.php new file mode 100644 index 000000000..fd34b199f --- /dev/null +++ b/src/Console/Helper/BorderlessTable.php @@ -0,0 +1,38 @@ +setStyle($this->tableStyle()); + } + + /** + * Provides the table style. + * + * @return \Symfony\Component\Console\Helper\TableStyle + * A TableStyle object. + */ + private function tableStyle(): TableStyle { + return (new TableStyle()) + ->setHorizontalBorderChars('', '') + ->setVerticalBorderChars('', '') + ->setDefaultCrossingChar(''); + } + +} diff --git a/src/Domain/Tool/Coverage/CodeCoverageReportBuilder.php b/src/Domain/Tool/Coverage/CodeCoverageReportBuilder.php index 42bc60aba..60acd2579 100644 --- a/src/Domain/Tool/Coverage/CodeCoverageReportBuilder.php +++ b/src/Domain/Tool/Coverage/CodeCoverageReportBuilder.php @@ -11,7 +11,6 @@ use Acquia\Orca\Helper\Filesystem\OrcaPathHandler; use Noodlehaus\Exception\FileNotFoundException as NoodlehausFileNotFoundException; use Noodlehaus\Exception\ParseException as NoodlehausParseException; -use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Finder\Exception\DirectoryNotFoundException as FinderDirectoryNotFoundException; /** @@ -54,6 +53,13 @@ class CodeCoverageReportBuilder { */ private $finderFactory; + /** + * The number of exported config files. + * + * @var int + */ + private $numConfigFiles = 0; + /** * The ORCA path handler. * @@ -78,7 +84,7 @@ class CodeCoverageReportBuilder { /** * The data on tests. * - * @var int[] + * @var array */ private $testsData = [ 'classes' => 0, @@ -147,8 +153,14 @@ private function ensurePreconditions(): void { * In case of error. */ private function compileData(): void { - $this->getPhplocData(); - $this->getTestsData(); + try { + $this->getPhplocData(); + $this->getTestsData(); + $this->getConfigFilesData(); + } + catch (FinderDirectoryNotFoundException $e) { + throw new OrcaDirectoryNotFoundException($e->getMessage()); + } } /** @@ -182,16 +194,11 @@ private function getPhplocData(): void { */ private function getTestsData(): void { $finder = $this->finderFactory->create(); - try { - $classes = $finder - ->in($this->path) - ->name('*Test.php') - ->notPath(self::FINDER_PATH_EXCLUSIONS) - ->contains('public function test'); - } - catch (FinderDirectoryNotFoundException $e) { - throw new OrcaDirectoryNotFoundException($e->getMessage()); - } + $classes = $finder + ->in($this->path) + ->name('*Test.php') + ->notPath(self::FINDER_PATH_EXCLUSIONS) + ->contains('public function test'); $this->testsData['classes'] = iterator_count($classes); @@ -202,6 +209,56 @@ private function getTestsData(): void { } } + /** + * Gets the config files data. + */ + private function getConfigFilesData(): void { + $paths = $this->getExtensionPaths(); + foreach ($paths as $path) { + $this->countConfigFiles($path); + } + } + + /** + * Gets the paths to Drupal extensions. + */ + private function getExtensionPaths(): array { + $paths = []; + + $finder = $this->finderFactory->create(); + $info_files = $finder + ->in($this->path) + ->name('*.info.yml') + ->notPath(self::FINDER_PATH_EXCLUSIONS) + ->notPath('tests') + ->files(); + + foreach ($info_files as $file) { + $paths[] = $file->getPath(); + } + + return $paths; + } + + /** + * Counts exported configuration files under the given path. + * + * @param string $path + * The path to search for config files. + * + * @throws \Acquia\Orca\Exception\OrcaDirectoryNotFoundException + */ + private function countConfigFiles(string $path): void { + $finder = $this->finderFactory->create(); + $config_files = $finder + ->in($path) + ->path('config') + ->name('*.yml') + ->notPath(self::FINDER_PATH_EXCLUSIONS) + ->notPath('tests'); + $this->numConfigFiles += iterator_count($config_files); + } + /** * Compiles the report data into a table array. * @@ -209,13 +266,13 @@ private function getTestsData(): void { * The report data array. */ private function buildTable(): array { - $complexity = $this->phplocData['ccn']; - $assertions = $this->testsData['assertions']; return [ - [' Test assertions', $assertions], - ['÷ Cyclomatic complexity', $complexity], - new TableSeparator(), - [' Magic number', $this->computeMagicNumber()], + ['Health score', $this->computeHealthScore()], + [' Numerator', $this->computeNumerator()], + [' Test assertions', $this->getAssertions()], + [' Denominator', $this->computeDenominator()], + [' Cyclomatic complexity', $this->getComplexity()], + [' Exported config files', $this->numConfigFiles], ]; } @@ -225,15 +282,55 @@ private function buildTable(): array { * @return float * The score as a floating point number. */ - private function computeMagicNumber(): float { - $assertions = $this->testsData['assertions']; - $complexity = $this->phplocData['ccn']; + private function computeHealthScore(): float { + $numerator = $this->computeNumerator(); + $denominator = $this->computeDenominator(); - if (!$assertions || !$complexity) { + if (!$numerator || !$denominator) { return 0; } - return number_format($assertions / $complexity, 2); + return (float) number_format($numerator / $denominator, 2); + } + + /** + * Computes the health score numerator. + * + * @return int + * The numerator. + */ + private function computeNumerator(): int { + return $this->getAssertions(); + } + + /** + * Computes the health score ratio denominator. + * + * @return int + * The denominator. + */ + private function computeDenominator(): int { + return $this->getComplexity() + $this->numConfigFiles; + } + + /** + * Gets the number of test assertions. + * + * @return int + * The number of assetions. + */ + private function getAssertions(): int { + return $this->testsData['assertions']; + } + + /** + * Gets the cyclomatic complexity. + * + * @return int + * The cyclomatic complexity. + */ + private function getComplexity(): int { + return $this->phplocData['ccn']; } } diff --git a/src/Domain/Tool/Coverage/CoverageTask.php b/src/Domain/Tool/Coverage/CoverageTask.php index 00dc3153d..ec67c0716 100644 --- a/src/Domain/Tool/Coverage/CoverageTask.php +++ b/src/Domain/Tool/Coverage/CoverageTask.php @@ -2,7 +2,7 @@ namespace Acquia\Orca\Domain\Tool\Coverage; -use Acquia\Orca\Console\Helper\StatusTable; +use Acquia\Orca\Console\Helper\BorderlessTable; use Acquia\Orca\Domain\Tool\TaskInterface; use Acquia\Orca\Exception\OrcaFileNotFoundException; use Symfony\Component\Console\Output\OutputInterface; @@ -52,7 +52,7 @@ public function __construct(CodeCoverageReportBuilder $coverage_report_builder, public function execute(): void { try { $rows = $this->builder->build($this->path); - (new StatusTable($this->output)) + (new BorderlessTable($this->output)) ->setRows($rows) ->render(); } @@ -81,7 +81,7 @@ public function label(): string { * {@inheritdoc} */ public function statusMessage(): string { - return 'Estimating Code Coverage'; + return 'Generating health score'; } } diff --git a/tests/Domain/Tool/Coverage/CodeCoverageReportBuilderTest.php b/tests/Domain/Tool/Coverage/CodeCoverageReportBuilderTest.php index 076874d5e..b75514717 100644 --- a/tests/Domain/Tool/Coverage/CodeCoverageReportBuilderTest.php +++ b/tests/Domain/Tool/Coverage/CodeCoverageReportBuilderTest.php @@ -16,18 +16,21 @@ use Noodlehaus\Exception\ParseException; use PHPUnit\Framework\TestCase; use Prophecy\Argument; -use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Finder\Exception\DirectoryNotFoundException as FinderDirectoryNotFoundException; use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\SplFileInfo; /** + * @property \Acquia\Orca\Helper\Config\ConfigLoader|\Prophecy\Prophecy\ObjectProphecy $configLoader * @property \Acquia\Orca\Helper\Filesystem\FinderFactory|\Prophecy\Prophecy\ObjectProphecy $finderFactory * @property \Acquia\Orca\Helper\Filesystem\OrcaPathHandler|\Prophecy\Prophecy\ObjectProphecy $orca - * @property \Acquia\Orca\Helper\Config\ConfigLoader|\Prophecy\Prophecy\ObjectProphecy $configLoader + * @property \ArrayIterator $configIterator + * @property \ArrayIterator $extensionIterator * @property \ArrayIterator $phpIterator * @property \ArrayIterator $testIterator * @property \Noodlehaus\Config|\Prophecy\Prophecy\ObjectProphecy $config + * @property \Symfony\Component\Finder\Finder|\Prophecy\Prophecy\ObjectProphecy $configFinder + * @property \Symfony\Component\Finder\Finder|\Prophecy\Prophecy\ObjectProphecy $extensionFinder * @property \Symfony\Component\Finder\Finder|\Prophecy\Prophecy\ObjectProphecy $phpFinder * @property \Symfony\Component\Finder\Finder|\Prophecy\Prophecy\ObjectProphecy $testFinder * @coversDefaultClass \Acquia\Orca\Domain\Tool\Coverage\CodeCoverageReportBuilder @@ -131,7 +134,39 @@ protected function setUp(): void { ->contains(Argument::any()) ->willReturn($this->testFinder); $test_file = $this->prophesize(SplFileInfo::class); - $this->testIterator = new ArrayIterator([$test_file->reveal()]); + $this->testIterator = new \ArrayIterator([$test_file->reveal()]); + + $this->extensionFinder = $this->prophesize(Finder::class); + $this->extensionFinder + ->in(Argument::any()) + ->willReturn($this->extensionFinder); + $this->extensionFinder + ->name(Argument::any()) + ->willReturn($this->extensionFinder); + $this->extensionFinder + ->notPath(Argument::any()) + ->willReturn($this->extensionFinder); + $this->extensionFinder + ->files() + ->willReturn($this->extensionFinder); + $extension_info_file = $this->prophesize(SplFileInfo::class); + $this->extensionIterator = new \ArrayIterator([$extension_info_file->reveal()]); + + $this->configFinder = $this->prophesize(Finder::class); + $this->configFinder + ->in(Argument::any()) + ->willReturn($this->configFinder); + $this->configFinder + ->name(Argument::any()) + ->willReturn($this->configFinder); + $this->configFinder + ->notPath(Argument::any()) + ->willReturn($this->configFinder); + $this->configFinder + ->files() + ->willReturn($this->configFinder); + $config_file = $this->prophesize(SplFileInfo::class); + $this->configIterator = new \ArrayIterator([$config_file->reveal()]); } private function createBuilder(): CodeCoverageReportBuilder { @@ -151,11 +186,23 @@ private function createBuilder(): CodeCoverageReportBuilder { ->getIterator() ->willReturn($this->testIterator); $test_finder = $this->testFinder->reveal(); + $this->extensionFinder + ->getIterator() + ->willReturn($this->extensionIterator); + $extension_finder = $this->extensionFinder->reveal(); + $this->configFinder + ->getIterator() + ->willReturn($this->configIterator); + $config_finder = $this->configFinder->reveal(); $this->finderFactory ->create() + // @todo This temporal coupling (i.e., dependence on the order of + // execution) suggests suboptimal design in the production code. ->willReturn( $php_finder, - $test_finder + $test_finder, + $extension_finder, + $config_finder ); $finder_factory = $this->finderFactory->reveal(); $orca_path_handler = $this->orca->reveal(); @@ -165,7 +212,10 @@ private function createBuilder(): CodeCoverageReportBuilder { /** * @dataProvider providerHappyPath */ - public function testHappyPath(string $path, int $assertions, int $complexity, string $score): void { + public function testHappyPath($path, $numerator, $assertions, $denominator, $complexity, $config_file_count, $score): void { + // @todo The following unwieldy test arrangement is mostly setting up + // FinderFactories and Finders. This complexity may point to a need to + // reconsider the corresponding production code design. $this->phplocData['ccn'] = $complexity; $this->testFinder ->in($path) @@ -175,36 +225,177 @@ public function testHappyPath(string $path, int $assertions, int $complexity, st ->name('*Test.php') ->shouldBeCalledOnce() ->willReturn($this->testFinder); - $file_info = $this->prophesize(SplFileInfo::class); - $file_info + $test_file_info = $this->prophesize(SplFileInfo::class); + $test_file_info ->getContents() ->willReturn('self::assertTrue(TRUE);'); - $file_info = $file_info->reveal(); - $files = array_fill(0, $assertions, $file_info); - $this->testIterator = new ArrayIterator($files); + $test_file_info = $test_file_info->reveal(); + $test_files = array_fill(0, $assertions, $test_file_info); + $this->testIterator = new \ArrayIterator($test_files); + $this->extensionFinder + ->in(Argument::any()) + ->shouldBeCalledOnce() + ->willReturn($this->extensionFinder); + $this->extensionFinder + ->name('*.info.yml') + ->shouldBeCalledOnce() + ->willReturn($this->extensionFinder); + $this->extensionFinder + ->notPath(Argument::any()) + ->shouldBeCalledTimes(2) + ->willReturn($this->extensionFinder); + $this->extensionFinder + ->notPath('tests') + ->shouldBeCalledOnce() + ->willReturn($this->extensionFinder); + $this->extensionFinder + ->files() + ->shouldBeCalledOnce() + ->willReturn($this->extensionFinder); + $extensions_file_info = $this->prophesize(SplFileInfo::class); + $extensions_file_info + ->getPath() + ->willReturn('/example/path'); + $extensions_file_info = $extensions_file_info->reveal(); + $extension_files = array_fill(0, 1, $extensions_file_info); + $this->extensionIterator = new \ArrayIterator($extension_files); + $this->configFinder + ->in($path) + ->willReturn($this->configFinder); + $this->configFinder + ->path('config') + ->willReturn($this->configFinder); + $this->configFinder + ->name('*.yml') + ->willReturn($this->configFinder); + $config_file_info = $this->prophesize(SplFileInfo::class); + $config_file_info = $config_file_info->reveal(); + $config_files = array_fill(0, $config_file_count, $config_file_info); + $this->configIterator = new \ArrayIterator($config_files); $builder = $this->createBuilder(); $report = $builder->build($path); self::assertEquals([ - [' Test assertions', $assertions], - ['÷ Cyclomatic complexity', $complexity], - new TableSeparator(), - [' Magic number', $score], + ['Health score', $score], + [' Numerator', $numerator], + [' Test assertions', $assertions], + [' Denominator', $denominator], + [' Cyclomatic complexity', $complexity], + [' Exported config files', $config_file_count], ], $report, 'Returned correct report data.'); } public function providerHappyPath(): array { return [ - ['test/example', 100, 100, '1.00'], - ['test/example', 200, 100, '2.00'], - ['test/example', 100, 200, '0.50'], - ['test/example', 100, 1, '100.00'], - ['test/example', 0, 100, '0.00'], - ['test/example', 100, 0, '0.00'], - ['test/example', 33, 375, '0.09'], - ['test/example', 161, 387, '0.42'], - ['lorem/ipsum', 1, 1, '1.00'], + [ + 'path' => 'test/example', + 'numerator' => 100, + 'assertions' => 100, + 'denominator' => 100, + 'complexity' => 100, + 'config_file_count' => 0, + 'score' => '1.00', + ], + [ + 'path' => 'test/example', + 'numerator' => 200, + 'assertions' => 200, + 'denominator' => 100, + 'complexity' => 100, + 'config_file_count' => 0, + 'score' => '2.00', + ], + [ + 'path' => 'test/example', + 'numerator' => 100, + 'assertions' => 100, + 'denominator' => 200, + 'complexity' => 200, + 'config_file_count' => 0, + 'score' => '0.50', + ], + [ + 'path' => 'test/example', + 'numerator' => 100, + 'assertions' => 100, + 'denominator' => 1, + 'complexity' => 1, + 'config_file_count' => 0, + 'score' => '100.00', + ], + [ + 'path' => 'test/example', + 'numerator' => 0, + 'assertions' => 0, + 'denominator' => 100, + 'complexity' => 100, + 'config_file_count' => 0, + 'score' => '0.00', + ], + [ + 'path' => 'test/example', + 'numerator' => 100, + 'assertions' => 100, + 'denominator' => 0, + 'complexity' => 0, + 'config_file_count' => 0, + 'score' => '0.00', + ], + [ + 'path' => 'test/example', + 'numerator' => 33, + 'assertions' => 33, + 'denominator' => 375, + 'complexity' => 375, + 'config_file_count' => 0, + 'score' => '0.09', + ], + [ + 'path' => 'test/example', + 'numerator' => 161, + 'assertions' => 161, + 'denominator' => 387, + 'complexity' => 387, + 'config_file_count' => 0, + 'score' => '0.42', + ], + [ + 'path' => 'lorem/ipsum', + 'numerator' => 1, + 'assertions' => 1, + 'denominator' => 1, + 'complexity' => 1, + 'config_file_count' => 0, + 'score' => '1.00', + ], + [ + 'path' => 'lorem/ipsum', + 'numerator' => 10, + 'assertions' => 10, + 'denominator' => 2, + 'complexity' => 1, + 'config_file_count' => 1, + 'score' => '5.00', + ], + [ + 'path' => 'lorem/ipsum', + 'numerator' => 10, + 'assertions' => 10, + 'denominator' => 100, + 'complexity' => 1, + 'config_file_count' => 99, + 'score' => '0.10', + ], + [ + 'path' => 'lorem/ipsum', + 'numerator' => 20, + 'assertions' => 20, + 'denominator' => 20, + 'complexity' => 10, + 'config_file_count' => 10, + 'score' => '1.00', + ], ]; }