diff --git a/CHANGELOG.md b/CHANGELOG.md index 101b8c1..fd45854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - **[NEW]** Introduced focused specs for powerful test isolation ([#199], [#204], [#197], [#194], [#188], [#185], [#181]) +- **[NEW]** Support for multiple reporters ([#202], [#186]) - **[IMPROVED]** Stack trace output now excludes irrelevant information ([#203], [#170]) - **[FIXED]** Fixed error handler signature ([#198] - thanks [@YuraLukashik]) @@ -14,12 +15,14 @@ [#171]: https://github.com/peridot-php/peridot/issues/171 [#181]: https://github.com/peridot-php/peridot/issues/181 [#185]: https://github.com/peridot-php/peridot/issues/185 +[#186]: https://github.com/peridot-php/peridot/issues/186 [#188]: https://github.com/peridot-php/peridot/pull/188 [#194]: https://github.com/peridot-php/peridot/pull/194 [#197]: https://github.com/peridot-php/peridot/pull/197 [#198]: https://github.com/peridot-php/peridot/pull/198 [#199]: https://github.com/peridot-php/peridot/pull/199 [#201]: https://github.com/peridot-php/peridot/pull/201 +[#202]: https://github.com/peridot-php/peridot/pull/202 [#203]: https://github.com/peridot-php/peridot/pull/203 [#204]: https://github.com/peridot-php/peridot/pull/204 diff --git a/specs/command.spec.php b/specs/command.spec.php index 3d113ea..86c198b 100644 --- a/specs/command.spec.php +++ b/specs/command.spec.php @@ -79,12 +79,13 @@ }); }); - context('when passing a reporter name', function() { - it('should set the reporter name on the configuration object', function() { - $this->factory->register('test', 'desc', function() {}); - $this->command->run(new ArrayInput(['-r' => 'test'], $this->definition), $this->output); - $reporter = $this->configuration->getReporter(); - assert($reporter == 'test', 'reporter name should be "test"'); + context('when passing reporter names', function() { + it('should set the reporter names on the configuration object', function() { + $this->factory->register('test-a', 'desc', function() {}); + $this->factory->register('test-b', 'desc', function() {}); + $this->command->run(new ArrayInput(['-r' => ['test-a', 'test-b']], $this->definition), $this->output); + $reporters = $this->configuration->getReporters(); + assert($reporters === ['test-a', 'test-b'], 'reporter names should be ["test-a", "test-b"]'); }); }); diff --git a/specs/composite-reporter.spec.php b/specs/composite-reporter.spec.php new file mode 100644 index 0000000..78d16e0 --- /dev/null +++ b/specs/composite-reporter.spec.php @@ -0,0 +1,63 @@ +configuration = new Configuration(); + $this->output = new BufferedOutput(); + $this->emitter = new EventEmitter(); + $this->reporterA = new FakeReporter($this->configuration, new BufferedOutput(), $this->emitter); + $this->reporterB = new FakeReporter($this->configuration, new NullOutput(), $this->emitter); + $this->reporterC = new FakeReporter($this->configuration, new BufferedOutput(), $this->emitter); + $this->reporters = [$this->reporterA, $this->reporterB, $this->reporterC]; + $this->reporter = new CompositeReporter($this->reporters, $this->configuration, $this->output, $this->emitter); + }); + + context('->setEventEmitter()', function() { + beforeEach(function () { + $this->emitter2 = new EventEmitter(); + $this->reporter->setEventEmitter($this->emitter2); + }); + + it('should set the event emitter', function() { + assert($this->reporter->getEventEmitter() === $this->emitter2, 'should be the same event emitter'); + }); + + it('should set the event emitter for each wrapped reporter', function() { + assert($this->reporterA->getEventEmitter() === $this->emitter2, 'should be the same event emitter'); + assert($this->reporterB->getEventEmitter() === $this->emitter2, 'should be the same event emitter'); + assert($this->reporterC->getEventEmitter() === $this->emitter2, 'should be the same event emitter'); + }); + }); + + context('when runner.end is emitted', function() { + it('should include an error number and the test description', function() { + $this->emitter->emit('runner.end', [1.0]); + $content = $this->output->fetch(); + $expected = implode([ + PHP_EOL, + spl_object_hash($this->reporterA), + PHP_EOL, + PHP_EOL, + spl_object_hash($this->reporterC), + PHP_EOL + ]); + assert($content === $expected, 'output should contain wrapped reporter output'); + }); + }); + +}); + +class FakeReporter extends AbstractBaseReporter +{ + public function init() + { + $this->getOutput()->writeln(spl_object_hash($this)); + } +} diff --git a/specs/configuration.spec.php b/specs/configuration.spec.php index f71d3aa..ef108d4 100644 --- a/specs/configuration.spec.php +++ b/specs/configuration.spec.php @@ -124,6 +124,34 @@ }); }); + describe('->setReporter()', function() { + it('should set both reporter and reporters', function() { + $this->configuration->setReporter('reporter-a'); + + assert($this->configuration->getReporter() === 'reporter-a', 'should have set reporter'); + assert($this->configuration->getReporters() === ['reporter-a'], 'should have set reporters'); + }); + }); + + describe('->setReporters()', function() { + it('should set both reporter and reporters', function() { + $this->configuration->setReporters(['reporter-a', 'reporter-b']); + + assert($this->configuration->getReporter() === 'reporter-a', 'should have set reporter'); + assert($this->configuration->getReporters() === ['reporter-a', 'reporter-b'], 'should have set reporters'); + }); + + it('should disallow setting an empty reporters array', function() { + $exception = null; + try { + $this->configuration->setReporters([]); + } catch (InvalidArgumentException $e) { + $exception = $e; + } + assert(!is_null($exception), 'expected exception to be thrown'); + }); + }); + describe('->enableColorsExplicit()', function() { it('should enable colors when explicit is set', function() { $this->configuration->enableColorsExplicit(); diff --git a/specs/reporter-factory.spec.php b/specs/reporter-factory.spec.php index b287dad..68412ab 100644 --- a/specs/reporter-factory.spec.php +++ b/specs/reporter-factory.spec.php @@ -3,6 +3,7 @@ use Peridot\Configuration; use Peridot\Core\Suite; use Peridot\Reporter\AnonymousReporter; +use Peridot\Reporter\CompositeReporter; use Peridot\Reporter\ReporterFactory; use Peridot\Reporter\SpecReporter; use Peridot\Runner\Runner; @@ -55,6 +56,55 @@ }); }); + describe('->createComposite()', function() { + context('using valid reporter names', function() { + it('should return a composite of the named reporters', function() { + $this->factory->register('spec2', 'desc', function($reporter) {}); + $reporter = $this->factory->createComposite(['spec', 'spec2']); + $reporters = $reporter->getReporters(); + assert($reporter instanceof CompositeReporter, 'should create CompositeReporter'); + assert($reporters[0] instanceof SpecReporter, 'first reporter should be a SpecReporter'); + }); + }); + + context('using valid names with invalid factories', function() { + it('should throw an exception', function() { + $this->factory->register('nope', 'doesnt work', 'Not\A\Class'); + $exception = null; + try { + $this->factory->createComposite(['spec', 'nope']); + } catch (RuntimeException $e) { + $exception = $e; + } + assert(!is_null($exception), 'exception should have been thrown'); + }); + }); + + context('using invalid names', function() { + it('should throw an exception', function() { + $exception = null; + try { + $this->factory->createComposite(['spec', 'nope']); + } catch (RuntimeException $e) { + $exception = $e; + } + assert(!is_null($exception), 'exception should have been thrown'); + }); + }); + + context('using an empty name list', function() { + it('should throw an exception', function() { + $exception = null; + try { + $this->factory->createComposite([]); + } catch (InvalidArgumentException $e) { + $exception = $e; + } + assert(!is_null($exception), 'exception should have been thrown'); + }); + }); + }); + describe('->getReporters()', function() { it("should return an array of reporter information", function() { $reporters = $this->factory->getReporters(); diff --git a/src/Configuration.php b/src/Configuration.php index 37b5fb0..f566568 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -35,9 +35,9 @@ class Configuration protected $grep = '*.spec.php'; /** - * @var string + * @var array */ - protected $reporter = 'spec'; + protected $reporters = ['spec']; /** * @var string @@ -137,7 +137,7 @@ public function getSkipPattern() */ public function setReporter($reporter) { - return $this->write('reporter', $reporter); + return $this->writeReporters([$reporter]); } /** @@ -147,7 +147,32 @@ public function setReporter($reporter) */ public function getReporter() { - return $this->reporter; + return $this->reporters[0]; + } + + /** + * Set the names of the reporters to use + * + * @param array $reporters + * @return $this + */ + public function setReporters(array $reporters) + { + if (empty($reporters)) { + throw new \InvalidArgumentException('Reporters cannot be empty.'); + } + + return $this->writeReporters($reporters); + } + + /** + * Return the names of the reporters configured for use + * + * @return array + */ + public function getReporters() + { + return $this->reporters; } /** @@ -328,4 +353,18 @@ protected function normalizeRegexPattern($pattern) return '~\b' . preg_quote($pattern, '~') . '\b~'; } + + /** + * Write the reporters and persist them to the current environment. + * + * @param array $reporters + * @return $this + */ + protected function writeReporters(array $reporters) + { + $this->reporters = $reporters; + putenv('PERIDOT_REPORTER=' . $reporters[0]); + putenv('PERIDOT_REPORTERS=' . implode(',', $reporters)); + return $this; + } } diff --git a/src/Console/Command.php b/src/Console/Command.php index 6b7f168..47e0da4 100644 --- a/src/Console/Command.php +++ b/src/Console/Command.php @@ -139,10 +139,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return 0; } - if ($reporter = $input->getOption('reporter')) { - $this->configuration->setReporter($reporter); - } - + $this->configuration->setReporters($input->getOption('reporter')); $this->eventEmitter->emit('peridot.load', [$this, $this->configuration]); return $this->getResult(); @@ -171,7 +168,7 @@ protected function getResult() { $result = new TestResult($this->eventEmitter); $this->getLoader()->load($this->configuration->getPath()); - $this->factory->create($this->configuration->getReporter()); + $this->factory->createComposite($this->configuration->getReporters()); $this->runner->run($result); if ($result->getFailureCount() > 0) { diff --git a/src/Console/InputDefinition.php b/src/Console/InputDefinition.php index 072c5f5..cc86834 100644 --- a/src/Console/InputDefinition.php +++ b/src/Console/InputDefinition.php @@ -26,7 +26,7 @@ public function __construct() $this->addOption(new InputOption('grep', 'g', InputOption::VALUE_REQUIRED, 'Run tests with filenames matching (default: *.spec.php)')); $this->addOption(new InputOption('no-colors', 'C', InputOption::VALUE_NONE, 'Disable output colors')); $this->addOption(new InputOption('--force-colors', null, InputOption::VALUE_NONE, 'Force output colors')); - $this->addOption(new InputOption('reporter', 'r', InputOption::VALUE_REQUIRED, 'Select which reporter to use (default: spec)')); + $this->addOption(new InputOption('reporter', 'r', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Select which reporter(s) to use', ['spec'])); $this->addOption(new InputOption('bail', 'b', InputOption::VALUE_NONE, 'Stop on failure')); $this->addOption(new InputOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'A php file containing peridot configuration')); $this->addOption(new InputOption('reporters', null, InputOption::VALUE_NONE, 'List all available reporters')); diff --git a/src/Reporter/CompositeReporter.php b/src/Reporter/CompositeReporter.php new file mode 100644 index 0000000..138b663 --- /dev/null +++ b/src/Reporter/CompositeReporter.php @@ -0,0 +1,86 @@ +reporters = $reporters; + + parent::__construct($configuration, $output, $eventEmitter); + } + + /** + * Return the wrapped reporters. + * + * @return array + */ + public function getReporters() + { + return $this->reporters; + } + + /** + * Initialize reporter. Setup and listen for runner events. + * + * @return void + */ + public function init() + { + $this->eventEmitter->on('runner.end', [$this, 'onRunnerEnd']); + } + + /** + * @param \Evenement\EventEmitterInterface $eventEmitter + */ + public function setEventEmitter(EventEmitterInterface $eventEmitter) + { + parent::setEventEmitter($eventEmitter); + + array_map(function (ReporterInterface $reporter) use ($eventEmitter) { + $reporter->setEventEmitter($eventEmitter); + }, $this->reporters); + + return $this; + } + + public function onRunnerEnd() + { + $stdout = $this->getOutput(); + + array_map(function (ReporterInterface $reporter) use ($stdout) { + $output = $reporter->getOutput(); + + if ($output instanceof BufferedOutput && $content = $output->fetch()) { + $stdout->writeln(''); + $stdout->write($content); + } + }, $this->reporters); + } +} diff --git a/src/Reporter/ReporterFactory.php b/src/Reporter/ReporterFactory.php index d9750a1..e2f814c 100644 --- a/src/Reporter/ReporterFactory.php +++ b/src/Reporter/ReporterFactory.php @@ -4,6 +4,7 @@ use Evenement\EventEmitterInterface; use Peridot\Configuration; use Peridot\Core\HasEventEmitterTrait; +use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\OutputInterface; /** @@ -62,19 +63,32 @@ public function __construct( */ public function create($name) { - $factory = $this->getReporterFactory($name); - - $isClass = is_string($factory) && class_exists($factory); - - if ($isClass) { - return new $factory($this->configuration, $this->output, $this->eventEmitter); - } + return $this->createWithOutput($this->output, $name); + } - if (is_callable($factory)) { - return new AnonymousReporter($factory, $this->configuration, $this->output, $this->eventEmitter); + /** + * Return an instance of the named reporter + * + * @param $name + * @return \Peridot\Reporter\AbstractBaseReporter + */ + public function createComposite(array $names) + { + if (empty($names)) { + throw new \InvalidArgumentException('Reporter names cannot be empty.'); } - throw new \RuntimeException("Reporter class could not be created"); + return new CompositeReporter( + array_merge( + [$this->createWithOutput($this->output, array_shift($names))], + array_map(function ($name) { + return $this->createWithOutput(new BufferedOutput(), $name); + }, $names) + ), + $this->configuration, + $this->output, + $this->eventEmitter + ); } /** @@ -126,4 +140,20 @@ public function getReporters() { return $this->reporters; } + + private function createWithOutput(OutputInterface $output, $name) + { + $factory = $this->getReporterFactory($name); + $isClass = is_string($factory) && class_exists($factory); + + if ($isClass) { + return new $factory($this->configuration, $output, $this->eventEmitter); + } + + if (is_callable($factory)) { + return new AnonymousReporter($factory, $this->configuration, $output, $this->eventEmitter); + } + + throw new \RuntimeException("Reporter class could not be created"); + } }