![]() Server : Apache System : Linux server2.corals.io 4.18.0-348.2.1.el8_5.x86_64 #1 SMP Mon Nov 15 09:17:08 EST 2021 x86_64 User : corals ( 1002) PHP Version : 7.4.33 Disable Function : exec,passthru,shell_exec,system Directory : /home/corals/old/vendor/codeception/codeception/src/Codeception/Command/ |
<?php declare(strict_types=1); namespace Codeception\Command; use Codeception\Codecept; use Codeception\Configuration; use Codeception\Exception\ConfigurationException; use Codeception\Exception\ParseException; use Exception; use InvalidArgumentException; use RuntimeException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\InvalidArgumentException as SymfonyConsoleInvalidArgumentException; use Symfony\Component\Console\Exception\InvalidOptionException; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use function array_flip; use function array_intersect_key; use function array_merge; use function count; use function explode; use function extension_loaded; use function getcwd; use function implode; use function in_array; use function preg_match; use function preg_replace; use function rtrim; use function sprintf; use function str_contains; use function str_replace; use function str_starts_with; use function strpos; use function strtolower; use function substr; use function substr_replace; /** * Executes tests. * * Usage: * * * `codecept run acceptance`: run all acceptance tests * * `codecept run tests/acceptance/MyCest.php`: run only MyCest * * `codecept run acceptance MyCest`: same as above * * `codecept run acceptance MyCest:myTestInIt`: run one test from a Cest * * `codecept run acceptance MyCest:myTestInIt#1`: run one example or data provider item by number * * `codecept run acceptance MyCest:myTestInIt#1-3`: run a range of examples or data provider items * * `codecept run acceptance MyCest:myTestInIt@name.*`: run data provider items with matching names * * `codecept run acceptance checkout.feature`: run feature-file * * `codecept run acceptance -g slow`: run tests from *slow* group * * `codecept run unit,functional`: run only unit and functional suites * * Verbosity modes: * * * `codecept run -v`: * * `codecept run --steps`: print step-by-step execution * * `codecept run -vv`: print steps and debug information * * `codecept run --debug`: alias for `-vv` * * `codecept run -vvv`: print Codeception-internal debug information * * Load config: * * * `codecept run -c path/to/another/config`: from another dir * * `codecept run -c another_config.yml`: from another config file * * Override config values: * * * `codecept run -o "settings: shuffle: true"`: enable shuffle * * `codecept run -o "settings: lint: false"`: disable linting * * Run with specific extension * * * `codecept run --ext Recorder` run with Recorder extension enabled * * `codecept run --ext DotReporter` run with DotReporter printer * * `codecept run --ext "My\Custom\Extension"` run with an extension loaded by class name * * Full reference: * ``` * Arguments: * suite suite to be tested * test test to be run * * Options: * -o, --override=OVERRIDE Override config values (multiple values allowed) * --config (-c) Use custom path for config * --report Show output in compact style * --html Generate html with results (default: "report.html") * --xml Generate JUnit XML Log (default: "report.xml") * --phpunit-xml Generate PhpUnit XML Log (default: "phpunit-report.xml") * --no-redirect Do not redirect to Composer-installed version in vendor/codeception * --colors Use colors in output * --no-colors Force no colors in output (useful to override config file) * --silent Only outputs suite names and final results. Almost the same as `--quiet` * --steps Show steps in output * --debug (-d) Alias for `-vv` * --bootstrap Execute bootstrap script before the test * --coverage Run with code coverage (default: "coverage.serialized") * --coverage-html Generate CodeCoverage HTML report in path (default: "coverage") * --coverage-xml Generate CodeCoverage XML report in file (default: "coverage.xml") * --coverage-text Generate CodeCoverage text report in file (default: "coverage.txt") * --coverage-phpunit Generate CodeCoverage PHPUnit report in file (default: "coverage-phpunit") * --coverage-cobertura Generate CodeCoverage Cobertura report in file (default: "coverage-cobertura") * --no-exit Don't finish with exit code * --group (-g) Groups of tests to be executed (multiple values allowed) * --skip (-s) Skip selected suites (multiple values allowed) * --skip-group (-x) Skip selected groups (multiple values allowed) * --env Run tests in selected environments. (multiple values allowed, environments can be merged with ',') * --fail-fast (-f) Stop after nth failure (defaults to 1) * --no-rebuild Do not rebuild actor classes on start * --help (-h) Display this help message. * --quiet (-q) Do not output any message. Almost the same as `--silent` * --verbose (-v|vv|vvv) Increase the verbosity of messages: `v` for normal output, `vv` for steps and debug, `vvv` for Codeception-internal debug * --version (-V) Display this application version. * --ansi Force ANSI output. * --no-ansi Disable ANSI output. * --no-interaction (-n) Do not ask any interactive question. * --seed Use the given seed for shuffling tests * ``` * */ class Run extends Command { use Shared\ConfigTrait; protected ?Codecept $codecept = null; /** * @var int Executed suites */ protected int $executed = 0; protected array $options = []; protected ?OutputInterface $output = null; /** * Sets Run arguments * * @throws SymfonyConsoleInvalidArgumentException */ protected function configure(): void { $this->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('phpunit-xml', '', InputOption::VALUE_OPTIONAL, 'Generate PhpUnit XML Log', 'phpunit-report.xml'), 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('shard', '', InputOption::VALUE_REQUIRED, 'Execute subset of tests to run tests on different machine. To split tests on 3 machines to run with shards: 1/3, 2/3, 3/3'), new InputOption('filter', '', InputOption::VALUE_REQUIRED, 'Filter tests by name'), new InputOption('grep', '', InputOption::VALUE_REQUIRED, 'Filter tests by name (alias to --filter)'), new InputOption('bootstrap', '', InputOption::VALUE_OPTIONAL, 'Execute custom PHP script before running tests. Path can be absolute or relative to current working directory', false), new InputOption('no-redirect', '', InputOption::VALUE_NONE, 'Do not redirect to Composer-installed version in vendor/codeception'), 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( 'coverage-cobertura', '', InputOption::VALUE_OPTIONAL, 'Generate CodeCoverage report in Cobertura XML format' ), new InputOption( 'coverage-phpunit', '', InputOption::VALUE_OPTIONAL, 'Generate CodeCoverage PHPUnit report in path' ), 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_OPTIONAL, 'Stop after nth failure'), new InputOption('no-rebuild', '', InputOption::VALUE_NONE, 'Do not rebuild actor classes on start'), new InputOption( 'seed', '', InputOption::VALUE_REQUIRED, 'Define random seed for shuffle setting' ), new InputOption('no-artifacts', '', InputOption::VALUE_NONE, "Don't report about artifacts"), ]); parent::configure(); } public function getDescription(): string { return 'Runs the test suites'; } /** * Executes Run * * @throws ConfigurationException|ParseException */ public function execute(InputInterface $input, OutputInterface $output): int { $this->ensurePhpExtIsAvailable('CURL'); $this->ensurePhpExtIsAvailable('mbstring'); $this->options = $input->getOptions(); $this->output = $output; if ($this->options['bootstrap']) { Configuration::loadBootstrap($this->options['bootstrap'], getcwd()); } $config = $this->getGlobalConfig(); $config = $this->addRuntimeOptionsToCurrentConfig($config); if (!$this->options['colors']) { $this->options['colors'] = $config['settings']['colors']; } if (!$this->options['silent']) { $this->output->writeln( Codecept::versionString() . ' https://stand-with-ukraine.pp.ua' ); if ($this->options['seed']) { $this->output->writeln( "Running with seed: <info>" . $this->options['seed'] . "</info>\n" ); } } 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', 'phpunit-xml' => 'phpunit-report.xml', 'html' => 'report.html', 'coverage' => 'coverage.serialized', 'coverage-xml' => 'coverage.xml', 'coverage-html' => 'coverage', 'coverage-text' => 'coverage.txt', 'coverage-crap4j' => 'crap4j.xml', 'coverage-cobertura' => 'cobertura.xml', 'coverage-phpunit' => 'coverage-phpunit']) ); $userOptions['verbosity'] = $this->output->getVerbosity(); $userOptions['interactive'] = !$input->hasParameterOption(['--no-interaction', '-n']); $userOptions['ansi'] = (!$input->hasParameterOption('--no-ansi') xor $input->hasParameterOption('ansi')); $userOptions['seed'] = $this->options['seed'] ? (int)$this->options['seed'] : rand(); 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['coverage-xml'] || $this->options['coverage-html'] || $this->options['coverage-text'] || $this->options['coverage-crap4j'] || $this->options['coverage-phpunit']) { $this->options['coverage'] = true; } if (!$userOptions['ansi'] && $input->getOption('colors')) { $userOptions['colors'] = true; // turn on colors even in non-ansi mode if strictly passed } // array key will exist if fail-fast option is used if (array_key_exists('fail-fast', $userOptions)) { $userOptions['fail-fast'] = (int)$this->options['fail-fast'] ?: 1; } $suite = $input->getArgument('suite'); $test = $input->getArgument('test'); if ($this->options['group']) { $this->output->writeln(sprintf("[Groups] <info>%s</info> ", implode(', ', $this->options['group']))); } if ($input->getArgument('test')) { $this->options['steps'] = true; } if (!$test) { // Check if suite is given and is in an included path if (!empty($suite) && !empty($config['include'])) { $isIncludeTest = false; // Remember original projectDir $projectDir = Configuration::projectDir(); foreach ($config['include'] as $include) { // Find if the suite begins with an include path if (str_starts_with($suite, (string)$include)) { // Use include config $config = Configuration::config($projectDir . $include); $config = $this->addRuntimeOptionsToCurrentConfig($config); if (!empty($this->options['override'])) { $config = $this->overrideConfig($this->options['override']); } if (!isset($config['paths']['tests'])) { throw new RuntimeException( sprintf("Included '%s' has no tests path configured", $include) ); } $testsPath = $include . DIRECTORY_SEPARATOR . $config['paths']['tests']; try { [, $suite, $test] = $this->matchTestFromFilename($suite, $testsPath); $isIncludeTest = true; } catch (InvalidArgumentException) { // Incorrect include match, continue trying to find one continue; } } else { $result = $this->matchSingleTest($suite, $config); if ($result) { [, $suite, $test] = $result; } } } // Restore main config if (!$isIncludeTest) { $config = $this->addRuntimeOptionsToCurrentConfig( Configuration::config($projectDir) ); } } elseif (!empty($suite)) { $result = $this->matchSingleTest($suite, $config); if ($result) { [, $suite, $test] = $result; } } } $filter = $input->getOption('filter') ?? $input->getOption('grep') ?? null; if ($test) { $userOptions['filter'] = $this->matchFilteredTestName($test); } elseif ( $suite && !$this->isWildcardSuiteName($suite) && !$this->isSuiteInMultiApplication($suite) ) { $userOptions['filter'] = $this->matchFilteredTestName($suite); } if (isset($userOptions['filter']) && $filter) { throw new InvalidOptionException("--filter and --grep can't be used with a test name"); } elseif ($filter) { $userOptions['filter'] = $filter; } if ($this->options['shard']) { $this->output->writeln( "[Shard {$userOptions['shard']}] <info>Running subset of tests</info>" ); } if (!$this->options['silent'] && $config['settings']['shuffle']) { $this->output->writeln( "[Seed] <info>" . $userOptions['seed'] . "</info>" ); } $this->codecept = new Codecept($userOptions); if ($suite && $test) { $this->codecept->run($suite, $test, $config); } // Run all tests of given suite or all suites if (!$test) { $didPassCliSuite = !empty($suite); $rawSuites = $didPassCliSuite ? explode(',', $suite) : Configuration::suites(); /** @var string[] $mainAppSuites */ $mainAppSuites = []; /** @var array<string,string> $appSpecificSuites */ $appSpecificSuites = []; /** @var string[] $wildcardSuites */ $wildcardSuites = []; foreach ($rawSuites as $rawSuite) { if ($this->isWildcardSuiteName($rawSuite)) { $wildcardSuites[] = explode('*::', $rawSuite)[1]; continue; } if ($this->isSuiteInMultiApplication($rawSuite)) { $appAndSuite = explode('::', $rawSuite); $appSpecificSuites[$appAndSuite[0]][] = $appAndSuite[1]; continue; } $mainAppSuites[] = $rawSuite; } if ([] !== $mainAppSuites) { $this->executed = $this->runSuites($mainAppSuites, $this->options['skip']); } if (!empty($wildcardSuites) && ! empty($appSpecificSuites)) { $this->output->writeLn('<error>Wildcard options can not be combined with specific suites of included apps.</error>'); return 2; } if ( !empty($config['include']) && (!$didPassCliSuite || !empty($wildcardSuites) || !empty($appSpecificSuites)) ) { $currentDir = Configuration::projectDir(); $includedApps = $config['include']; if (!empty($appSpecificSuites)) { $includedApps = array_intersect($includedApps, array_keys($appSpecificSuites)); } $this->runIncludedSuites( $includedApps, $currentDir, $appSpecificSuites, $wildcardSuites ); } if ($this->executed === 0) { throw new RuntimeException( sprintf("Suite '%s' could not be found", implode(', ', $rawSuites)) ); } } $this->codecept->printResult(); if ($this->options['shard']) { $this->output->writeln( "[Shard {$userOptions['shard']}] <info>Merge this result with other shards to see the complete report</info>" ); } if (!$input->getOption('no-exit') && !$this->codecept->getResultAggregator()->wasSuccessfulIgnoringWarnings()) { exit(1); } return 0; } protected function matchSingleTest($suite, $config): ?array { // Workaround when codeception.yml is inside tests directory and tests path is set to "." // @see https://github.com/Codeception/Codeception/issues/4432 if (isset($config['paths']['tests']) && $config['paths']['tests'] === '.' && !preg_match('#^\.[/\\\]#', $suite)) { $suite = './' . $suite; } // running a single test when suite has a configured path if (isset($config['suites'])) { foreach ($config['suites'] as $s => $suiteConfig) { if (!isset($suiteConfig['path'])) { continue; } $testsPath = $config['paths']['tests'] . DIRECTORY_SEPARATOR . $suiteConfig['path']; if ($suiteConfig['path'] === '.') { $testsPath = $config['paths']['tests']; } if (preg_match("#^{$testsPath}/(.*?)$#", $suite, $matches)) { $matches[2] = $matches[1]; $matches[1] = $s; return $matches; } } } if (!Configuration::isEmpty()) { // Run single test without included tests if (str_starts_with($suite, (string)$config['paths']['tests'])) { return $this->matchTestFromFilename($suite, $config['paths']['tests']); } // Run single test from working directory $realTestDir = (string)realpath(Configuration::testsDir()); $cwd = (string)getcwd(); if (str_starts_with($realTestDir, $cwd)) { $file = $suite; if (str_contains($file, ':')) { [$file] = explode(':', $suite, -1); } $realPath = $cwd . DIRECTORY_SEPARATOR . $file; if (file_exists($realPath) && str_starts_with($realPath, $realTestDir)) { //only match test if file is in tests directory return $this->matchTestFromFilename( $cwd . DIRECTORY_SEPARATOR . $suite, $realTestDir ); } } } return null; } /** * Runs included suites recursively * * @param string[] $suites * @param array<string,string[]> $filterAppSuites An array keyed by included app name where values are suite names to run. * @param string[] $filterSuitesByWildcard A list of suite names (applies to all included apps) * @throws ConfigurationException */ protected function runIncludedSuites( array $suites, string $parentDir, array $filterAppSuites = [], array $filterSuitesByWildcard = [], ) { $defaultConfig = Configuration::config(); $absolutePath = Configuration::projectDir(); foreach ($suites as $relativePath) { $currentDir = rtrim($parentDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $relativePath; $config = Configuration::config($currentDir); if (!empty($defaultConfig['groups'])) { $groups = array_map(fn ($g) => $absolutePath . $g, $defaultConfig['groups']); Configuration::append(['groups' => $groups]); } $suites = Configuration::suites(); if (!empty($filterSuitesByWildcard)) { $suites = array_intersect($suites, $filterSuitesByWildcard); } if (isset($filterAppSuites[$relativePath])) { $suites = array_intersect($suites, $filterAppSuites[$relativePath]); } $namespace = $this->currentNamespace(); $this->output->writeln( "\n<fg=white;bg=magenta>\n[{$namespace}]: tests from {$currentDir}\n</fg=white;bg=magenta>" ); $this->executed += $this->runSuites($suites, $this->options['skip']); if (!empty($config['include'])) { $this->runIncludedSuites($config['include'], $currentDir); } } } protected function currentNamespace(): string { $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']; } /** * @param string[] $suites * @param string[] $skippedSuites * @return int Number of executed test suites */ protected function runSuites(array $suites, array $skippedSuites = []): int { $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; } /** * @return string[] */ protected function matchTestFromFilename(string $filename, string $testsPath): array { $filter = ''; if (str_contains($filename, ':')) { if ((PHP_OS === 'Windows' || PHP_OS === 'WINNT') && $filename[1] === ':') { // match C:\... [$drive, $path, $filter] = explode(':', $filename, 3); $filename = $drive . ':' . $path; } else { [$filename, $filter] = explode(':', $filename, 2); } if ($filter !== '') { $filter = ':' . $filter; } } $testsPath = str_replace(['//', '\/', '\\'], '/', $testsPath); $filename = str_replace(['//', '\/', '\\'], '/', $filename); if (rtrim($filename, '/') === $testsPath) { //codecept run tests return ['', '', $filter]; } $res = preg_match("#^{$testsPath}/(.*?)(?>/(.*))?$#", $filename, $matches); if (!$res) { throw new InvalidArgumentException("Test file can't be matched"); } if (!isset($matches[2])) { $matches[2] = ''; } if ($filter !== '') { $matches[2] .= $filter; } return $matches; } private function matchFilteredTestName(string &$path): ?string { $testParts = explode(':', $path, 2); if (count($testParts) > 1) { [$path, $filter] = $testParts; // 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 : $caratPos = strpos($filter, '^'); if ($caratPos !== false) { $filter = substr_replace($filter, ':', $caratPos, 1); } return $filter; } return null; } /** * @return string[] */ protected function passedOptionKeys(ArgvInput $input): array { $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 (str_starts_with($token, '--')) { $options[] = substr($token, 2); } elseif ($token[0] === '-') { $shortOption = substr($token, 1); $options[] = $this->getDefinition()->getOptionForShortcut($shortOption)->getName(); } } return $options; } /** * @return array<string, bool> */ protected function booleanOptions(ArgvInput $input, array $options = []): array { $values = []; $request = (string)$input; foreach ($options as $option => $defaultValue) { if (strpos($request, sprintf('--%s', $option))) { $values[$option] = $input->getOption($option) ?: $defaultValue; } else { $values[$option] = false; } } return $values; } /** * @throws Exception */ private function ensurePhpExtIsAvailable(string $ext): void { if (!extension_loaded(strtolower($ext))) { throw new Exception( "Codeception requires \"{$ext}\" extension installed to make tests run\n" . "If you are not sure, how to install \"{$ext}\", 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 \"{$ext}\" enabled." ); } } private function isWildcardSuiteName(string $suiteName): bool { return str_starts_with($suiteName, '*::'); } private function isSuiteInMultiApplication(string $suiteName): bool { return str_contains($suiteName, '::'); } private function addRuntimeOptionsToCurrentConfig(array $config): array { // update config from options if (count($this->options['override'])) { $config = $this->overrideConfig($this->options['override']); } // enable extensions if ($this->options['ext']) { $config = $this->enableExtensions($this->options['ext']); } return $config; } }