'beforeSuite', Events::SUITE_AFTER => 'afterSuite', Events::TEST_START => 'startTest', Events::TEST_END => 'endTest', Events::STEP_BEFORE => 'beforeStep', Events::STEP_AFTER => 'afterStep', Events::TEST_SUCCESS => 'testSuccess', Events::TEST_FAIL => 'testFail', Events::TEST_ERROR => 'testError', Events::TEST_INCOMPLETE => 'testIncomplete', Events::TEST_SKIPPED => 'testSkipped', Events::TEST_FAIL_PRINT => 'printFail', Events::RESULT_PRINT_AFTER => 'afterResult', ]; /** * @var Step */ protected $metaStep; /** * @var Message */ protected $message = null; protected $steps = true; protected $debug = false; protected $ansi = true; protected $silent = false; protected $lastTestFailed = false; protected $printedTest = null; protected $rawStackTrace = false; protected $traceLength = 5; protected $width; /** * @var OutputInterface */ protected $output; protected $conditionalFails = []; protected $failedStep; protected $reports = []; protected $namespace = ''; protected $chars = ['success' => '+', 'fail' => 'x', 'of' => ':']; protected $options = [ 'debug' => false, 'ansi' => false, 'steps' => true, 'verbosity' => 0, 'xml' => null, 'html' => null, 'tap' => null, 'json' => null, ]; /** * @var MessageFactory */ protected $messageFactory; public function __construct($options) { $this->prepareOptions($options); $this->output = new Output($options); $this->messageFactory = new MessageFactory($this->output); if ($this->debug) { Debug::setOutput($this->output); } $this->detectWidth(); if ($this->options['ansi'] && !$this->isWin()) { $this->chars['success'] = '✔'; $this->chars['fail'] = '✖'; } foreach (['html', 'xml', 'tap', 'json'] as $report) { if (!$this->options[$report]) { continue; } $path = $this->absolutePath($this->options[$report]); $this->reports[] = sprintf( "- %s report generated in file://%s", strtoupper($report), $path ); } } // triggered for scenario based tests: cept, cest public function beforeSuite(SuiteEvent $e) { $this->namespace = ""; $settings = $e->getSettings(); if (isset($settings['namespace'])) { $this->namespace = $settings['namespace']; } $this->message("%s Tests (%d) ") ->with(ucfirst($e->getSuite()->getName()), $e->getSuite()->count()) ->style('bold') ->width($this->width, '-') ->prepend("\n") ->writeln(); if ($e->getSuite() instanceof Suite) { $message = $this->message( implode( ', ', array_map( function ($module) { return $module->_getName(); }, $e->getSuite()->getModules() ) ) ); $message->style('info') ->prepend('Modules: ') ->writeln(OutputInterface::VERBOSITY_VERBOSE); } $this->message('')->width($this->width, '-')->writeln(OutputInterface::VERBOSITY_VERBOSE); } // triggered for all tests public function startTest(TestEvent $e) { $this->conditionalFails = []; $test = $e->getTest(); $this->printedTest = $test; $this->message = null; if (!$this->output->isInteractive() and !$this->isDetailed($test)) { return; } $this->writeCurrentTest($test); if ($this->isDetailed($test)) { $this->output->writeln(''); $this->message(Descriptor::getTestSignature($test)) ->style('info') ->prepend('Signature: ') ->writeln(); $this->message(codecept_relative_path(Descriptor::getTestFullName($test))) ->style('info') ->prepend('Test: ') ->writeln(); if ($this->steps) { $this->message('Scenario --')->style('comment')->writeln(); $this->output->waitForDebugOutput = false; } } } public function afterStep(StepEvent $e) { $step = $e->getStep(); if (!$step->hasFailed()) { return; } if ($step instanceof Step\ConditionalAssertion) { $this->conditionalFails[] = $step; return; } $this->failedStep = $step; } /** * @param PrintResultEvent $event */ public function afterResult(PrintResultEvent $event) { $result = $event->getResult(); if ($result->skippedCount() + $result->notImplementedCount() > 0 and $this->options['verbosity'] < OutputInterface::VERBOSITY_VERBOSE) { $this->output->writeln("run with `-v` to get more info about skipped or incomplete tests"); } foreach ($this->reports as $message) { $this->output->writeln($message); } } private function absolutePath($path) { if ((strpos($path, '/') === 0) or (strpos($path, ':') === 1)) { // absolute path return $path; } return codecept_output_dir() . $path; } public function testSuccess(TestEvent $e) { if ($this->isDetailed($e->getTest())) { $this->message('PASSED')->center(' ')->style('ok')->append("\n")->writeln(); return; } $this->writelnFinishedTest($e, $this->message($this->chars['success'])->style('ok')); } public function endTest(TestEvent $e) { $this->metaStep = null; $this->printedTest = null; } public function testFail(FailEvent $e) { if ($this->isDetailed($e->getTest())) { $this->message('FAIL')->center(' ')->style('fail')->append("\n")->writeln(); return; } $this->writelnFinishedTest($e, $this->message($this->chars['fail'])->style('fail')); } public function testError(FailEvent $e) { if ($this->isDetailed($e->getTest())) { $this->message('ERROR')->center(' ')->style('fail')->append("\n")->writeln(); return; } $this->writelnFinishedTest($e, $this->message('E')->style('fail')); } public function testSkipped(FailEvent $e) { if ($this->isDetailed($e->getTest())) { $msg = $e->getFail()->getMessage(); $this->message('SKIPPED')->append($msg ? ": $msg" : '')->center(' ')->style('pending')->writeln(); return; } $this->writelnFinishedTest($e, $this->message('S')->style('pending')); } public function testIncomplete(FailEvent $e) { if ($this->isDetailed($e->getTest())) { $msg = $e->getFail()->getMessage(); $this->message('INCOMPLETE')->append($msg ? ": $msg" : '')->center(' ')->style('pending')->writeln(); return; } $this->writelnFinishedTest($e, $this->message('I')->style('pending')); } protected function isDetailed($test) { if ($test instanceof ScenarioDriven && $this->steps) { return true; } return false; } public function beforeStep(StepEvent $e) { if (!$this->steps or !$e->getTest() instanceof ScenarioDriven) { return; } $metaStep = $e->getStep()->getMetaStep(); if ($metaStep and $this->metaStep != $metaStep) { $this->message(' ' . $metaStep->getPrefix()) ->style('bold') ->append($metaStep->__toString()) ->writeln(); } $this->metaStep = $metaStep; $this->printStep($e->getStep()); } private function printStep(Step $step) { if ($step instanceof Comment and $step->__toString() == '') { return; // don't print empty comments } $msg = $this->message(' '); if ($this->metaStep) { $msg->append(' '); } $msg->append($step->getPrefix()); $prefixLength = $msg->getLength(); if (!$this->metaStep) { $msg->style('bold'); } $maxLength = $this->width - $prefixLength; $msg->append(OutputFormatter::escape($step->toString($maxLength))); if ($this->metaStep) { $msg->style('info'); } $msg->writeln(); } public function afterSuite(SuiteEvent $e) { $this->message()->width($this->width, '-')->writeln(); $messages = Notification::all(); foreach (array_count_values($messages) as $message => $count) { if ($count > 1) { $message = $count . 'x ' . $message; } $this->output->notification($message); } } public function printFail(FailEvent $e) { $failedTest = $e->getTest(); $fail = $e->getFail(); $this->output->write($e->getCount() . ") "); $this->writeCurrentTest($failedTest, false); $this->output->writeln(''); $this->message(" Test ") ->append(codecept_relative_path(Descriptor::getTestFullName($failedTest))) ->write(); if ($failedTest instanceof ScenarioDriven) { $this->printScenarioFail($failedTest, $fail); return; } $this->printException($fail); $this->printExceptionTrace($fail); } public function printException($e, $cause = null) { if ($e instanceof \PHPUnit_Framework_SkippedTestError or $e instanceof \PHPUnit_Framework_IncompleteTestError) { if ($e->getMessage()) { $this->message(OutputFormatter::escape($e->getMessage()))->prepend("\n")->writeln(); } return; } $class = $e instanceof \PHPUnit_Framework_ExceptionWrapper ? $e->getClassname() : get_class($e); if (strpos($class, 'Codeception\Exception') === 0) { $class = substr($class, strlen('Codeception\Exception\\')); } $this->output->writeln(''); $message = $this->message(OutputFormatter::escape($e->getMessage())); if ($e instanceof \PHPUnit_Framework_ExpectationFailedException) { $comparisonFailure = $e->getComparisonFailure(); if ($comparisonFailure) { $message->append($this->messageFactory->prepareComparisonFailureMessage($comparisonFailure)); } } $isFailure = $e instanceof \PHPUnit_Framework_AssertionFailedError || $class === 'PHPUnit_Framework_ExpectationFailedException' || $class === 'PHPUnit_Framework_AssertionFailedError'; if (!$isFailure) { $message->prepend("[$class] ")->block('error'); } if ($isFailure && $cause) { $cause = OutputFormatter::escape(ucfirst($cause)); $message->prepend(" Step $cause\n Fail "); } $message->writeln(); } public function printScenarioFail(ScenarioDriven $failedTest, $fail) { if ($this->conditionalFails) { $failedStep = (string) array_shift($this->conditionalFails); } else { $failedStep = (string) $failedTest->getScenario()->getMetaStep(); if ($failedStep === '') { $failedStep = (string)$this->failedStep; } } $this->printException($fail, $failedStep); $this->printScenarioTrace($failedTest); if ($this->output->getVerbosity() == OutputInterface::VERBOSITY_DEBUG) { $this->printExceptionTrace($fail); return; } if (!$fail instanceof \PHPUnit_Framework_AssertionFailedError) { $this->printExceptionTrace($fail); return; } } public function printExceptionTrace(\Exception $e) { static $limit = 10; if ($e instanceof \PHPUnit_Framework_SkippedTestError or $e instanceof \PHPUnit_Framework_IncompleteTestError) { return; } if ($this->rawStackTrace) { $this->message(OutputFormatter::escape(\PHPUnit_Util_Filter::getFilteredStacktrace($e, true, false)))->writeln(); return; } $trace = \PHPUnit_Util_Filter::getFilteredStacktrace($e, false); $i = 0; foreach ($trace as $step) { if ($i >= $limit) { break; } $i++; $message = $this->message($i)->prepend('#')->width(4); if (!isset($step['file'])) { foreach (['class', 'type', 'function'] as $info) { if (!isset($step[$info])) { continue; } $message->append($step[$info]); } $message->writeln(); continue; } $message->append($step['file'] . ':' . $step['line']); $message->writeln(); } $prev = $e->getPrevious(); if ($prev) { $this->printExceptionTrace($prev); } } /** * @param $failedTest */ public function printScenarioTrace(ScenarioDriven $failedTest) { $trace = array_reverse($failedTest->getScenario()->getSteps()); $length = $stepNumber = count($trace); if (!$length) { return; } $this->message("\nScenario Steps:\n")->style('comment')->writeln(); foreach ($trace as $step) { /** * @var $step Step */ if (!$step->__toString()) { continue; } $message = $this ->message($stepNumber) ->prepend(' ') ->width(strlen($length)) ->append(". "); $message->append(OutputFormatter::escape($step->getPhpCode($this->width - $message->getLength()))); if ($step->hasFailed()) { $message->style('bold'); } $line = $step->getLine(); if ($line and (!$step instanceof Comment)) { $message->append(" at $line"); } $stepNumber--; $message->writeln(); if (($length - $stepNumber - 1) >= $this->traceLength) { break; } } $this->output->writeln(""); } public function detectWidth() { $this->width = 60; if (!$this->isWin() && (php_sapi_name() === "cli") && (getenv('TERM')) && (getenv('TERM') != 'unknown') ) { // try to get terminal width from ENV variable (bash), see also https://github.com/Codeception/Codeception/issues/3788 if (getenv('COLUMNS')) { $this->width = getenv('COLUMNS'); } else { $this->width = (int) (`command -v tput >> /dev/null 2>&1 && tput cols`) - 2; } } elseif ($this->isWin() && (php_sapi_name() === "cli")) { exec('mode con', $output); if (isset($output[4])) { preg_match('/^ +.* +(\d+)$/', $output[4], $matches); if (!empty($matches[1])) { $this->width = (int) $matches[1]; } } } return $this->width; } private function isWin() { return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; } /** * @param \PHPUnit_Framework_SelfDescribing $test * @param bool $inProgress */ protected function writeCurrentTest(\PHPUnit_Framework_SelfDescribing $test, $inProgress = true) { $prefix = ($this->output->isInteractive() and !$this->isDetailed($test) and $inProgress) ? '- ' : ''; $testString = Descriptor::getTestAsString($test); $testString = preg_replace('~^([^:]+):\s~', "$1{$this->chars['of']} ", $testString); $this ->message($testString) ->prepend($prefix) ->write(); } protected function writelnFinishedTest(TestEvent $event, Message $result) { $test = $event->getTest(); if ($this->isDetailed($test)) { return; } if ($this->output->isInteractive()) { $this->output->write("\x0D"); } $result->append(' ')->write(); $this->writeCurrentTest($test, false); $conditionalFailsMessage = ""; $numFails = count($this->conditionalFails); if ($numFails == 1) { $conditionalFailsMessage = "[F]"; } elseif ($numFails) { $conditionalFailsMessage = "{$numFails}x[F]"; } $conditionalFailsMessage = "$conditionalFailsMessage "; $this->message($conditionalFailsMessage)->write(); $this->writeTimeInformation($event); $this->output->writeln(''); } /** * @param $string * @return Message */ private function message($string = '') { return $this->messageFactory->message($string); } /** * @param TestEvent $event */ protected function writeTimeInformation(TestEvent $event) { $time = $event->getTime(); if ($time) { $this ->message(number_format(round($time, 2), 2)) ->prepend('(') ->append('s)') ->style('info') ->write(); } } /** * @param $options */ private function prepareOptions($options) { $this->options = array_merge($this->options, $options); $this->debug = $this->options['debug'] || $this->options['verbosity'] >= OutputInterface::VERBOSITY_VERY_VERBOSE; $this->steps = $this->debug || $this->options['steps']; $this->rawStackTrace = ($this->options['verbosity'] === OutputInterface::VERBOSITY_DEBUG); } }