diff --git a/src/Codeception/Test/Cept.php b/src/Codeception/Test/Cept.php index 5d52361c54..bf2ba8aaba 100644 --- a/src/Codeception/Test/Cept.php +++ b/src/Codeception/Test/Cept.php @@ -7,8 +7,8 @@ use Codeception\Exception\TestParseException; use Codeception\Lib\Console\Message; use Codeception\Lib\Parser; -use Exception; use ParseError; +use RuntimeException; use function basename; use function file_get_contents; @@ -48,11 +48,10 @@ public function preload(): void public function test(): void { $scenario = $this->getScenario(); - $testFile = $this->getMetadata()->getFilename(); try { - require $testFile; + require $this->getFileName(); } catch (ParseError $error) { - throw new TestParseException($testFile, $error->getMessage(), $error->getLine()); + throw new TestParseException($this->getFileName(), $error->getMessage(), $error->getLine()); } } @@ -69,8 +68,8 @@ public function toString(): string public function getSourceCode(): string { $fileName = $this->getFileName(); - if (!$sourceCode = file_get_contents($fileName)) { - throw new Exception("Could not get content of file {$fileName}, please check its permissions."); + if (!is_readable($fileName) || ($sourceCode = file_get_contents($fileName)) === false) { + throw new RuntimeException("Could not read file {$fileName}, please check its permissions."); } return $sourceCode; } diff --git a/src/Codeception/Test/Cest.php b/src/Codeception/Test/Cest.php index f71c26e487..3825e106db 100644 --- a/src/Codeception/Test/Cest.php +++ b/src/Codeception/Test/Cest.php @@ -27,11 +27,9 @@ use function file; use function implode; use function is_callable; -use function method_exists; use function preg_replace; use function sprintf; use function strtolower; -use function trim; /** * Executes tests delivered in Cest format. @@ -67,8 +65,8 @@ public function __construct(object $testInstance, string $methodName, string $fi $metadata->setParamsFromAttributes($methodAnnotations->attributes()); $this->setMetadata($metadata); $this->testInstance = $testInstance; - $this->testClass = $testInstance::class; - $this->testMethod = $methodName; + $this->testClass = $testInstance::class; + $this->testMethod = $methodName; $this->createScenario(); $this->parser = new Parser($this->getScenario(), $this->getMetadata()); } @@ -81,14 +79,14 @@ public function __clone(): void public function preload(): void { $this->scenario->setFeature($this->getSpecFromMethod()); - $code = $this->getSourceCode(); - $this->parser->parseFeature($code); - $this->getMetadata()->getService('di')->injectDependencies($this->testInstance); - - // add example params to feature - if ($this->getMetadata()->getCurrent('example')) { - $step = new Comment('', $this->getMetadata()->getCurrent('example')); - $this->getScenario()->setFeature($this->getScenario()->getFeature() . ' | ' . $step->getArgumentsAsString(100)); + $this->parser->parseFeature($this->getSourceCode()); + $this->getDiService()->injectDependencies($this->testInstance); + + if ($example = $this->getMetadata()->getCurrent('example')) { + $step = new Comment('', $example); + $this->scenario->setFeature( + $this->scenario->getFeature() . ' | ' . $step->getArgumentsAsString(100) + ); } } @@ -96,9 +94,11 @@ public function getSourceCode(): string { $method = new ReflectionMethod($this->testInstance, $this->testMethod); $startLine = $method->getStartLine() - 1; // it's actually - 1, otherwise you wont get the function() block - $endLine = $method->getEndLine(); - $source = file($method->getFileName()); - return implode("", array_slice($source, $startLine, $endLine - $startLine)); + $lines = file($method->getFileName()); + return implode( + '', + array_slice($lines, $startLine, $method->getEndLine() - $startLine) + ); } public function getSpecFromMethod(): string @@ -111,18 +111,14 @@ public function getSpecFromMethod(): string public function test(): void { - $actorClass = $this->getMetadata()->getCurrent('actor'); - - if ($actorClass === null) { - throw new ConfigurationException( + $actorClass = $this->getMetadata()->getCurrent('actor') + ?? throw new ConfigurationException( 'actor setting is missing in suite configuration. Replace `class_name` with `actor` in config to fix this' ); - } - /** @var Di $di */ - $di = $this->getMetadata()->getService('di'); + $di = $this->getDiService(); $di->set($this->getScenario()); - $I = $di->instantiate($actorClass); + $I = $di->instantiate($actorClass); try { $this->executeHook($I, 'before'); @@ -177,12 +173,12 @@ protected function executeContextMethod(string $context, $I): void ); } - protected function invoke($methodName, array $context): void + protected function invoke(string $methodName, array $context): void { foreach ($context as $class) { - $this->getMetadata()->getService('di')->set($class); + $this->getDiService()->set($class); } - $this->getMetadata()->getService('di')->injectDependencies($this->testInstance, $methodName, $context); + $this->getDiService()->injectDependencies($this->testInstance, $methodName, $context); } protected function executeTestMethod($I): void @@ -191,14 +187,11 @@ protected function executeTestMethod($I): void throw new Exception("Method {$this->testMethod} can't be found in tested class"); } - if ($this->getMetadata()->getCurrent('example')) { - $this->invoke( - $this->testMethod, - [$I, $this->scenario, new Example($this->getMetadata()->getCurrent('example'))] - ); - return; + if ($example = $this->getMetadata()->getCurrent('example')) { + $this->invoke($this->testMethod, [$I, $this->scenario, new Example($example)]); + } else { + $this->invoke($this->testMethod, [$I, $this->scenario]); } - $this->invoke($this->testMethod, [$I, $this->scenario]); } public function toString(): string @@ -212,7 +205,7 @@ public function toString(): string public function getSignature(): string { - return $this->testClass . ":" . $this->testMethod; + return "{$this->testClass}:{$this->testMethod}"; } public function getTestInstance(): object @@ -279,4 +272,9 @@ public function getLinesToBeUsed(): array return (new CodeCoverage())->linesToBeUsed($this->testClass, $this->testMethod); } + + private function getDiService(): Di + { + return $this->getMetadata()->getService('di'); + } } diff --git a/src/Codeception/Test/DataProvider.php b/src/Codeception/Test/DataProvider.php index 827279777f..e0572dcf8b 100644 --- a/src/Codeception/Test/DataProvider.php +++ b/src/Codeception/Test/DataProvider.php @@ -19,83 +19,69 @@ class DataProvider { public static function getDataForMethod(ReflectionMethod $method, ?ReflectionClass $class = null, ?Actor $I = null): ?iterable { - $testClass = self::getTestClass($method, $class); + $testClass = self::getTestClass($method, $class); $testClassName = $testClass->getName(); - $methodName = $method->getName(); - - // example annotation - $rawExamples = array_values( - Annotation::forMethod($testClassName, $methodName)->fetchAll('example'), - ); + $methodName = $method->getName(); + $annotation = Annotation::forMethod($testClassName, $methodName); + $data = []; + $rawExamples = $annotation->fetchAll('example'); if ($rawExamples !== []) { - $rawExample = reset($rawExamples); - if (is_string($rawExample)) { - $result = array_map( - static fn ($v): ?array => Annotation::arrayValue($v), - $rawExamples - ); - } else { - $result = $rawExamples; + $convert = is_string(reset($rawExamples)); + foreach ($rawExamples as $example) { + $data[] = $convert ? Annotation::arrayValue($example) : $example; } - } else { - $result = []; } - // dataProvider annotation - $dataProviderAnnotations = Annotation::forMethod($testClassName, $methodName)->fetchAll('dataProvider'); - // lowercase for back compatible - if ($dataProviderAnnotations === []) { - $dataProviderAnnotations = Annotation::forMethod($testClassName, $methodName)->fetchAll('dataprovider'); - } + $providers = array_merge( + $annotation->fetchAll('dataProvider'), + $annotation->fetchAll('dataprovider') + ); - if ($result === [] && $dataProviderAnnotations === []) { + if ($data === [] && $providers === []) { return null; } - foreach ($dataProviderAnnotations as $dataProviderAnnotation) { - [$dataProviderClassName, $dataProviderMethodName] = self::parseDataProviderAnnotation( - $dataProviderAnnotation, + foreach ($providers as $provider) { + [$providerClass, $providerMethod] = self::parseDataProviderAnnotation( + $provider, $testClassName, - $methodName, + $methodName ); try { - $dataProviderMethod = new ReflectionMethod($dataProviderClassName, $dataProviderMethodName); - if ($dataProviderMethod->isStatic()) { - $dataProviderResult = call_user_func([$dataProviderClassName, $dataProviderMethodName], $I); + $refMethod = new ReflectionMethod($providerClass, $providerMethod); + + if ($refMethod->isStatic()) { + $result = $providerClass::$providerMethod($I); } else { - $testInstance = new $dataProviderClassName($dataProviderMethodName); - - if ($dataProviderMethod->isPublic()) { - $dataProviderResult = $testInstance->$dataProviderMethodName($I); - } else { - $dataProviderResult = ReflectionHelper::invokePrivateMethod( - $testInstance, - $dataProviderMethodName, - [$I] - ); - } + $instance = new $providerClass($providerMethod); + $result = $refMethod->isPublic() + ? $instance->$providerMethod($I) + : ReflectionHelper::invokePrivateMethod($instance, $providerMethod, [$I]); } - foreach ($dataProviderResult as $key => $value) { - if (is_int($key)) { - $result [] = $value; - } else { - $result[$key] = $value; - } + if (!is_iterable($result)) { + throw new InvalidTestException( + "DataProvider '{$provider}' for {$testClassName}::{$methodName} " . + 'must return iterable data, got ' . gettype($result) + ); } - } catch (ReflectionException) { + + foreach ($result as $key => $value) { + is_int($key) ? $data[] = $value : $data[$key] = $value; + } + } catch (ReflectionException $e) { throw new InvalidTestException(sprintf( "DataProvider '%s' for %s::%s is invalid or not callable", - $dataProviderAnnotation, + $provider, $testClassName, $methodName - )); + ), 0, $e); } } - return $result; + return $data ?: null; } /** @@ -108,37 +94,30 @@ public static function parseDataProviderAnnotation( string $testMethodName, ): array { $parts = explode('::', $annotation); - if (count($parts) > 2) { - throw new InvalidTestException( - sprintf( - 'Data provider "%s" specified for %s::%s is invalid', - $annotation, - $testClassName, - $testMethodName, - ) - ); - } - if (count($parts) === 2) { + if (count($parts) === 2 && $parts[0] !== '') { return $parts; } + if (count($parts) === 1 || $parts[0] === '') { + return [$testClassName, ltrim($parts[1] ?? $parts[0], ':')]; + } - return [ - $testClassName, + throw new InvalidTestException(sprintf( + 'Data provider "%s" specified for %s::%s is invalid', $annotation, - ]; + $testClassName, + $testMethodName + )); } - /** - * Retrieves actual test class for dataProvider. - */ - private static function getTestClass(ReflectionMethod $dataProviderMethod, ?ReflectionClass $testClass): ReflectionClass + private static function getTestClass(ReflectionMethod $method, ?ReflectionClass $class): ReflectionClass { - $dataProviderDeclaringClass = $dataProviderMethod->getDeclaringClass(); - // data provider in abstract class? - if ($dataProviderDeclaringClass->isAbstract() && $testClass instanceof ReflectionClass && $dataProviderDeclaringClass->name !== $testClass->name) { - return $testClass; - } - return $dataProviderDeclaringClass; + $declaringClass = $method->getDeclaringClass(); + + return $declaringClass->isAbstract() + && $class instanceof ReflectionClass + && $declaringClass->getName() !== $class->getName() + ? $class + : $declaringClass; } } diff --git a/src/Codeception/Test/Descriptor.php b/src/Codeception/Test/Descriptor.php index 9919581eec..0156fcc184 100644 --- a/src/Codeception/Test/Descriptor.php +++ b/src/Codeception/Test/Descriptor.php @@ -22,38 +22,27 @@ class Descriptor { - /** - * Provides a test name which can be located by - */ public static function getTestSignature(Descriptive $test): string { return $test->getSignature(); } - /** - * Provides a test name which is unique for individual iterations of tests using examples - */ public static function getTestSignatureUnique(SelfDescribing $testCase): string { - $env = ''; - $example = ''; - - if ( - method_exists($testCase, 'getScenario') - && !empty($testCase->getScenario()?->current('env')) - ) { - $env = ':' . $testCase->getScenario()->current('env'); + $signature = self::getTestSignature($testCase); + if (method_exists($testCase, 'getScenario')) { + if ($env = $testCase->getScenario()?->current('env')) { + $signature .= ':' . $env; + } } - - if ( - method_exists($testCase, 'getMetaData') - && !empty($testCase->getMetadata()->getCurrent('example')) - ) { - $currentExample = json_encode($testCase->getMetadata()->getCurrent('example'), JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_SUBSTITUTE); - $example = ':' . substr(sha1($currentExample), 0, 7); + if (method_exists($testCase, 'getMetadata')) { + if ($example = $testCase->getMetadata()->getCurrent('example')) { + $encoded = json_encode($example, JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_SUBSTITUTE); + $signature .= ':' . substr(sha1($encoded), 0, 7); + } } - return self::getTestSignature($testCase) . $env . $example; + return $signature; } public static function getTestAsString(SelfDescribing $testCase): string @@ -66,14 +55,12 @@ public static function getTestCaseNameAsString(string $testCaseName): string $text = $testCaseName; $text = preg_replace('#([A-Z]+)([A-Z][a-z])#', '\\1 \\2', $text); $text = preg_replace('#([a-z\d])([A-Z])#', '\\1 \\2', $text); - $text = preg_replace('#^test #', '', $text); + $text = preg_replace('#^test #i', '', $text); $text = ucfirst(strtolower($text)); + return str_replace(['::', 'with data set'], [':', '|'], $text); } - /** - * Provides a test file name relative to Codeception root - */ public static function getTestFileName(Descriptive $test): string { return codecept_relative_path(realpath($test->getFileName())); @@ -85,25 +72,21 @@ public static function getTestFullName(Plain|Descriptive $test): string return self::getTestFileName($test); } - $signature = $test->getSignature(); // cut everything before ":" from signature - return self::getTestFileName($test) . ':' . preg_replace('#^(.*?):#', '', $signature); + return self::getTestFileName($test) . ':' . + preg_replace('#^(.*?):#', '', $test->getSignature()); } - /** - * Provides a test data set index - */ public static function getTestDataSetIndex(SelfDescribing $testCase): string { - if ($testCase instanceof TestInterface) { - $index = $testCase->getMetadata()->getIndex(); - if ($index === null) { - return ''; - } - if (is_int($index)) { - return ' with data set #' . $index; - } - return ' with data set "' . $index . '"'; + if (!$testCase instanceof TestInterface) { + return ''; } - return ''; + $index = $testCase->getMetadata()->getIndex(); + + return match (true) { + is_int($index) => ' with data set #' . $index, + $index !== null => ' with data set "' . $index . '"', + default => '', + }; } } diff --git a/src/Codeception/Test/Filter.php b/src/Codeception/Test/Filter.php index ac00bf7c3d..8e2f150659 100644 --- a/src/Codeception/Test/Filter.php +++ b/src/Codeception/Test/Filter.php @@ -9,8 +9,8 @@ class Filter { private ?string $namePattern = null; - private ?int $filterMin = null; - private ?int $filterMax = null; + private ?int $filterMin = null; + private ?int $filterMax = null; /** * @param string[] $includeGroups @@ -22,60 +22,43 @@ public function __construct( private readonly ?array $excludeGroups, ?string $namePattern ) { - if ($namePattern === null) { - return; + if ($namePattern !== null) { + $this->namePattern = $this->preparePattern($namePattern); } + } - // Validates regexp without E_WARNING - set_error_handler(function (): void { - }, E_WARNING); - $isRegularExpression = preg_match($namePattern, '') !== false; - restore_error_handler(); + private function preparePattern(string $namePattern): string + { + if (@preg_match($namePattern, '') !== false) { + return $namePattern; + } - if ($isRegularExpression === false) { - // Handles: - // * :testAssertEqualsSucceeds#4 - // * "testAssertEqualsSucceeds#4-8 - if (preg_match('/^(.*?)#(\d+)(?:-(\d+))?$/', $namePattern, $matches)) { - if (isset($matches[3]) && $matches[2] < $matches[3]) { - $namePattern = sprintf( - '%s.*with data set #(\d+)$', - $matches[1] - ); + if (preg_match('/^(.*?)#(\d+)(?:-(\d+))?$/', $namePattern, $matches)) { + if (isset($matches[3]) && (int)$matches[2] < (int)$matches[3]) { + $this->filterMin = (int) $matches[2]; + $this->filterMax = (int) $matches[3]; - $this->filterMin = (int)$matches[2]; - $this->filterMax = (int)$matches[3]; - } else { - $namePattern = sprintf( - '%s.*with data set #%s$', - $matches[1], - $matches[2] - ); - } - } elseif (preg_match('/^(.*?)@(.+)$/', $namePattern, $matches)) { - // Handles: - // * :testDetermineJsonError@JSON_ERROR_NONE - // * :testDetermineJsonError@JSON.* $namePattern = sprintf( - '%s.*with data set "%s"$', + '%s.*with data set #(\\d+)$', + $matches[1] + ); + } else { + $namePattern = sprintf( + '%s.*with data set #%s$', $matches[1], $matches[2] ); } - - // Escape delimiters in regular expression. Do NOT use preg_quote, - // to keep magic characters. + } elseif (preg_match('/^(.*?)@(.+)$/', $namePattern, $matches)) { $namePattern = sprintf( - '/%s/i', - str_replace( - '/', - '\\/', - $namePattern - ) + '%s.*with data set "%s"$', + $matches[1], + $matches[2] ); } - $this->namePattern = $namePattern; + $escaped = str_replace('/', '\\/', $namePattern); + return "/{$escaped}/i"; } public function isNameAccepted(Test $test): bool @@ -84,23 +67,26 @@ public function isNameAccepted(Test $test): bool return true; } - $name = Descriptor::getTestSignature($test) . Descriptor::getTestDataSetIndex($test); - - $accepted = preg_match($this->namePattern, $name, $matches); + $name = Descriptor::getTestSignature($test) . Descriptor::getTestDataSetIndex($test); + $matches = []; + if (preg_match($this->namePattern, $name, $matches) === 0) { + return false; + } - if ($accepted && $this->filterMax !== null) { - $set = end($matches); - $accepted = $set >= $this->filterMin && $set <= $this->filterMax; + if ($this->filterMax !== null) { + $set = (int) end($matches); + return $set >= $this->filterMin && $set <= $this->filterMax; } - return (bool)$accepted; + + return true; } public function isGroupAccepted(Test $test, array $groups): bool { - if ($this->includeGroups !== null && $this->includeGroups !== [] && array_intersect($groups, $this->includeGroups) === []) { + if ($this->includeGroups && !array_intersect($groups, $this->includeGroups)) { return false; } - if ($this->excludeGroups !== null && $this->excludeGroups !== [] && count(array_intersect($groups, $this->excludeGroups)) > 0) { + if ($this->excludeGroups && array_intersect($groups, $this->excludeGroups)) { return false; } diff --git a/src/Codeception/Test/Gherkin.php b/src/Codeception/Test/Gherkin.php index fb2639fc05..2c5de65635 100644 --- a/src/Codeception/Test/Gherkin.php +++ b/src/Codeception/Test/Gherkin.php @@ -9,7 +9,6 @@ use Behat\Gherkin\Node\ScenarioNode; use Behat\Gherkin\Node\StepNode; use Behat\Gherkin\Node\TableNode; -use Codeception\Lib\Di; use Codeception\Lib\Generator\GherkinSnippets; use Codeception\Scenario; use Codeception\Step\Comment; @@ -18,9 +17,8 @@ use Codeception\Test\Interfaces\ScenarioDriven; use Exception; +use function array_keys; use function array_merge; -use function array_pop; -use function array_shift; use function basename; use function call_user_func_array; use function count; @@ -35,8 +33,11 @@ class Gherkin extends Test implements ScenarioDriven, Reported { protected Scenario $scenario; - public function __construct(protected FeatureNode $featureNode, protected ScenarioInterface $scenarioNode, protected array $steps = []) - { + public function __construct( + protected FeatureNode $featureNode, + protected ScenarioInterface $scenarioNode, + protected array $steps = [] + ) { $this->setMetadata(new Metadata()); $this->scenario = new Scenario($this); $this->getMetadata()->setName($this->scenarioNode->getTitle()); @@ -51,21 +52,13 @@ public function __clone(): void public function preload(): void { - $this->getMetadata()->setGroups($this->featureNode->getTags()); - $this->getMetadata()->setGroups($this->scenarioNode->getTags()); + $metadata = $this->getMetadata(); + $metadata->setGroups(array_merge($this->featureNode->getTags(), $this->scenarioNode->getTags())); $this->scenario->setMetaStep(null); + $this->processSteps([$this, 'validateStep']); - if (($background = $this->featureNode->getBackground()) !== null) { - foreach ($background->getSteps() as $step) { - $this->validateStep($step); - } - } - - foreach ($this->scenarioNode->getSteps() as $step) { - $this->validateStep($step); - } - if ($this->getMetadata()->getIncomplete()) { - $this->getMetadata()->setIncomplete($this->getMetadata()->getIncomplete() . "\nRun gherkin:snippets to define missing steps"); + if ($incomplete = rtrim((string) $metadata->getIncomplete(), "\n")) { + $metadata->setIncomplete($incomplete . "\nRun gherkin:snippets to define missing steps"); } } @@ -77,125 +70,102 @@ public function getSignature(): string public function test(): void { $this->makeContexts(); - $description = explode("\n", (string)$this->featureNode->getDescription()); - foreach ($description as $line) { - $this->getScenario()->runStep(new Comment($line)); - } - - if (($background = $this->featureNode->getBackground()) !== null) { - foreach ($background->getSteps() as $step) { - $this->runStep($step); - } - } - - foreach ($this->scenarioNode->getSteps() as $step) { - $this->runStep($step); + foreach (explode("\n", (string)$this->featureNode->getDescription()) as $line) { + $this->scenario->runStep(new Comment($line)); } + $this->processSteps([$this, 'runStep']); } - protected function validateStep(StepNode $stepNode): void + private function processSteps(callable $callback): void { - $stepText = $stepNode->getText(); - if (GherkinSnippets::stepHasPyStringArgument($stepNode)) { - $stepText .= ' ""'; - } - $matches = []; - foreach ($this->steps as $pattern => $context) { - $res = preg_match($pattern, $stepText); - if (!$res) { - continue; - } - $matches[$pattern] = $context; - } - if ($matches === []) { - // There were no matches, meaning that the user should first add a step definition for this step - $incomplete = $this->getMetadata()->getIncomplete(); - $this->getMetadata()->setIncomplete("{$incomplete}\nStep definition for `{$stepText}` not found in contexts"); - } - if (count($matches) > 1) { - // There were more than one match, meaning that we don't know which step definition to execute for this step - $incomplete = $this->getMetadata()->getIncomplete(); - $matchingDefinitions = []; - foreach ($matches as $pattern => $context) { - $matchingDefinitions[] = '- ' . $pattern . ' (' . self::contextAsString($context) . ')'; - } - $this->getMetadata()->setIncomplete( - "{$incomplete}\nAmbiguous step: `{$stepText}` matches multiple definitions:\n" - . implode("\n", $matchingDefinitions) - ); + if ($background = $this->featureNode->getBackground()) { + array_map($callback, $background->getSteps()); } + array_map($callback, $this->scenarioNode->getSteps()); } - private function contextAsString($context): string + protected function validateStep(StepNode $stepNode): void { - if (is_array($context) && count($context) === 2) { - [$class, $method] = $context; + $text = $stepNode->getText() . (GherkinSnippets::stepHasPyStringArgument($stepNode) ? ' ""' : ''); + $metadata = $this->getMetadata(); - if (is_string($class) && is_string($method)) { - return $class . ':' . $method; - } - } + $matchedPatterns = array_filter( + array_keys($this->steps), + fn(string $pattern): bool => preg_match($pattern, $text) === 1 + ); - return var_export($context, true); + if (empty($matchedPatterns)) { + $metadata->setIncomplete( + ($metadata->getIncomplete() ?? '') + . "\nStep definition for `{$text}` not found in contexts" + ); + } elseif (count($matchedPatterns) > 1) { + $defs = array_map( + fn(string $pattern): string => "- {$pattern} ({$this->contextAsString($this->steps[$pattern])})", + $matchedPatterns + ); + $metadata->setIncomplete( + ($metadata->getIncomplete() ?? '') + . "\nAmbiguous step: `{$text}` matches multiple definitions:\n" + . implode("\n", $defs) + ); + } } protected function runStep(StepNode $stepNode): void { + $text = $stepNode->getText() . (GherkinSnippets::stepHasPyStringArgument($stepNode) ? ' ""' : ''); $params = []; - if ($stepNode->hasArguments()) { - $args = $stepNode->getArguments(); - $table = $args[0]; - if ($table instanceof TableNode) { - $params = [$table->getTableAsString()]; - } + if ($stepNode->hasArguments() && $stepNode->getArguments()[0] instanceof TableNode) { + $params[] = $stepNode->getArguments()[0]->getTableAsString(); } $meta = new Meta($stepNode->getText(), $params); $meta->setPrefix($stepNode->getKeyword()); + $this->scenario->setMetaStep($meta); + $this->scenario->comment(''); - $this->scenario->setMetaStep($meta); // enable metastep - $stepText = $stepNode->getText(); - $hasPyStringArg = GherkinSnippets::stepHasPyStringArgument($stepNode); - if ($hasPyStringArg) { - // pretend it is inline argument - $stepText .= ' ""'; - } - $this->getScenario()->comment(''); // make metastep to be printed even if no steps in it foreach ($this->steps as $pattern => $context) { - $matches = []; - if (!preg_match($pattern, $stepText, $matches)) { + if (!preg_match($pattern, $text, $matches)) { continue; } - array_shift($matches); - if ($hasPyStringArg) { - // get rid off last fake argument - array_pop($matches); + $args = array_slice($matches, 1); + if (GherkinSnippets::stepHasPyStringArgument($stepNode)) { + array_pop($args); } if ($stepNode->hasArguments()) { - $matches = array_merge($matches, $stepNode->getArguments()); + $args = array_merge($args, $stepNode->getArguments()); } - call_user_func_array($context, $matches); // execute the step + call_user_func_array($context, $args); break; } - $this->scenario->setMetaStep(null); // disable metastep + + $this->scenario->setMetaStep(null); } protected function makeContexts(): void { - /** @var Di $di */ $di = $this->getMetadata()->getService('di'); - $di->set($this->getScenario()); - - $actorClass = $this->getMetadata()->getCurrent('actor'); - if ($actorClass) { - $di->instantiate($actorClass); + $di->set($this->scenario); + if ($actor = $this->getMetadata()->getCurrent('actor')) { + $di->instantiate($actor); } - - foreach ($this->steps as $pattern => $step) { + foreach ($this->steps as $pattern => &$step) { $di->instantiate($step[0]); - $this->steps[$pattern][0] = $di->get($step[0]); + $step[0] = $di->get($step[0]); } } + private function contextAsString(mixed $context): string + { + if (is_array($context) && count($context) === 2) { + [$class, $method] = $context; + if (is_string($class) && is_string($method)) { + return "{$class}:{$method}"; + } + } + return var_export($context, true); + } + public function toString(): string { return $this->getFeature() . ': ' . $this->getScenarioTitle(); @@ -248,9 +218,9 @@ public function getFeatureNode(): FeatureNode public function getReportFields(): array { return [ - 'name' => $this->toString(), + 'name' => $this->toString(), 'feature' => $this->getFeature(), - 'file' => $this->getFileName(), + 'file' => $this->getFileName(), ]; } } diff --git a/src/Codeception/Test/Loader.php b/src/Codeception/Test/Loader.php index 88f38acb09..9b7167a01e 100644 --- a/src/Codeception/Test/Loader.php +++ b/src/Codeception/Test/Loader.php @@ -13,7 +13,6 @@ use Exception; use Symfony\Component\Finder\Finder; -use function array_merge; use function file_exists; use function getcwd; use function is_dir; @@ -50,58 +49,48 @@ */ class Loader { - /** - * @var LoaderInterface[] - */ - protected array $formats = []; - + /** @var LoaderInterface[] */ + protected array $formats; protected array $tests = []; - - protected ?string $path = null; - - private ?string $shard = null; + protected ?string $path; + private ?string $shard; public function __construct(array $suiteSettings) { - $this->path = $suiteSettings['path']; + $this->path = !empty($suiteSettings['path']) ? rtrim($suiteSettings['path'], "/\\") . '/' : null; $this->shard = $suiteSettings['shard'] ?? null; $this->formats = [ new CeptLoader(), new CestLoader($suiteSettings), new UnitLoader(), - new GherkinLoader($suiteSettings) + new GherkinLoader($suiteSettings), ]; - if (isset($suiteSettings['formats'])) { - foreach ($suiteSettings['formats'] as $format) { - $this->formats[] = new $format($suiteSettings); - } + + foreach ($suiteSettings['formats'] ?? [] as $format) { + $this->formats[] = new $format($suiteSettings); } } public function getTests(): array { - if ($this->shard) { - $this->shard = trim($this->shard); - if (!preg_match('~^\d+\/\d+$~', $this->shard)) { - throw new ConfigurationException('Shard must be set as --shard=CURRENT/TOTAL where CURRENT and TOTAL are number. For instance: --shard=1/3'); - } - - [$shard, $totalShards] = explode('/', $this->shard); - - if ($shard < 1) { - throw new ConfigurationException("Incorrect shard index. Use 1/{$totalShards} to start the first shard"); - } + if ($this->shard === null) { + return $this->tests; + } - if ($totalShards < $shard) { - throw new ConfigurationException('Total shards are less than current shard'); - } + if (sscanf(trim($this->shard), '%d/%d', $current, $total) !== 2) { + throw new ConfigurationException('Shard must be set as --shard=CURRENT/TOTAL where both parts are numbers, e.g. --shard=1/3'); + } + if ($current < 1) { + throw new ConfigurationException("Incorrect shard index. Use 1/{$total} to start the first shard"); + } + if ($total < $current) { + throw new ConfigurationException('Total shards are less than current shard'); + } - $chunks = $this->splitTestsIntoChunks((int)$totalShards); + $chunks = $this->splitTestsIntoChunks($total); - return $chunks[$shard - 1] ?? []; - } - return $this->tests; + return $chunks[$current - 1] ?? []; } private function splitTestsIntoChunks(int $chunks): array @@ -109,12 +98,13 @@ private function splitTestsIntoChunks(int $chunks): array if ($this->tests === []) { return []; } - return array_chunk($this->tests, (int) ceil(count($this->tests) / $chunks)); + + return array_chunk($this->tests, (int) ceil(count($this->tests) / $chunks), true); } protected function relativeName(string $file): string { - return str_replace([$this->path, '\\'], ['', '/'], $file); + return str_replace('\\', '/', str_replace($this->path ?? '', '', $file)); } protected function findPath(string $path): string @@ -132,25 +122,29 @@ protected function findPath(string $path): string protected function makePath(string $originalPath): string { - $path = $this->path . $this->relativeName($originalPath); - - if ( - file_exists($newPath = $this->findPath($path)) - || file_exists($newPath = $this->findPath(getcwd() . "/{$originalPath}")) - ) { - $path = $newPath; - } - - if (!file_exists($path)) { - throw new Exception("File or path {$originalPath} not found"); + $candidates = [ + $this->findPath(($this->path ?? '') . $this->relativeName($originalPath)), + $this->findPath(getcwd() . "/{$originalPath}"), + ]; + foreach ($candidates as $candidate) { + if (file_exists($candidate)) { + return $candidate; + } } - return $path; + throw new Exception("File or path {$originalPath} not found"); } public function loadTest(string $path): void { $path = $this->makePath($path); + if (is_dir($path)) { + $previous = $this->path; + $this->path = rtrim($path, "/\\") . '/'; + $this->loadTests(); + $this->path = $previous; + return; + } foreach ($this->formats as $format) { if (preg_match($format->getPattern(), $path)) { @@ -160,33 +154,28 @@ public function loadTest(string $path): void } } - if (is_dir($path)) { - $currentPath = $this->path; - $this->path = $path; - $this->loadTests(); - $this->path = $currentPath; - return; - } throw new Exception('Test format not supported. Please, check you use the right suffix. Available filetypes: Cept, Cest, Test'); } public function loadTests(?string $fileName = null): void { - if ($fileName) { + if ($fileName !== null) { $this->loadTest($fileName); return; } - $finder = Finder::create()->files()->sortByName()->in($this->path)->followLinks(); + $files = Finder::create()->files()->sortByName()->in($this->path)->followLinks(); + foreach ($files as $file) { + foreach ($this->formats as $format) { + if (preg_match($format->getPattern(), $path = $file->getPathname())) { + $format->loadTests($path); + break; + } + } + } foreach ($this->formats as $format) { - $formatFinder = clone($finder); - $testFiles = $formatFinder->name($format->getPattern()); - foreach ($testFiles as $test) { - $pathname = str_replace(["//", "\\\\"], ["/", "\\"], $test->getPathname()); - $format->loadTests($pathname); - } - $this->tests = array_merge($this->tests, $format->getTests()); + $this->tests = [...$this->tests, ...$format->getTests()]; } } } diff --git a/src/Codeception/Test/Loader/Cest.php b/src/Codeception/Test/Loader/Cest.php index a8a70047af..e0e92e5691 100644 --- a/src/Codeception/Test/Loader/Cest.php +++ b/src/Codeception/Test/Loader/Cest.php @@ -6,7 +6,6 @@ use Codeception\Command\Shared\ActorTrait; use Codeception\Lib\Parser; -use Codeception\Scenario; use Codeception\Test\Cest as CestFormat; use Codeception\Test\DataProvider; use ReflectionClass; diff --git a/src/Codeception/Test/Loader/Gherkin.php b/src/Codeception/Test/Loader/Gherkin.php index efdc0c9f10..824faa1d80 100644 --- a/src/Codeception/Test/Loader/Gherkin.php +++ b/src/Codeception/Test/Loader/Gherkin.php @@ -18,7 +18,6 @@ use Codeception\Lib\Generator\Shared\Classname; use Codeception\Test\Gherkin as GherkinFormat; use Codeception\Util\Annotation; -use ReflectionClass; use function array_keys; use function array_map; diff --git a/src/Codeception/Test/Metadata.php b/src/Codeception/Test/Metadata.php index 9e588ad4c3..a2866f4f11 100644 --- a/src/Codeception/Test/Metadata.php +++ b/src/Codeception/Test/Metadata.php @@ -14,34 +14,27 @@ class Metadata { protected ?string $name = null; - protected ?string $filename = null; - protected string $feature = ''; - - protected null|int|string $index = null; + protected int|string|null $index = null; protected array $params = [ - 'env' => [], - 'group' => [], - 'depends' => [], - 'skip' => null, - 'incomplete' => null + 'env' => [], + 'group' => [], + 'depends' => [], + 'skip' => null, + 'incomplete' => null, ]; - protected array $current = []; - + protected array $current = []; protected array $services = []; + protected array $reports = []; - protected array $reports = []; - /** - * @var string[] - */ + /** @var string[] */ private array $beforeClassMethods = []; - /** - * @var string[] - */ - private array $afterClassMethods = []; + + /** @var string[] */ + private array $afterClassMethods = []; public function getEnv(): array { @@ -53,9 +46,7 @@ public function getGroups(): array return array_unique($this->params['group']); } - /** - * @param string[] $groups - */ + /** @param string[] $groups */ public function setGroups(array $groups): void { $this->params['group'] = array_merge($this->params['group'], $groups); @@ -83,17 +74,10 @@ public function setIncomplete(string $incomplete): void public function getCurrent(?string $key = null): mixed { - if ($key) { - if (isset($this->current[$key])) { - return $this->current[$key]; - } - if ($key === 'name') { - return $this->getName(); - } - return null; + if ($key === null) { + return $this->current; } - - return $this->current; + return $this->current[$key] ?? ($key === 'name' ? $this->getName() : null); } public function setCurrent(array $currents): void @@ -103,7 +87,7 @@ public function setCurrent(array $currents): void public function getName(): string { - return $this->name; + return $this->name ?? ''; } public function setName(string $name): void @@ -113,22 +97,22 @@ public function setName(string $name): void public function getFilename(): string { - return $this->filename; + return $this->filename ?: ''; } - public function setIndex(int|string $index): void + public function setFilename(string $filename): void { - $this->index = $index; + $this->filename = $filename; } - public function getIndex(): null|int|string + public function setIndex(int|string $index): void { - return $this->index; + $this->index = $index; } - public function setFilename(string $filename): void + public function getIndex(): int|string|null { - $this->filename = $filename; + return $this->index; } /** @return string[] */ @@ -139,10 +123,7 @@ public function getDependencies(): array public function isBlocked(): bool { - if ($this->getSkip() !== null) { - return true; - } - return $this->getIncomplete() !== null; + return $this->getSkip() !== null || $this->getIncomplete() !== null; } public function getFeature(): string @@ -168,9 +149,6 @@ public function setServices(array $services): void $this->services = $services; } - /** - * Returns all test reports - */ public function getReports(): array { return $this->reports; @@ -187,29 +165,24 @@ public function addReport(string $type, $report): void */ public function getParam(?string $key = null): mixed { - if ($key) { - if (isset($this->params[$key])) { - return $this->params[$key]; - } - return null; - } - - return $this->params; + return $key === null ? $this->params : ($this->params[$key] ?? null); } public function setParamsFromAnnotations($annotations): void { - $params = Annotation::fetchAllAnnotationsFromDocblock((string)$annotations); - $this->params = array_merge_recursive($this->params, $params); - + $this->params = array_merge_recursive( + $this->params, + Annotation::fetchAllAnnotationsFromDocblock((string) $annotations) + ); $this->setSingularValueForSomeParams(); } private function setSingularValueForSomeParams(): void { foreach (['skip', 'incomplete'] as $single) { - if (is_array($this->params[$single])) { - $this->params[$single] = $this->params[$single][0] ?? $this->params[$single][1] ?? ''; + $value = $this->params[$single] ?? null; + if (is_array($value)) { + $this->params[$single] = $value[1] ?? $value[0] ?? ''; } } } @@ -218,67 +191,54 @@ public function setParamsFromAttributes($attributes): void { $params = []; foreach ($attributes as $attribute) { - $name = lcfirst(str_replace('Codeception\\Attribute\\', '', (string) $attribute->getName())); + $name = lcfirst(str_replace('Codeception\\Attribute\\', '', (string) $attribute->getName())); + $arguments = $attribute->getArguments(); + if ($attribute->isRepeated()) { - $params[$name] ??= []; - $params[$name][] = $attribute->getArguments(); - continue; + $params[$name][] = $arguments; + } else { + $params[$name] = $arguments; } - $params[$name] = $attribute->getArguments(); } + $this->params = array_merge_recursive($this->params, $params); - // flatten arrays for some attributes foreach (['group', 'env', 'before', 'after', 'prepare'] as $single) { - if (!isset($this->params[$single])) { - continue; - }; - if (!is_array($this->params[$single])) { - continue; - }; - - $this->params[$single] = array_map(fn($a): array => is_array($a) ? $a : [$a], $this->params[$single]); - $this->params[$single] = array_merge(...$this->params[$single]); + if (isset($this->params[$single]) && is_array($this->params[$single])) { + $this->params[$single] = array_merge( + ...array_map(static fn($a): array => (array) $a, $this->params[$single]) + ); + } } $this->setSingularValueForSomeParams(); } - /** - * @deprecated - */ + /** @deprecated */ public function setParams(array $params): void { $this->params = array_merge_recursive($this->params, $params); } - /** - * @param string[] $beforeClassMethods - */ + /** @param string[] $beforeClassMethods */ public function setBeforeClassMethods(array $beforeClassMethods): void { $this->beforeClassMethods = $beforeClassMethods; } - /** - * @return string[] - */ + /** @return string[] */ public function getBeforeClassMethods(): array { return $this->beforeClassMethods; } - /** - * @param string[] $afterClassMethods - */ + /** @param string[] $afterClassMethods */ public function setAfterClassMethods(array $afterClassMethods): void { $this->afterClassMethods = $afterClassMethods; } - /** - * @return string[] - */ + /** @return string[] */ public function getAfterClassMethods(): array { return $this->afterClassMethods; diff --git a/src/Codeception/Test/Test.php b/src/Codeception/Test/Test.php index dd26585eab..27d96fa0bc 100644 --- a/src/Codeception/Test/Test.php +++ b/src/Codeception/Test/Test.php @@ -108,9 +108,7 @@ abstract class Test extends TestWrapper implements TestInterface, Interfaces\Des */ abstract public function test(); - /** - * Test representation - */ + /** Test representation */ abstract public function toString(): string; public function collectCodeCoverage(bool $enabled): void @@ -135,90 +133,68 @@ public function setEventDispatcher(EventDispatcher $eventDispatcher): void final public function realRun(ResultAggregator $result): void { $this->resultAggregator = $result; + $result->addTest($this); + $timer = new Timer(); $status = self::STATUS_PENDING; - $time = 0; - $e = null; - $timer = new Timer(); - - $result->addTest($this); + $time = 0.0; + $e = null; try { $this->fire(Events::TEST_BEFORE, new TestEvent($this)); - - foreach ($this->hooks as $hook) { - if ($hook === 'codeCoverage' && !$this->collectCodeCoverage) { - continue; - } - if (method_exists($this, $hook . 'Start')) { - $this->{$hook . 'Start'}(); - } - } + $this->runHooks('Start'); $failedToStart = false; } catch (\Exception $e) { $failedToStart = true; - $result->addError(new FailEvent($this, $e, $time)); - $this->fire(Events::TEST_ERROR, new FailEvent($this, $e, $time)); + $this->dispatchOutcome(Events::TEST_ERROR, $e, $time); } if (!$this->ignored && !$failedToStart) { Assert::resetCount(); $timer->start(); + try { $this->test(); - $status = self::STATUS_OK; + $status = self::STATUS_OK; $eventType = Events::TEST_SUCCESS; - $this->checkConditionalAsserts($result); } catch (UselessTestException $e) { - $result->addUseless(new FailEvent($this, $e, $time)); - $status = self::STATUS_USELESS; + $status = self::STATUS_USELESS; $eventType = Events::TEST_USELESS; } catch (IncompleteTestError $e) { - $result->addIncomplete(new FailEvent($this, $e, $time)); - $status = self::STATUS_INCOMPLETE; + $status = self::STATUS_INCOMPLETE; $eventType = Events::TEST_INCOMPLETE; } catch (SkippedTest | SkippedTestError $e) { - $result->addSkipped(new FailEvent($this, $e, $time)); - $status = self::STATUS_SKIPPED; + $status = self::STATUS_SKIPPED; $eventType = Events::TEST_SKIPPED; } catch (AssertionFailedError $e) { - $result->addFailure(new FailEvent($this, $e, $time)); - $status = self::STATUS_FAIL; + $status = self::STATUS_FAIL; $eventType = Events::TEST_FAIL; } catch (Exception | Throwable $e) { - $result->addError(new FailEvent($this, $e, $time)); - $status = self::STATUS_ERROR; + $status = self::STATUS_ERROR; $eventType = Events::TEST_ERROR; } $time = $timer->stop()->asSeconds(); - - $this->callTestEndHooks($status, $time, $e); + $this->runHooks('End', true, $status, $time, $e); // We need to get the number of performed assertions _after_ calling the test end hooks because the // AssertionCounter needs to set the number of performed assertions first. $result->addToAssertionCount($this->numberOfAssertionsPerformed()); if ( - $this->reportUselessTests && - $this->numberOfAssertionsPerformed() === 0 && - !$this->doesNotPerformAssertions() && - $eventType === Events::TEST_SUCCESS + $this->reportUselessTests + && $this->numberOfAssertionsPerformed() === 0 + && !$this->doesNotPerformAssertions() + && $eventType === Events::TEST_SUCCESS ) { $eventType = Events::TEST_USELESS; - $e = new UselessTestException('This test did not perform any assertions'); - $result->addUseless(new FailEvent($this, $e, $time)); + $e = new UselessTestException('This test did not perform any assertions'); } - if ($eventType === Events::TEST_SUCCESS) { - $result->addSuccessful($this); - $this->fire($eventType, new TestEvent($this, $time)); - } else { - $this->fire($eventType, new FailEvent($this, $e, $time)); - } + $this->dispatchOutcome($eventType, $e, $time); } else { - $this->callTestEndHooks($status, $time, $e); + $this->runHooks('End', true, $status, $time, $e); } $this->fire(Events::TEST_AFTER, new TestEvent($this, $time)); @@ -236,7 +212,7 @@ protected function doesNotPerformAssertions(): bool public function getResultAggregator(): ResultAggregator { - if (!$this->resultAggregator instanceof ResultAggregator) { + if (!$this->resultAggregator) { throw new LogicException('ResultAggregator is not set'); } return $this->resultAggregator; @@ -263,50 +239,74 @@ public function numberOfAssertionsPerformed(): int return $this->getNumAssertions(); } - protected function fire(string $eventType, TestEvent $event): void { - if (!$this->eventDispatcher instanceof EventDispatcher) { + if (!$this->eventDispatcher) { throw new RuntimeException('EventDispatcher must be injected before running test'); } - $test = $event->getTest(); - foreach ($test->getMetadata()->getGroups() as $group) { - $this->eventDispatcher->dispatch($event, $eventType . '.' . $group); + foreach ($event->getTest()->getMetadata()->getGroups() as $group) { + $this->eventDispatcher->dispatch($event, "$eventType.$group"); } $this->eventDispatcher->dispatch($event, $eventType); } - private function callTestEndHooks(string $status, float $time, ?Throwable $e): void + private function runHooks(string $suffix, bool $reverse = false, mixed ...$args): void { - foreach (array_reverse($this->hooks) as $hook) { + $hooks = $reverse ? array_reverse($this->hooks) : $this->hooks; + foreach ($hooks as $hook) { if ($hook === 'codeCoverage' && !$this->collectCodeCoverage) { continue; } - if (method_exists($this, $hook . 'End')) { - $this->{$hook . 'End'}($status, $time, $e); + $method = $hook . $suffix; + if (method_exists($this, $method)) { + $this->{$method}(...$args); } } } - private function checkConditionalAsserts(ResultAggregator $result): void + private function dispatchOutcome(string $eventType, ?Throwable $e, float $time): void { - if (!method_exists($this, 'getScenario')) { + if ($eventType === Events::TEST_SUCCESS) { + $this->resultAggregator->addSuccessful($this); + $this->fire($eventType, new TestEvent($this, $time)); return; } - $lastFailure = $result->getLastFailure(); - if (!$lastFailure instanceof FailEvent) { + $failEvent = new FailEvent($this, $e, $time); + + $map = [ + Events::TEST_FAIL => 'addFailure', + Events::TEST_ERROR => 'addError', + Events::TEST_USELESS => 'addUseless', + Events::TEST_INCOMPLETE => 'addIncomplete', + Events::TEST_SKIPPED => 'addSkipped', + ]; + + if (isset($map[$eventType])) { + $this->resultAggregator->{$map[$eventType]}($failEvent); + } + + $this->fire($eventType, $failEvent); + } + + private function checkConditionalAsserts(ResultAggregator $result): void + { + if (!method_exists($this, 'getScenario')) { return; } - if (Descriptor::getTestSignatureUnique($lastFailure->getTest()) !== Descriptor::getTestSignatureUnique($this)) { + $last = $result->getLastFailure(); + if ( + !$last instanceof FailEvent + || Descriptor::getTestSignatureUnique($last->getTest()) !== Descriptor::getTestSignatureUnique($this) + ) { return; } foreach ($this->getScenario()?->getSteps() ?? [] as $step) { if ($step->hasFailed()) { $result->popLastFailure(); - throw $lastFailure->getFail(); + throw $last->getFail(); } } } diff --git a/src/Codeception/Test/TestCaseWrapper.php b/src/Codeception/Test/TestCaseWrapper.php index 8f6f7ebc79..51e01e7753 100644 --- a/src/Codeception/Test/TestCaseWrapper.php +++ b/src/Codeception/Test/TestCaseWrapper.php @@ -40,29 +40,29 @@ class TestCaseWrapper extends Test implements Reported, Dependent, StrictCoverag public function __construct( private TestCase $testCase, array $beforeClassMethods = [], - array $afterClassMethods = [], + array $afterClassMethods = [] ) { $this->metadata = new Metadata(); - $metadata = $this->metadata; - - $methodName = PHPUnitVersion::series() < 10 ? $testCase->getName(false) : $testCase->name(); - $metadata->setName($methodName); - $metadata->setFilename((new ReflectionClass($testCase))->getFileName()); + $methodName = PHPUnitVersion::series() < 10 + ? $testCase->getName(false) + : $testCase->name(); + $this->metadata->setName($methodName); + $this->metadata->setFilename((new ReflectionClass($testCase))->getFileName()); if ($testCase->dataName() !== '') { - $metadata->setIndex($testCase->dataName()); + $this->metadata->setIndex($testCase->dataName()); } $classAnnotations = Annotation::forClass($testCase); - $metadata->setParamsFromAnnotations($classAnnotations->raw()); - $metadata->setParamsFromAttributes($classAnnotations->attributes()); + $this->metadata->setParamsFromAnnotations($classAnnotations->raw()); + $this->metadata->setParamsFromAttributes($classAnnotations->attributes()); $methodAnnotations = Annotation::forMethod($testCase, $methodName); - $metadata->setParamsFromAnnotations($methodAnnotations->raw()); - $metadata->setParamsFromAttributes($methodAnnotations->attributes()); + $this->metadata->setParamsFromAnnotations($methodAnnotations->raw()); + $this->metadata->setParamsFromAttributes($methodAnnotations->attributes()); - $metadata->setBeforeClassMethods($beforeClassMethods); - $metadata->setAfterClassMethods($afterClassMethods); + $this->metadata->setBeforeClassMethods($beforeClassMethods); + $this->metadata->setAfterClassMethods($afterClassMethods); } public function __clone(): void @@ -82,96 +82,74 @@ public function getMetadata(): Metadata public function getScenario(): ?Scenario { - if ($this->testCase instanceof Unit) { - return $this->testCase->getScenario(); - } - - return null; + return $this->testCase instanceof Unit + ? $this->testCase->getScenario() + : null; } public function fetchDependencies(): array { - $names = []; - foreach ($this->metadata->getDependencies() as $required) { - if (!str_contains($required, ':') && method_exists($this->testCase::class, $required)) { - $required = $this->testCase::class . ':' . $required; - } - $names[] = $required; - } - return $names; + $class = $this->testCase::class; + return array_map( + static fn(string $dep): string => str_contains($dep, ':') || !method_exists($class, $dep) + ? $dep + : "$class:$dep", + $this->metadata->getDependencies() + ); } - /** - * @return array - */ public function getReportFields(): array { return [ - 'name' => $this->getNameWithDataSet(), - 'class' => $this->testCase::class, - 'file' => $this->metadata->getFilename() + 'name' => $this->getNameWithDataSet(), + 'class' => $this->testCase::class, + 'file' => $this->metadata->getFilename(), ]; } public function getLinesToBeCovered(): array|bool { - $class = $this->testCase::class; - $method = $this->metadata->getName(); - if (PHPUnitVersion::series() < 10) { - return TestUtil::getLinesToBeCovered($class, $method); - } - - if (version_compare(CodeCoverageVersion::id(), '12', '>=')) { - return (new CodeCoverage())->coversTargets($class, $method)->asArray(); + return TestUtil::getLinesToBeCovered($this->testCase::class, $this->metadata->getName()); } - - return (new CodeCoverage())->linesToBeCovered($class, $method); + return $this->coverageTargets('coversTargets', 'linesToBeCovered'); } public function getLinesToBeUsed(): array { - $class = $this->testCase::class; - $method = $this->metadata->getName(); - if (PHPUnitVersion::series() < 10) { - return TestUtil::getLinesToBeUsed($class, $method); + return TestUtil::getLinesToBeUsed($this->testCase::class, $this->metadata->getName()); } - - if (version_compare(CodeCoverageVersion::id(), '12', '>=')) { - return (new CodeCoverage())->usesTargets($class, $method)->asArray(); - } - - return (new CodeCoverage())->linesToBeUsed($class, $method); + return (array) $this->coverageTargets('usesTargets', 'linesToBeUsed'); } public function test(): void { - $dependencyInput = []; - foreach ($this->fetchDependencies() as $dependency) { - $dependencyInput[] = self::$testResults[$dependency] ?? null; - } - $this->testCase->setDependencyInput($dependencyInput); + $inputs = array_map( + fn(string $dep) => self::$testResults[$dep] ?? null, + $this->fetchDependencies() + ); + $this->testCase->setDependencyInput($inputs); $this->testCase->runBare(); - $this->testCase->addToAssertionCount(Assert::getCount()); - if (PHPUnitVersion::series() < 10) { - self::$testResults[$this->getSignature()] = $this->testCase->getResult(); - } else { - self::$testResults[$this->getSignature()] = $this->testCase->result(); - } - - $numberOfAssertionsPerformed = $this->getNumAssertions(); - if (!$this->reportUselessTests || $numberOfAssertionsPerformed <= 0 || !$this->testCase->doesNotPerformAssertions()) { - return; + self::$testResults[$this->getSignature()] = PHPUnitVersion::series() < 10 + ? $this->testCase->getResult() + : $this->testCase->result(); + + $assertions = $this->getNumAssertions(); + if ( + $this->reportUselessTests && + $assertions > 0 && + $this->doesNotPerformAssertions() + ) { + throw new UselessTestException( + sprintf( + 'This test indicates it does not perform assertions but %d assertions were performed', + $assertions + ) + ); } - throw new UselessTestException( - sprintf( - 'This test indicates it does not perform assertions but %d assertions were performed', - $numberOfAssertionsPerformed - ) - ); } /** @@ -179,7 +157,7 @@ public function test(): void */ protected function doesNotPerformAssertions(): bool { - return $this->testCase->doesNotPerformAssertions(); + return $this->testCase->doesNotPerformAssertions(); } public function toString(): string @@ -200,11 +178,17 @@ public function getSignature(): string private function getNameWithDataSet(): string { - if (PHPUnitVersion::series() < 10) { - return $this->testCase->getName(true); - } + return PHPUnitVersion::series() < 10 + ? $this->testCase->getName(true) + : $this->testCase->nameWithDataSet(); + } - return $this->testCase->nameWithDataSet(); + private function coverageTargets(string $newMethod, string $legacyMethod): array|bool + { + $coverage = new CodeCoverage(); + return version_compare(CodeCoverageVersion::id(), '12', '>=') + ? $coverage->$newMethod($this->testCase::class, $this->metadata->getName())->asArray() + : $coverage->$legacyMethod($this->testCase::class, $this->metadata->getName()); } /** @@ -216,10 +200,8 @@ private function getNameWithDataSet(): string */ public function getNumAssertions(): int { - if (PHPUnitVersion::series() < 10) { - return $this->testCase->getNumAssertions(); - } else { - return $this->testCase->numberOfAssertionsPerformed(); - } + return PHPUnitVersion::series() < 10 + ? $this->testCase->getNumAssertions() + : $this->testCase->numberOfAssertionsPerformed(); } } diff --git a/src/Codeception/Test/Unit.php b/src/Codeception/Test/Unit.php index 072dacddcc..47e88208f2 100644 --- a/src/Codeception/Test/Unit.php +++ b/src/Codeception/Test/Unit.php @@ -38,17 +38,12 @@ class Unit extends TestCase implements public function __clone(): void { - if ($this->scenario instanceof Scenario) { - $this->scenario = clone $this->scenario; - } + $this->scenario = $this->scenario instanceof Scenario ? clone $this->scenario : null; } public function getMetadata(): Metadata { - if (!$this->metadata instanceof Metadata) { - $this->metadata = new Metadata(); - } - return $this->metadata; + return $this->metadata ??= new Metadata(); } public function getScenario(): ?Scenario @@ -63,32 +58,37 @@ public function setMetadata(?Metadata $metadata): void public function getResultAggregator(): ResultAggregator { - throw new LogicException('This method should not be called, TestCaseWrapper class must be used instead'); + throw new LogicException('This method should not be called; use TestCaseWrapper instead.'); } protected function _setUp() { - if ($this->getMetadata()->isBlocked()) { - if ($this->getMetadata()->getSkip() !== null) { - $this->markTestSkipped($this->getMetadata()->getSkip()); + $metadata = $this->getMetadata(); + if ($metadata->isBlocked()) { + if ($skip = $metadata->getSkip()) { + $this->markTestSkipped($skip); } - if ($this->getMetadata()->getIncomplete() !== null) { - $this->markTestIncomplete($this->getMetadata()->getIncomplete()); + if ($incomplete = $metadata->getIncomplete()) { + $this->markTestIncomplete($incomplete); } return; } /** @var Di $di */ - $di = $this->getMetadata()->getService('di'); - // auto-inject $tester property - if (($this->getMetadata()->getCurrent('actor')) && ($property = lcfirst((string) Configuration::config()['actor_suffix']))) { - $this->$property = $di->instantiate($this->getMetadata()->getCurrent('actor')); + $di = $metadata->getService('di'); + + // Auto-inject $tester property + if ($actor = $metadata->getCurrent('actor')) { + $suffix = (string) Configuration::config()['actor_suffix']; + $property = lcfirst($suffix); + if (property_exists($this, $property)) { + $this->{$property} = $di->instantiate($actor); + } } $this->scenario = $di->get(Scenario::class); - - // Auto inject into the _inject method - $di->injectDependencies($this); // injecting dependencies + // Auto-inject into the _inject method + $di->injectDependencies($this); $this->_before(); } @@ -136,9 +136,6 @@ public function pause(array $vars = []): void $psy->run(); } - /** - * Returns current values - */ public function getCurrent(?string $current): mixed { return $this->getMetadata()->getCurrent($current); @@ -147,22 +144,20 @@ public function getCurrent(?string $current): mixed public function getReportFields(): array { return [ - 'name' => $this->getName(false), - 'class' => static::class, - 'file' => $this->getMetadata()->getFilename() + 'name' => $this->getName(false), + 'class' => static::class, + 'file' => $this->getMetadata()->getFilename(), ]; } public function fetchDependencies(): array { - $names = []; - foreach ($this->getMetadata()->getDependencies() as $required) { - if (!str_contains((string) $required, ':') && method_exists($this, $required)) { - $required = static::class . ":{$required}"; - } - $names[] = $required; - } - return $names; + return array_map( + fn($dep) => !str_contains((string)$dep, ':') && method_exists($this, $dep) + ? static::class . ":{$dep}" + : $dep, + $this->getMetadata()->getDependencies() + ); } public function getFileName(): string