Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions src/Codeception/Test/Cept.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
}

Expand All @@ -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;
}
Expand Down
66 changes: 32 additions & 34 deletions src/Codeception/Test/Cest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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());
}
Expand All @@ -81,24 +79,26 @@ 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)
);
}
}

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
Expand All @@ -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');
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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');
}
}
131 changes: 55 additions & 76 deletions src/Codeception/Test/DataProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -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;
}
}
Loading