setDefinition([ new InputArgument('suite', InputArgument::OPTIONAL, 'suite to be tested'), new InputArgument('test', InputArgument::OPTIONAL, 'test to be run'), new InputOption('override', 'o', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Override config values'), new InputOption('ext', 'e', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Run with extension enabled'), new InputOption('report', '', InputOption::VALUE_NONE, 'Show output in compact style'), new InputOption('html', '', InputOption::VALUE_OPTIONAL, 'Generate html with results', 'report.html'), new InputOption('xml', '', InputOption::VALUE_OPTIONAL, 'Generate JUnit XML Log', 'report.xml'), new InputOption('tap', '', InputOption::VALUE_OPTIONAL, 'Generate Tap Log', 'report.tap.log'), new InputOption('json', '', InputOption::VALUE_OPTIONAL, 'Generate Json Log', 'report.json'), new InputOption('colors', '', InputOption::VALUE_NONE, 'Use colors in output'), new InputOption( 'no-colors', '', InputOption::VALUE_NONE, 'Force no colors in output (useful to override config file)' ), new InputOption('silent', '', InputOption::VALUE_NONE, 'Only outputs suite names and final results'), new InputOption('steps', '', InputOption::VALUE_NONE, 'Show steps in output'), new InputOption('debug', 'd', InputOption::VALUE_NONE, 'Show debug and scenario output'), new InputOption( 'coverage', '', InputOption::VALUE_OPTIONAL, 'Run with code coverage' ), new InputOption( 'coverage-html', '', InputOption::VALUE_OPTIONAL, 'Generate CodeCoverage HTML report in path' ), new InputOption( 'coverage-xml', '', InputOption::VALUE_OPTIONAL, 'Generate CodeCoverage XML report in file' ), new InputOption( 'coverage-text', '', InputOption::VALUE_OPTIONAL, 'Generate CodeCoverage text report in file' ), new InputOption( 'coverage-crap4j', '', InputOption::VALUE_OPTIONAL, 'Generate CodeCoverage report in Crap4J XML format' ), new InputOption('no-exit', '', InputOption::VALUE_NONE, 'Don\'t finish with exit code'), new InputOption( 'group', 'g', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Groups of tests to be executed' ), new InputOption( 'skip', 's', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Skip selected suites' ), new InputOption( 'skip-group', 'x', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Skip selected groups' ), new InputOption( 'env', '', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Run tests in selected environments.' ), new InputOption('fail-fast', 'f', InputOption::VALUE_NONE, 'Stop after first failure'), new InputOption('no-rebuild', '', InputOption::VALUE_NONE, 'Do not rebuild actor classes on start'), ]); parent::configure(); } public function getDescription() { return 'Runs the test suites'; } /** * Executes Run * * @param \Symfony\Component\Console\Input\InputInterface $input * @param \Symfony\Component\Console\Output\OutputInterface $output * @return int|null|void * @throws \RuntimeException */ public function execute(InputInterface $input, OutputInterface $output) { $this->ensureCurlIsAvailable(); $this->options = $input->getOptions(); $this->output = $output; // load config $config = $this->getGlobalConfig(); // update config from options if (count($this->options['override'])) { $config = $this->overrideConfig($this->options['override']); } if ($this->options['ext']) { $config = $this->enableExtensions($this->options['ext']); } if (!$this->options['colors']) { $this->options['colors'] = $config['settings']['colors']; } if (!$this->options['silent']) { $this->output->writeln( Codecept::versionString() . "\nPowered by " . \PHPUnit_Runner_Version::getVersionString() ); } if ($this->options['debug']) { $this->output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); } $userOptions = array_intersect_key($this->options, array_flip($this->passedOptionKeys($input))); $userOptions = array_merge( $userOptions, $this->booleanOptions($input, [ 'xml' => 'report.xml', 'html' => 'report.html', 'json' => 'report.json', 'tap' => 'report.tap.log', 'coverage' => 'coverage.serialized', 'coverage-xml' => 'coverage.xml', 'coverage-html' => 'coverage', 'coverage-text' => 'coverage.txt', 'coverage-crap4j' => 'crap4j.xml']) ); $userOptions['verbosity'] = $this->output->getVerbosity(); $userOptions['interactive'] = !$input->hasParameterOption(['--no-interaction', '-n']); $userOptions['ansi'] = (!$input->hasParameterOption('--no-ansi') xor $input->hasParameterOption('ansi')); if ($this->options['no-colors'] || !$userOptions['ansi']) { $userOptions['colors'] = false; } if ($this->options['group']) { $userOptions['groups'] = $this->options['group']; } if ($this->options['skip-group']) { $userOptions['excludeGroups'] = $this->options['skip-group']; } if ($this->options['report']) { $userOptions['silent'] = true; } if ($this->options['coverage-xml'] or $this->options['coverage-html'] or $this->options['coverage-text'] or $this->options['coverage-crap4j']) { $this->options['coverage'] = true; } if (!$userOptions['ansi'] && $input->getOption('colors')) { $userOptions['colors'] = true; // turn on colors even in non-ansi mode if strictly passed } $suite = $input->getArgument('suite'); $test = $input->getArgument('test'); if (! Configuration::isEmpty() && ! $test && strpos($suite, $config['paths']['tests']) === 0) { list(, $suite, $test) = $this->matchTestFromFilename($suite, $config['paths']['tests']); } if ($this->options['group']) { $this->output->writeln(sprintf("[Groups] %s ", implode(', ', $this->options['group']))); } if ($input->getArgument('test')) { $this->options['steps'] = true; } if ($test) { $filter = $this->matchFilteredTestName($test); $userOptions['filter'] = $filter; } $this->codecept = new Codecept($userOptions); if ($suite and $test) { $this->codecept->run($suite, $test); } if (!$test) { $suites = $suite ? explode(',', $suite) : Configuration::suites(); $this->executed = $this->runSuites($suites, $this->options['skip']); if (!empty($config['include']) and !$suite) { $current_dir = Configuration::projectDir(); $suites += $config['include']; $this->runIncludedSuites($config['include'], $current_dir); } if ($this->executed === 0) { throw new \RuntimeException( sprintf("Suite '%s' could not be found", implode(', ', $suites)) ); } } $this->codecept->printResult(); if (!$input->getOption('no-exit')) { if (!$this->codecept->getResult()->wasSuccessful()) { exit(1); } } } /** * Runs included suites recursively * * @param array $suites * @param string $parent_dir */ protected function runIncludedSuites($suites, $parent_dir) { foreach ($suites as $relativePath) { $current_dir = rtrim($parent_dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $relativePath; $config = Configuration::config($current_dir); $suites = Configuration::suites(); $namespace = $this->currentNamespace(); $this->output->writeln( "\n\n[$namespace]: tests from $current_dir\n" ); $this->executed += $this->runSuites($suites, $this->options['skip']); if (!empty($config['include'])) { $this->runIncludedSuites($config['include'], $current_dir); } } } protected function currentNamespace() { $config = Configuration::config(); if (!$config['namespace']) { throw new \RuntimeException( "Can't include into runner suite without a namespace;\n" . "Please add `namespace` section into included codeception.yml file" ); } return $config['namespace']; } protected function runSuites($suites, $skippedSuites = []) { $executed = 0; foreach ($suites as $suite) { if (in_array($suite, $skippedSuites)) { continue; } if (!in_array($suite, Configuration::suites())) { continue; } $this->codecept->run($suite); $executed++; } return $executed; } protected function matchTestFromFilename($filename, $tests_path) { $filename = str_replace(['//', '\/', '\\'], '/', $filename); $res = preg_match("~^$tests_path/(.*?)(?>/(.*))?$~", $filename, $matches); if (!$res) { throw new \InvalidArgumentException("Test file can't be matched"); } if (!isset($matches[2])) { $matches[2] = null; } return $matches; } private function matchFilteredTestName(&$path) { $test_parts = explode(':', $path, 2); if (count($test_parts) > 1) { list($path, $filter) = $test_parts; // use carat to signify start of string like in normal regex // phpunit --filter matches against the fully qualified method name, so tests actually begin with : $carat_pos = strpos($filter, '^'); if ($carat_pos !== false) { $filter = substr_replace($filter, ':', $carat_pos, 1); } return $filter; } return null; } protected function passedOptionKeys(InputInterface $input) { $options = []; $request = (string)$input; $tokens = explode(' ', $request); foreach ($tokens as $token) { $token = preg_replace('~=.*~', '', $token); // strip = from options if (empty($token)) { continue; } if ($token == '--') { break; // there should be no options after ' -- ', only arguments } if (substr($token, 0, 2) === '--') { $options[] = substr($token, 2); } elseif ($token[0] === '-') { $shortOption = substr($token, 1); $options[] = $this->getDefinition()->getOptionForShortcut($shortOption)->getName(); } } return $options; } protected function booleanOptions(InputInterface $input, $options = []) { $values = []; $request = (string)$input; foreach ($options as $option => $defaultValue) { if (strpos($request, "--$option")) { $values[$option] = $input->getOption($option) ? $input->getOption($option) : $defaultValue; } else { $values[$option] = false; } } return $values; } private function ensureCurlIsAvailable() { if (!extension_loaded('curl')) { throw new \Exception( "Codeception requires CURL extension installed to make tests run\n" . "If you are not sure, how to install CURL, please refer to StackOverflow\n\n" . "Notice: PHP for Apache/Nginx and CLI can have different php.ini files.\n" . "Please make sure that your PHP you run from console has CURL enabled." ); } } }