kahlan/kahlan

View on GitHub
src/Reporter/Coverage.php

Summary

Maintainability
C
1 day
Test Coverage
<?php
namespace Kahlan\Reporter;

use Kahlan\Jit\ClassLoader;
use Kahlan\Reporter\Coverage\Collector;
use Kahlan\Reporter\Coverage\Metrics;

class Coverage extends Terminal
{
    /**
     * Class dependencies.
     *
     * @var array
     */
    protected static $_classes = [
        'classloader' => ClassLoader::class
    ];

    /**
     * Collect time.
     *
     * @var float
     */
    protected $_time = 0;

    /**
     * The coverage verbosity.
     *
     * @param integer
     */
    protected $_verbosity = 0;

    /**
     * Reference to the coverage collector driver.
     *
     * @param object
     */
    protected $_collector = '';

    /**
     * Status of the reporter.
     *
     * @var array
     */
    protected $_enabled = true;

    /**
     * Store prefix by level for tree rendering.
     *
     * @var array
     */
    protected $_prefixes = [];

    /**
     * The Constructor.
     *
     * @param array $config The config for the reporter, the options are:
     *                      - `'verbosity`' _integer|string_: The verbosity level:
     *                        - 1      : overall coverage value for the whole code.
     *                        - 2      : overall coverage by namespaces.
     *                        - 3      : overall coverage by classes.
     *                        - 4      : overall coverage by methods and functions.
     *                        - string : coverage for a fully namespaced (class/method/namespace) string.
     */
    public function __construct($config = [])
    {
        parent::__construct($config);
        $defaults = ['verbosity' => 1];
        $config += $defaults;

        $verbosity = $config['verbosity'];
        $this->_verbosity  = is_numeric($verbosity) ? (integer) $verbosity : (string) $verbosity;

        if (is_string($this->_verbosity)) {
            $class = preg_replace('/(::)?\w+\(\)$/', '', $this->_verbosity);
            $classloader = static::$_classes['classloader'];
            $loader = $classloader::instance();

            if ($loader && $path = $loader->findPath($class)) {
                $config['path'] = $path;
            }
        }

        $this->_collector = new Collector($config);
    }

    /**
     * Callback called before any specs processing.
     *
     * @param array $args The suite arguments.
     */
    public function start($args)
    {
    }

    /**
     * Callback called on a suite start.
     *
     * @param object $suite The suite instance.
     */
    public function suiteStart($suite = null)
    {
        $this->_collector->start();
    }

    /**
     * Callback called on a spec start.
     *
     * @param object $spec The spec object of the whole spec.
     */
    public function specStart($spec = null)
    {
        parent::specStart($spec);
        if (!$this->enabled()) {
            return;
        }
    }

    /**
     * Callback called after a spec execution.
     *
     * @param object $log The log object of the whole spec.
     */
    public function specEnd($log = null)
    {
        parent::specEnd($log);
        if (!$this->enabled()) {
            return;
        }
    }

    /**
     * Callback called after a suite execution.
     *
     * @param object $suite The suite instance.
     */
    public function suiteEnd($suite = null)
    {
        $this->_collector->stop();
    }

    /**
     * Gets the collector.
     *
     * @return object
     */
    public function collector()
    {
        return $this->_collector;
    }

    /**
     * Delegates the call to the collector instance.
     *
     * @param  string  $name The function name.
     * @param  array   $args The arguments to pass to the function.
     * @return mixed
     */
    public function __call($name, $args)
    {
        return call_user_func_array([$this->collector(), $name], $args);
    }

    /**
     * Gets the metrics about the coverage result.
     */
    public function metrics()
    {
        $this->_start = microtime(true);
        $result = $this->_collector->metrics();
        $this->_time  = microtime(true) - $this->_start;
        return $result;
    }

    /**
     * Outputs some metrics info where the metric is not the total coverage.
     *
     * @param Metrics $metrics   A metrics instance.
     * @param mixed   $verbosity The options for the reporter, the options are:
     *                            - `'verbosity`' _integer|string_: The verbosity level:
     *                            - 1      : overall coverage value for the whole code.
     *                            - 2      : overall coverage by namespaces.
     *                            - 3      : overall coverage by classes.
     *                            - 4      : overall coverage by methods and functions.
     *                            - string : coverage for a fully namespaced (class/method/namespace) string.
     */
    protected function _renderMetrics($metrics, $verbosity)
    {
        $maxLabelWidth = null;
        if ($verbosity === 1) {
            return;
        }
        $metricsReport = $this->_getMetricsReport($metrics->children(), $verbosity, 0, 3, $maxLabelWidth);
        $name = $metrics->name() ?: '\\';
        $maxLabelWidth = max(strlen($name) + 1, $maxLabelWidth);
        $maxLabelWidth += 4;
        $stats = $metrics->data();
        $percent = number_format($stats['percent'], 2);
        $style = $this->_style($percent);
        $maxLineWidth = strlen($stats['lloc']);

        $this->write(str_repeat(' ', $maxLabelWidth));
        $this->write('  ');
        $this->write(str_pad('Lines', $maxLineWidth * 2 + 3, ' ', STR_PAD_BOTH));
        $this->write(str_pad('%', 12, ' ', STR_PAD_LEFT));
        $this->write("\n\n");
        $this->write(str_pad(' ' . $name, $maxLabelWidth));
        $this->write('  ');
        $this->write(str_pad("{$stats['cloc']}", $maxLineWidth, ' ', STR_PAD_LEFT));
        $this->write(' / ');
        $this->write(str_pad("{$stats['lloc']}", $maxLineWidth, ' ', STR_PAD_LEFT));
        $this->write('     ');
        $this->write(str_pad("{$percent}%", 7, ' ', STR_PAD_LEFT), $style);
        $this->write("\n");
        $this->_renderMetricsReport($metricsReport, $maxLabelWidth, $maxLineWidth, 0);
    }

    /**
     * Outputs some metrics reports built using `::_getMetricsReport()`.
     *
     * @param array $metricsReport An array of nested metrics reports extracted according some verbosity.
     * @param array $labelWidth    The width column of the label column used for padding.
     * @param array $lineWidth     The width column of the covered lines data used for padding.
     * @param int   $depth         The actual depth in the reporting to build tree prefix.
     */
    protected function _renderMetricsReport($metricsReport, $labelWidth, $lineWidth, $depth)
    {
        $nbChilden = count($metricsReport);
        $index = 0;
        foreach ($metricsReport as $name => $data) {
            $isLast = $index === $nbChilden - 1;
            if ($isLast) {
                $this->_prefixes[$depth] = '└──';
            } else {
                $this->_prefixes[$depth] = '├──';
            }

            $metrics = $data['metrics'];
            $stats = $metrics->data();
            $percent = number_format($stats['percent'], 2);
            $style = $this->_style($percent);

            $prefix = join('', $this->_prefixes) . ' ';
            $diff = strlen($prefix) - strlen(mb_convert_encoding($prefix, 'ISO-8859-1'));

            $type = $metrics->type();
            $color = $type === 'function' || $type === 'method' ? 'd' : '';
            $this->write($prefix);
            $this->write(str_pad($name, $labelWidth + $diff - strlen($prefix)), $color);
            $this->write('  ');
            $this->write(str_pad("{$stats['cloc']}", $lineWidth, ' ', STR_PAD_LEFT));
            $this->write(' / ');
            $this->write(str_pad("{$stats['lloc']}", $lineWidth, ' ', STR_PAD_LEFT));
            $this->write('     ');
            $this->write(str_pad("{$percent}%", 7, ' ', STR_PAD_LEFT), $style);
            $this->write("\n");

            if ($isLast) {
                $this->_prefixes[$depth] = '   ';
            } else {
                $this->_prefixes[$depth] = '│  ';
            }
            $this->_renderMetricsReport($data['children'], $labelWidth, $lineWidth, $depth + 1);
            $index++;
        }
        $this->_prefixes[$depth] = '';
    }

    /**
     * Extract some metrics reports to display according to a verbosity parameter.
     *
     * @param array  $children  A array of metrics.
     * @param mixed  $verbosity The options for the reporter, the options are:
     *                           - `'verbosity`' _integer|string_: The verbosity level:
     *                           - 1      : overall coverage value for the whole code.
     *                           - 2      : overall coverage by namespaces.
     *                           - 3      : overall coverage by classes.
     *                           - 4      : overall coverage by methods and functions.
     *                           - string : coverage for a fully namespaced (class/method/namespace) string.
     * @param integer $depth    The actual depth in the reporting.
     * @param integer $tab      The size of the tab used for lablels.
     * @param integer $maxWidth Will contain the maximum width obtained for labels.
     *
     * @return array
     */
    protected function _getMetricsReport($children, $verbosity, $depth = 0, $tab = 3, &$maxWidth = null)
    {
        $list = [];
        foreach ($children as $child) {
            $type = $child->type();

            if ($verbosity === 2 && $type !== 'namespace') {
                continue;
            }
            if ($verbosity === 3 && ($type === 'function' || $type === 'method')) {
                continue;
            }

            $name = $child->name();

            if ($name !== '\\') {
                $pos = strrpos($name, '\\', $type === 'namespace' ? - 2 : 0);
                $basename = substr($name, $pos !== false ? $pos + 1 : 0);
            } else {
                $basename = '\\';
            }

            $len = strlen($basename) + ($depth + 1) * $tab;
            if ($len > $maxWidth) {
                $maxWidth = $len;
            }
            $list[$basename] = [
                'metrics'  => $child,
                'children' => $this->_getMetricsReport($child->children(), $verbosity, $depth + 1, $tab, $maxWidth)
            ];
        }
        return $list;
    }

    /**
     * Outputs the coverage report of a metrics instance.
     *
     * @param Metrics $metrics A metrics instance.
     */
    protected function _renderCoverage($metrics)
    {
        $stats = $metrics->data();
        foreach ($stats['files'] as $file) {
            $this->write("File: {$file}" . "\n\n");

            $lines = file($file);

            $coverage = $this->_collector->export($file);

            if (isset($stats['line'])) {
                $start = $stats['line']['start'];
                $stop = $stats['line']['stop'];
            } else {
                $start = 0;
                $stop = count($lines) - 1;
            }

            for ($i = $start; $i <= $stop; $i++) {
                $value = $coverage[$i] ?? null;
                $line = str_pad($i + 1, 6, ' ', STR_PAD_LEFT);
                $line .= ':' . str_pad($value, 6, ' ');
                $line .= $lines[$i];
                if ($value) {
                    $this->write($line, 'n;green');
                } elseif ($value === 0) {
                    $this->write($line, 'n;red');
                } else {
                    $this->write($line);
                }
            }
            $this->write("\n\n");
        }
    }

    /**
     * Helper determinig a color from a coverage rate.
     *
     * @param integer $percent The coverage rate in percent.
     */
    protected function _style($percent)
    {
        switch (true) {
            case $percent >= 80:
                return 'n;green';
            break;
            case $percent >= 60:
                return 'n;default';
            break;
            case $percent >= 40:
                return 'n;yellow';
            break;
        }
        return 'n;red';
    }

    /**
     * Callback called at the end of the process.
     *
     * @param object $summary The execution summary instance.
     */
    public function stop($summary)
    {
        if (!$this->_verbosity) {
            return;
        }
        $this->write("Coverage Summary\n----------------\n");

        $verbosity = $this->_verbosity;
        $metrics = is_numeric($this->_verbosity) ? $this->metrics() : $this->metrics()->get($verbosity);

        if (!$metrics) {
            $this->write("\nUnexisting namespace/reference: `{$this->_verbosity}`, coverage can't be generated.\n\n", "n;yellow");
            return;
        }

        $this->_renderMetrics($metrics, $verbosity);
        $this->write("\n");

        if (is_string($verbosity)) {
            $this->_renderCoverage($metrics);
            $this->write("\n");
        }

        // Output the original stored metrics object (the total coverage)
        $name = $metrics->name();
        $stats = $metrics->data();
        $percent = number_format($stats['percent'], 2);
        $this->write(str_repeat('  ', substr_count($name, '\\')));

        $this->write('Total: ');
        $this->write("{$percent}% ", $this->_style($percent));
        $this->write("({$stats['cloc']}/{$stats['lloc']})");
        $this->write("\n");

        // Output the time to collect coverage
        $time = number_format($this->_time, 3);
        $this->write("\nCoverage collected in {$time} seconds\n\n\n");
    }

    /**
     * Gets the status of the reporter.
     *
     * @return boolean $active
     */
    public function enabled()
    {
        return $this->_enabled;
    }

    /**
     * Gets this reporter.
     */
    public function enable()
    {
        $this->_enabled = true;
        $this->_collector->start();
    }

    /**
     * Disables this reporter.
     */
    public function disable()
    {
        $this->_enabled = false;
        $this->_collector->stop();
    }
}