diff --git a/Makefile b/Makefile index e4b674e..5f51c2e 100644 --- a/Makefile +++ b/Makefile @@ -3,10 +3,20 @@ phpcheck: phpcheck-coverage-html: @phpcheck --coverage-html build/coverage + @xdg-open build/coverage/index.html -phpcheck-coverage-text: +phpcheck-coverage-console: @phpcheck --coverage-text +phpcheck-coverage-text: + @phpcheck --coverage-text build/coverage.txt + +phpcheck-log-junit: + @phpcheck --log-junit build/phpcheck.xml + +phpcheck-log-text: + @phpcheck --log-text build/phpcheck.txt + phpcheck-no-defects: @phpcheck -d diff --git a/README.md b/README.md index 491e344..2fd6643 100644 --- a/README.md +++ b/README.md @@ -109,19 +109,22 @@ The `phpcheck` program accept a number of arguments and options: path File or folder with checks [default: "checks"] Options: - --bootstrap[=BOOTSTRAP] A PHP script that is included before the checks run - -f, --filter[=FILTER] Filter the checks that will be run - -i, --iterations=ITERATIONS How many times each check will be run [default: 100] - -j, --log-junit[=LOG-JUNIT] Log check execution in JUnit XML format to file - -d, --no-defects[=NO-DEFECTS] Ignore previous defects [default: false] - -h, --help Display this help message - -q, --quiet Do not output any message - -s, --seed[=SEED] Seed the random number generator to get repeatable runs - -V, --version Display this application version - --ansi Force ANSI output - --no-ansi Disable ANSI output - -n, --no-interaction Do not ask any interactive question - -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug + --bootstrap[=BOOTSTRAP] A PHP script that is included before the checks run + --coverage-html[=COVERAGE-HTML] Generate HTML code coverage report [default: false] + --coverage-text[=COVERAGE-TEXT] Generate text code coverage report [default: false] + -f, --filter[=FILTER] Filter the checks that will be run + -i, --iterations=ITERATIONS How many times each check will be run [default: 100] + -j, --log-junit[=LOG-JUNIT] Log check execution to JUnit XML file [default: false] + -t, --log-text[=LOG-TEXT] Log check execution to text file [default: false] + -d, --no-defects[=NO-DEFECTS] Ignore previous defects [default: false] + -h, --help Display this help message + -q, --quiet Do not output any message + -s, --seed[=SEED] Seed the random number generator to get repeatable runs + -V, --version Display this application version + --ansi Force ANSI output + --no-ansi Disable ANSI output + -n, --no-interaction Do not ask any interactive question + -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug The `--bootstrap` parameter can be included in a _phpcheck.xml_ or _phpcheck.xml.dist_ file. See [ours](phpcheck.xml.dist) for an example. diff --git a/bin/phpcheck b/bin/phpcheck index b757cf0..d28fd0f 100755 --- a/bin/phpcheck +++ b/bin/phpcheck @@ -22,8 +22,13 @@ foreach ($autoloaders as $file) { unset($file); use Datashaman\PHPCheck\CheckCommand; +use SebastianBergmann\CodeCoverage\CodeCoverage; use Symfony\Component\Console\Application; +$coverage = new CodeCoverage(); +$coverage->filter()->addDirectoryToWhitelist('./src'); +$coverage->start('phpcheck'); + $application = new Application('phpcheck', CheckCommand::VERSION); $command = new CheckCommand(); $application->add($command); diff --git a/phpcheck.xml.dist b/phpcheck.xml.dist index d88d91d..0a4033e 100644 --- a/phpcheck.xml.dist +++ b/phpcheck.xml.dist @@ -1,11 +1,15 @@ + + diff --git a/src/CheckCommand.php b/src/CheckCommand.php index df42f5b..f711478 100644 --- a/src/CheckCommand.php +++ b/src/CheckCommand.php @@ -28,11 +28,12 @@ protected function configure(): void $this ->setDescription('Run checks.') ->addOption('bootstrap', null, InputOption::VALUE_OPTIONAL, 'A PHP script that is included before the checks run') - ->addOption('coverage-html', null, InputOption::VALUE_OPTIONAL, 'Generate code coverage report in HTML', false) - ->addOption('coverage-text', null, InputOption::VALUE_OPTIONAL, 'Generate code coverage report in text', false) + ->addOption('coverage-html', null, InputOption::VALUE_OPTIONAL, 'Generate HTML code coverage report', false) + ->addOption('coverage-text', null, InputOption::VALUE_OPTIONAL, 'Generate text code coverage report', false) ->addOption('filter', 'f', InputOption::VALUE_OPTIONAL, 'Filter the checks that will be run') ->addOption('iterations', 'i', InputOption::VALUE_REQUIRED, 'How many times each check will be run', Runner::MAX_ITERATIONS) - ->addOption('log-junit', 'j', InputOption::VALUE_OPTIONAL, 'Log check execution in JUnit XML format to file') + ->addOption('log-junit', 'j', InputOption::VALUE_OPTIONAL, 'Log check execution to JUnit XML file', false) + ->addOption('log-text', 't', InputOption::VALUE_OPTIONAL, 'Log check execution to text file', false) ->addOption('no-defects', 'd', InputOption::VALUE_OPTIONAL, 'Ignore previous defects', false) ->addOption('seed', 's', InputOption::VALUE_OPTIONAL, 'Seed the random number generator to get repeatable runs') ->addArgument('path', InputArgument::OPTIONAL, 'File or folder with checks', 'checks'); diff --git a/src/Coverage/Coverage.php b/src/Coverage/Coverage.php new file mode 100644 index 0000000..c7add8e --- /dev/null +++ b/src/Coverage/Coverage.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Datashaman\PHPCheck\Coverage; + +use Datashaman\PHPCheck\Runner; + +abstract class Coverage +{ + protected $input; + + public function __construct(Runner $runner) + { + $this->input = $runner->getInput(); + } + + public function __destruct() + { + global $coverage; + + $coverage->stop(); + } +} diff --git a/src/Coverage/HtmlCoverage.php b/src/Coverage/HtmlCoverage.php new file mode 100644 index 0000000..370e03a --- /dev/null +++ b/src/Coverage/HtmlCoverage.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Datashaman\PHPCheck\Coverage; + +use SebastianBergmann\CodeCoverage\Report\Html\Facade as HtmlFacade; + +class HtmlCoverage extends Coverage +{ + public function __destruct() + { + global $coverage; + + parent::__destruct(); + + $writer = new HtmlFacade(); + $writer->process($coverage, $this->input->getOption('coverage-html')); + } +} diff --git a/src/Coverage/TextCoverage.php b/src/Coverage/TextCoverage.php new file mode 100644 index 0000000..dc9d5b6 --- /dev/null +++ b/src/Coverage/TextCoverage.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Datashaman\PHPCheck\Coverage; + +use SebastianBergmann\CodeCoverage\Report\Text; + +class TextCoverage extends Coverage +{ + public function __destruct() + { + global $coverage; + + parent::__destruct(); + + $writer = new Text(); + + if ($this->input->getOption('coverage-text')) { + $output = $writer->process($coverage, false); + \file_put_contents($this->input->getOption('coverage-text'), $output); + + return; + } + + $color = true; + + if ($this->input->getOption('no-ansi') !== false) { + $color = false; + } + + print $writer->process($coverage, $color); + } +} diff --git a/src/Reporters/HtmlCoverageReporter.php b/src/Reporters/HtmlCoverageReporter.php deleted file mode 100644 index 60e42a8..0000000 --- a/src/Reporters/HtmlCoverageReporter.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Datashaman\PHPCheck\Reporters; - -use Datashaman\PHPCheck\CheckEvents; -use Datashaman\PHPCheck\Events; -use SebastianBergmann\CodeCoverage\CodeCoverage; -use SebastianBergmann\CodeCoverage\Report\Html\Facade as HtmlFacade; - -class HtmlCoverageReporter extends Reporter -{ - protected $coverage; - - public static function getSubscribedEvents(): array - { - return [ - CheckEvents::END => 'onEnd', - CheckEvents::END_ALL => 'onEndAll', - CheckEvents::START => 'onStart', - CheckEvents::START_ALL => 'onStartAll', - ]; - } - - public function onEnd(Events\EndEvent $event): void - { - $this->coverage->stop(); - } - - public function onEndAll(Events\EndAllEvent $event): void - { - $writer = new HtmlFacade(); - $writer->process($this->coverage, $this->input->getOption('coverage-html')); - } - - public function onStart(Events\StartEvent $event): void - { - $this->coverage->start($event->method->getName()); - } - - public function onStartAll(Events\StartAllEvent $event): void - { - if (!$this->input->getOption('coverage-html')) { - $this->output->writeln('You must specify a folder with --coverage-html'); - exit(1); - } - - $this->coverage = new CodeCoverage(); - $this->coverage->filter()->addDirectoryToWhitelist('./src'); - } -} diff --git a/src/Reporters/TextCoverageReporter.php b/src/Reporters/TextCoverageReporter.php deleted file mode 100644 index d2ed882..0000000 --- a/src/Reporters/TextCoverageReporter.php +++ /dev/null @@ -1,61 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Datashaman\PHPCheck\Reporters; - -use Datashaman\PHPCheck\CheckEvents; -use Datashaman\PHPCheck\Events; -use SebastianBergmann\CodeCoverage\CodeCoverage; -use SebastianBergmann\CodeCoverage\Report\Text; - -class TextCoverageReporter extends Reporter -{ - protected $coverage; - - public static function getSubscribedEvents(): array - { - return [ - CheckEvents::END => 'onEnd', - CheckEvents::END_ALL => 'onEndAll', - CheckEvents::START => 'onStart', - CheckEvents::START_ALL => 'onStartAll', - ]; - } - - public function onEnd(Events\EndEvent $event): void - { - $this->coverage->stop(); - } - - public function onEndAll(Events\EndAllEvent $event): void - { - $writer = new Text(); - - - if ($this->input->getOption('coverage-text')) { - $output = $writer->process($this->coverage, false); - file_put_contents($this->input->getOption('coverage-text'), $output); - - return; - } - - print $writer->process($this->coverage, true); - } - - public function onStart(Events\StartEvent $event): void - { - $this->coverage->start($event->method->getName()); - } - - public function onStartAll(Events\StartAllEvent $event): void - { - $this->coverage = new CodeCoverage(); - $this->coverage->filter()->addDirectoryToWhitelist('./src'); - } -} diff --git a/src/Runner.php b/src/Runner.php index c996e4a..6bd6b1e 100644 --- a/src/Runner.php +++ b/src/Runner.php @@ -141,22 +141,39 @@ public function execute(InputInterface $input, OutputInterface $output): void $this->input = $input; $this->output = $output; + if ($input->getOption('coverage-html') !== false) { + if (is_null($input->getOption('coverage-html'))) { + $output->writeln('You must specify a directory for coverage-html'); + exit(1); + } + $coverage = new Coverage\HtmlCoverage($this); + } + + if ($input->getOption('coverage-text') !== false) { + $coverage = new Coverage\TextCoverage($this); + } + $config = $this->getConfig(); $this->maxIterations = (int) $input->getOption('iterations'); - $this->dispatcher->addSubscriber(new Reporters\ConsoleReporter($this)); + $this->dispatcher->addSubscriber(new Subscribers\ConsoleReporter($this)); - if ($input->getOption('coverage-html') !== false) { - $this->dispatcher->addSubscriber(new Reporters\HtmlCoverageReporter($this)); - } - - if ($input->getOption('coverage-text') !== false) { - $this->dispatcher->addSubscriber(new Reporters\TextCoverageReporter($this)); + if ($input->getOption('log-junit') !== false) { + if (is_null($input->getOption('log-junit'))) { + $output->writeln('You must specify a filename for log-junit'); + exit(1); + } + $reporter = new Subscribers\JUnitReporter($this); + $this->dispatcher->addSubscriber($reporter); } - if ($input->getOption('log-junit')) { - $reporter = new Reporters\JUnitReporter($this); + if ($input->getOption('log-text') !== false) { + if (is_null($input->getOption('log-text'))) { + $output->writeln('You must specify a filename for log-text'); + exit(1); + } + $reporter = new Subscribers\TextReporter($this); $this->dispatcher->addSubscriber($reporter); } @@ -168,6 +185,13 @@ public function execute(InputInterface $input, OutputInterface $output): void include_once $bootstrap; } + if (isset($config->subscribers)) { + foreach ($config->subscribers->subscriber as $subscriber) { + $class = (string) $subscriber['class']; + $this->dispatcher->addSubscriber(new $class($this)); + } + } + $event = new Events\StartAllEvent(); $this->dispatcher->dispatch(CheckEvents::START_ALL, $event); @@ -249,16 +273,16 @@ function ($class) { } catch (ExecutionFailure $failure) { $event = new Events\FailureEvent( $method, - $failure->args, - $failure->cause + $failure->getArgs(), + $failure->getCause() ); $this->dispatcher->dispatch(CheckEvents::FAILURE, $event); $status = 'FAILURE'; } catch (ExecutionError $error) { $event = new Events\ErrorEvent( $method, - $error->args, - $error->cause + $error->getArgs(), + $error->getCause() ); $this->dispatcher->dispatch(CheckEvents::ERROR, $event); $status = 'ERROR'; diff --git a/src/Reporters/ConsoleReporter.php b/src/Subscribers/ConsoleReporter.php similarity index 99% rename from src/Reporters/ConsoleReporter.php rename to src/Subscribers/ConsoleReporter.php index d3df629..c6b14be 100644 --- a/src/Reporters/ConsoleReporter.php +++ b/src/Subscribers/ConsoleReporter.php @@ -9,7 +9,7 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Datashaman\PHPCheck\Reporters; +namespace Datashaman\PHPCheck\Subscribers; use Datashaman\PHPCheck\CheckCommand; use Datashaman\PHPCheck\CheckEvents; diff --git a/src/Reporters/JUnitReporter.php b/src/Subscribers/JUnitReporter.php similarity index 95% rename from src/Reporters/JUnitReporter.php rename to src/Subscribers/JUnitReporter.php index d4f6535..6cbb522 100644 --- a/src/Reporters/JUnitReporter.php +++ b/src/Subscribers/JUnitReporter.php @@ -9,7 +9,7 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Datashaman\PHPCheck\Reporters; +namespace Datashaman\PHPCheck\Subscribers; use Datashaman\PHPCheck\CheckEvents; use Datashaman\PHPCheck\Events; @@ -67,7 +67,7 @@ public function onFailure(Events\FailureEvent $event): void ); } - public function onEndAll(Events\EndAllEvent $event): void + public function onEndAll(): void { $this->testsuite->asXML($this->input->getOption('log-junit')); } diff --git a/src/Subscribers/Reporter.php b/src/Subscribers/Reporter.php new file mode 100644 index 0000000..7c39673 --- /dev/null +++ b/src/Subscribers/Reporter.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Datashaman\PHPCheck\Subscribers; + +abstract class Reporter extends Subscriber +{ +} diff --git a/src/Reporters/Reporter.php b/src/Subscribers/Subscriber.php similarity index 89% rename from src/Reporters/Reporter.php rename to src/Subscribers/Subscriber.php index c7c28b1..ae4db60 100644 --- a/src/Reporters/Reporter.php +++ b/src/Subscribers/Subscriber.php @@ -1,6 +1,4 @@ - + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Datashaman\PHPCheck\Subscribers; + +use Datashaman\PHPCheck\CheckEvents; +use Datashaman\PHPCheck\Events; +use ReflectionClass; + +class TextReporter extends Reporter +{ + protected $file; + + public static function getSubscribedEvents(): array + { + return [ + CheckEvents::START_ALL => 'onStartAll', + CheckEvents::START => 'onStart', + CheckEvents::SUCCESS => 'onSuccess', + CheckEvents::ERROR => 'onError', + CheckEvents::FAILURE => 'onFailure', + CheckEvents::END_ALL => 'onEndAll', + ]; + } + + public function onStartAll(Events\StartAllEvent $event): void + { + $this->file = fopen($this->input->getOption('log-text'), 'a'); + $this->report('onStartAll', $event); + } + + public function onEndAll(Events\EndAllEvent $event): void + { + $this->report('onEndAll', $event); + + if (is_resource($this->file)) { + fclose($this->file); + } + } + + public function __call(string $name, array $args) + { + $this->report($name, ...$args); + } + + protected function report(string $name, Events\Event $event) + { + $shortName = preg_replace( + '/Event$/', + '', + (new ReflectionClass($event))->getShortName() + ); + $message = sprintf( + '%s [%-10s]', + strftime('%F %T', (int) $event->time), + strtoupper($shortName) + ); + + if ( + in_array( + $name, + [ + 'onError', + 'onFailure', + 'onStart', + 'onSuccess', + ] + ) + ) { + $message .= ' ' . $this->getMethodSignature($event->method); + } + + if ( + $event instanceof Events\ResultEvent + ) { + if (!is_null($event->args)) { + $args = preg_replace( + [ + '/^\[/', + '/\]$/', + ], + '', + json_encode($event->args) + ); + + $message .= '(' . $args . ')'; + } + + if ($event->cause) { + $message .= ' caused ' . get_class($event->cause) . '("' . $event->cause->getMessage() . '")'; + } + } + + $message .= "\n"; + + fwrite($this->file, $message); + } +}