This commit is contained in:
2020-03-27 10:13:51 +07:00
commit da1024a5b3
16614 changed files with 3274282 additions and 0 deletions

View File

@@ -0,0 +1,84 @@
<?php
namespace Codeception\Test;
use Codeception\Exception\TestParseException;
use Codeception\Lib\Parser;
use Codeception\Lib\Console\Message;
/**
* Executes tests delivered in Cept format.
* Prepares metadata, parses test body on preload, and executes a test in `test` method.
*/
class Cept extends Test implements Interfaces\Plain, Interfaces\ScenarioDriven, Interfaces\Reported, Interfaces\Dependent
{
use Feature\ScenarioLoader;
/**
* @var Parser
*/
protected $parser;
public function __construct($name, $file)
{
$metadata = new Metadata();
$metadata->setName($name);
$metadata->setFilename($file);
$this->setMetadata($metadata);
$this->createScenario();
$this->parser = new Parser($this->getScenario(), $this->getMetadata());
}
public function preload()
{
$this->getParser()->prepareToRun($this->getSourceCode());
}
public function test()
{
$scenario = $this->getScenario();
$testFile = $this->getMetadata()->getFilename();
/** @noinspection PhpIncludeInspection */
try {
require $testFile;
} catch (\ParseError $e) {
throw new TestParseException($testFile, $e->getMessage(), $e->getLine());
}
}
public function getSignature()
{
return $this->getMetadata()->getName() . 'Cept';
}
public function toString()
{
return $this->getSignature() . ': ' . Message::ucfirst($this->getFeature());
}
public function getSourceCode()
{
return file_get_contents($this->getFileName());
}
public function getReportFields()
{
return [
'name' => basename($this->getFileName(), 'Cept.php'),
'file' => $this->getFileName(),
'feature' => $this->getFeature()
];
}
/**
* @return Parser
*/
protected function getParser()
{
return $this->parser;
}
public function fetchDependencies()
{
return $this->getMetadata()->getDependencies();
}
}

View File

@@ -0,0 +1,218 @@
<?php
namespace Codeception\Test;
use Codeception\Example;
use Codeception\Lib\Console\Message;
use Codeception\Lib\Parser;
use Codeception\Step\Comment;
use Codeception\Util\Annotation;
use Codeception\Util\ReflectionHelper;
/**
* Executes tests delivered in Cest format.
*
* Handles loading of Cest cases, executing specific methods, following the order from `@before` and `@after` annotations.
*/
class Cest extends Test implements
Interfaces\ScenarioDriven,
Interfaces\Reported,
Interfaces\Dependent,
Interfaces\StrictCoverage
{
use Feature\ScenarioLoader;
/**
* @var Parser
*/
protected $parser;
protected $testClassInstance;
protected $testMethod;
public function __construct($testClass, $methodName, $fileName)
{
$metadata = new Metadata();
$metadata->setName($methodName);
$metadata->setFilename($fileName);
$this->setMetadata($metadata);
$this->testClassInstance = $testClass;
$this->testMethod = $methodName;
$this->createScenario();
$this->parser = new Parser($this->getScenario(), $this->getMetadata());
}
public function preload()
{
$this->scenario->setFeature($this->getSpecFromMethod());
$code = $this->getSourceCode();
$this->parser->parseFeature($code);
$this->getMetadata()->setParamsFromAnnotations(Annotation::forMethod($this->testClassInstance, $this->testMethod)->raw());
$this->getMetadata()->getService('di')->injectDependencies($this->testClassInstance);
// 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));
}
}
public function getSourceCode()
{
$method = new \ReflectionMethod($this->testClassInstance, $this->testMethod);
$start_line = $method->getStartLine() - 1; // it's actually - 1, otherwise you wont get the function() block
$end_line = $method->getEndLine();
$source = file($method->getFileName());
return implode("", array_slice($source, $start_line, $end_line - $start_line));
}
public function getSpecFromMethod()
{
$text = $this->testMethod;
$text = preg_replace('/([A-Z]+)([A-Z][a-z])/', '\\1 \\2', $text);
$text = preg_replace('/([a-z\d])([A-Z])/', '\\1 \\2', $text);
$text = strtolower($text);
return $text;
}
public function test()
{
$actorClass = $this->getMetadata()->getCurrent('actor');
$I = new $actorClass($this->getScenario());
try {
$this->executeHook($I, 'before');
$this->executeBeforeMethods($this->testMethod, $I);
$this->executeTestMethod($I);
$this->executeAfterMethods($this->testMethod, $I);
$this->executeHook($I, 'passed');
} catch (\Exception $e) {
$this->executeHook($I, 'failed');
// fails and errors are now handled by Codeception\PHPUnit\Listener
throw $e;
} finally {
$this->executeHook($I, 'after');
}
}
protected function executeHook($I, $hook)
{
if (is_callable([$this->testClassInstance, "_$hook"])) {
$this->invoke("_$hook", [$I, $this->scenario]);
}
}
protected function executeBeforeMethods($testMethod, $I)
{
$annotations = \PHPUnit\Util\Test::parseTestMethodAnnotations(get_class($this->testClassInstance), $testMethod);
if (!empty($annotations['method']['before'])) {
foreach ($annotations['method']['before'] as $m) {
$this->executeContextMethod(trim($m), $I);
}
}
}
protected function executeAfterMethods($testMethod, $I)
{
$annotations = \PHPUnit\Util\Test::parseTestMethodAnnotations(get_class($this->testClassInstance), $testMethod);
if (!empty($annotations['method']['after'])) {
foreach ($annotations['method']['after'] as $m) {
$this->executeContextMethod(trim($m), $I);
}
}
}
protected function executeContextMethod($context, $I)
{
if (method_exists($this->testClassInstance, $context)) {
$this->executeBeforeMethods($context, $I);
$this->invoke($context, [$I, $this->scenario]);
$this->executeAfterMethods($context, $I);
return;
}
throw new \LogicException(
"Method $context defined in annotation but does not exist in " . get_class($this->testClassInstance)
);
}
protected function invoke($methodName, array $context)
{
foreach ($context as $class) {
$this->getMetadata()->getService('di')->set($class);
}
$this->getMetadata()->getService('di')->injectDependencies($this->testClassInstance, $methodName, $context);
}
protected function executeTestMethod($I)
{
if (!method_exists($this->testClassInstance, $this->testMethod)) {
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;
}
$this->invoke($this->testMethod, [$I, $this->scenario]);
}
public function toString()
{
return sprintf('%s: %s', ReflectionHelper::getClassShortName($this->getTestClass()), Message::ucfirst($this->getFeature()));
}
public function getSignature()
{
return get_class($this->getTestClass()) . ":" . $this->getTestMethod();
}
public function getTestClass()
{
return $this->testClassInstance;
}
public function getTestMethod()
{
return $this->testMethod;
}
/**
* @return array
*/
public function getReportFields()
{
return [
'file' => $this->getFileName(),
'name' => $this->getTestMethod(),
'class' => get_class($this->getTestClass()),
'feature' => $this->getFeature()
];
}
protected function getParser()
{
return $this->parser;
}
public function fetchDependencies()
{
$names = [];
foreach ($this->getMetadata()->getDependencies() as $required) {
if ((strpos($required, ':') === false) and method_exists($this->getTestClass(), $required)) {
$required = get_class($this->getTestClass()) . ":$required";
}
$names[] = $required;
}
return $names;
}
public function getLinesToBeCovered()
{
$class = get_class($this->getTestClass());
$method = $this->getTestMethod();
return \PHPUnit\Util\Test::getLinesToBeCovered($class, $method);
}
public function getLinesToBeUsed()
{
$class = get_class($this->getTestClass());
$method = $this->getTestMethod();
return \PHPUnit\Util\Test::getLinesToBeUsed($class, $method);
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace Codeception\Test;
use Codeception\Test\Interfaces\Descriptive;
use Codeception\Test\Interfaces\Plain;
use Codeception\Util\ReflectionHelper;
class Descriptor
{
/**
* Provides a test name which can be located by
*
* @param \PHPUnit\Framework\SelfDescribing $testCase
* @return string
*/
public static function getTestSignature(\PHPUnit\Framework\SelfDescribing $testCase)
{
if ($testCase instanceof Descriptive) {
return $testCase->getSignature();
}
if ($testCase instanceof \PHPUnit\Framework\TestCase) {
return get_class($testCase) . ':' . $testCase->getName(false);
}
return $testCase->toString();
}
/**
* Provides a test name which is unique for individual iterations of tests using examples
*
* @param \PHPUnit\Framework\SelfDescribing $testCase
* @return string
*/
public static function getTestSignatureUnique(\PHPUnit\Framework\SelfDescribing $testCase)
{
$example = null;
if (method_exists($testCase, 'getMetaData')
&& $example = $testCase->getMetadata()->getCurrent('example')
) {
$example = ':' . substr(sha1(json_encode($example)), 0, 7);
}
return self::getTestSignature($testCase) . $example;
}
public static function getTestAsString(\PHPUnit\Framework\SelfDescribing $testCase)
{
if ($testCase instanceof \PHPUnit\Framework\TestCase) {
$text = $testCase->getName();
$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 = ucfirst(strtolower($text));
$text = str_replace(['::', 'with data set'], [':', '|'], $text);
return ReflectionHelper::getClassShortName($testCase) . ': ' . $text;
}
return $testCase->toString();
}
/**
* Provides a test file name relative to Codeception root
*
* @param \PHPUnit\Framework\SelfDescribing $testCase
* @return mixed
*/
public static function getTestFileName(\PHPUnit\Framework\SelfDescribing $testCase)
{
if ($testCase instanceof Descriptive) {
return codecept_relative_path(realpath($testCase->getFileName()));
}
return (new \ReflectionClass($testCase))->getFileName();
}
/**
* @param \PHPUnit\Framework\SelfDescribing $testCase
* @return mixed|string
*/
public static function getTestFullName(\PHPUnit\Framework\SelfDescribing $testCase)
{
if ($testCase instanceof Plain) {
return self::getTestFileName($testCase);
}
if ($testCase instanceof Descriptive) {
$signature = $testCase->getSignature(); // cut everything before ":" from signature
return self::getTestFileName($testCase) . ':' . preg_replace('~^(.*?):~', '', $signature);
}
if ($testCase instanceof \PHPUnit\Framework\TestCase) {
return self::getTestFileName($testCase) . ':' . $testCase->getName(false);
}
return self::getTestFileName($testCase) . ':' . $testCase->toString();
}
/**
* Provides a test data set index
*
* @param \PHPUnit\Framework\SelfDescribing $testCase
* @return int|null
*/
public static function getTestDataSetIndex(\PHPUnit\Framework\SelfDescribing $testCase)
{
if ($testCase instanceof Descriptive) {
return $testCase->getMetadata()->getIndex();
}
return null;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Codeception\Test\Feature;
trait AssertionCounter
{
protected $numAssertions = 0;
public function getNumAssertions()
{
return $this->numAssertions;
}
protected function assertionCounterStart()
{
\PHPUnit\Framework\Assert::resetCount();
}
protected function assertionCounterEnd()
{
$this->numAssertions = \PHPUnit\Framework\Assert::getCount();
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Codeception\Test\Feature;
use Codeception\Test\Descriptor;
use Codeception\Test\Interfaces\StrictCoverage;
trait CodeCoverage
{
/**
* @return \PHPUnit\Framework\TestResult
*/
abstract public function getTestResultObject();
public function codeCoverageStart()
{
$codeCoverage = $this->getTestResultObject()->getCodeCoverage();
if (!$codeCoverage) {
return;
}
$codeCoverage->start(Descriptor::getTestSignature($this));
}
public function codeCoverageEnd($status, $time)
{
$codeCoverage = $this->getTestResultObject()->getCodeCoverage();
if (!$codeCoverage) {
return;
}
if ($this instanceof StrictCoverage) {
$linesToBeCovered = $this->getLinesToBeCovered();
$linesToBeUsed = $this->getLinesToBeUsed();
} else {
$linesToBeCovered = [];
$linesToBeUsed = [];
}
try {
$codeCoverage->stop(true, $linesToBeCovered, $linesToBeUsed);
} catch (\PHP_CodeCoverage_Exception $cce) {
if ($status === \Codeception\Test\Test::STATUS_OK) {
$this->getTestResultObject()->addError($this, $cce, $time);
}
}
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Codeception\Test\Feature;
use Codeception\Test\Test as CodeceptionTest;
trait ErrorLogger
{
/**
* @return \PHPUnit\Framework\TestResult
*/
abstract public function getTestResultObject();
public function errorLoggerEnd($status, $time, $exception = null)
{
if (!$exception) {
return;
}
if ($status === CodeceptionTest::STATUS_ERROR) {
$this->getTestResultObject()->addError($this, $exception, $time);
}
if ($status === CodeceptionTest::STATUS_FAIL) {
$this->getTestResultObject()->addFailure($this, $exception, $time);
}
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Codeception\Test\Feature;
use Codeception\Test\Metadata;
trait IgnoreIfMetadataBlocked
{
/**
* @return Metadata
*/
abstract protected function getMetadata();
abstract protected function ignore($ignored);
/**
* @return \PHPUnit\Framework\TestResult
*/
abstract protected function getTestResultObject();
protected function ignoreIfMetadataBlockedStart()
{
if (!$this->getMetadata()->isBlocked()) {
return;
}
$this->ignore(true);
if ($this->getMetadata()->getSkip() !== null) {
$this->getTestResultObject()->addFailure($this, new \PHPUnit\Framework\SkippedTestError((string)$this->getMetadata()->getSkip()), 0);
return;
}
if ($this->getMetadata()->getIncomplete() !== null) {
$this->getTestResultObject()->addFailure($this, new \PHPUnit\Framework\IncompleteTestError((string)$this->getMetadata()->getIncomplete()), 0);
return;
}
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Codeception\Test\Feature;
use Codeception\Test\Metadata;
trait MetadataCollector
{
/**
* @var Metadata
*/
private $metadata;
protected function setMetadata(Metadata $metadata)
{
$this->metadata = $metadata;
}
public function getMetadata()
{
return $this->metadata;
}
public function getName()
{
return $this->getMetadata()->getName();
}
public function getFileName()
{
return $this->getMetadata()->getFilename();
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Codeception\Test\Feature;
use Codeception\Lib\Parser;
use Codeception\Scenario;
use Codeception\Test\Metadata;
trait ScenarioLoader
{
/**
* @var Scenario
*/
private $scenario;
/**
* @return Metadata
*/
abstract public function getMetadata();
protected function createScenario()
{
$this->scenario = new Scenario($this);
}
/**
* @return Scenario
*/
public function getScenario()
{
return $this->scenario;
}
public function getFeature()
{
return $this->getScenario()->getFeature();
}
public function getScenarioText($format = 'text')
{
$code = $this->getSourceCode();
$this->getParser()->parseFeature($code);
$this->getParser()->parseSteps($code);
if ($format == 'html') {
return $this->getScenario()->getHtml();
}
return $this->getScenario()->getText();
}
/**
* @return Parser
*/
abstract protected function getParser();
abstract public function getSourceCode();
}

View File

@@ -0,0 +1,221 @@
<?php
namespace Codeception\Test;
use Behat\Gherkin\Node\FeatureNode;
use Behat\Gherkin\Node\ScenarioNode;
use Behat\Gherkin\Node\ScenarioInterface;
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;
use Codeception\Step\Meta;
use Codeception\Test\Interfaces\Reported;
use Codeception\Test\Interfaces\ScenarioDriven;
class Gherkin extends Test implements ScenarioDriven, Reported
{
protected $steps = [];
/**
* @var FeatureNode
*/
protected $featureNode;
/**
* @var ScenarioNode
*/
protected $scenarioNode;
/**
* @var Scenario
*/
protected $scenario;
public function __construct(FeatureNode $featureNode, ScenarioInterface $scenarioNode, $steps = [])
{
$this->featureNode = $featureNode;
$this->scenarioNode = $scenarioNode;
$this->steps = $steps;
$this->setMetadata(new Metadata());
$this->scenario = new Scenario($this);
$this->getMetadata()->setName($featureNode->getTitle());
$this->getMetadata()->setFeature($scenarioNode->getTitle());
$this->getMetadata()->setFilename($featureNode->getFile());
}
public function preload()
{
$this->getMetadata()->setGroups($this->featureNode->getTags());
$this->getMetadata()->setGroups($this->scenarioNode->getTags());
$this->scenario->setMetaStep(null);
if ($background = $this->featureNode->getBackground()) {
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");
}
}
public function getSignature()
{
return basename($this->getFileName(), '.feature') . ':' . $this->getFeature();
}
public function test()
{
$this->makeContexts();
$description = explode("\n", $this->featureNode->getDescription());
foreach ($description as $line) {
$this->getScenario()->runStep(new Comment($line));
}
if ($background = $this->featureNode->getBackground()) {
foreach ($background->getSteps() as $step) {
$this->runStep($step);
}
}
foreach ($this->scenarioNode->getSteps() as $step) {
$this->runStep($step);
}
}
protected function validateStep(StepNode $stepNode)
{
$stepText = $stepNode->getText();
if (GherkinSnippets::stepHasPyStringArgument($stepNode)) {
$stepText .= ' ""';
}
foreach ($this->steps as $pattern => $context) {
$res = preg_match($pattern, $stepText);
if (!$res) {
continue;
}
return;
}
$incomplete = $this->getMetadata()->getIncomplete();
$this->getMetadata()->setIncomplete("$incomplete\nStep definition for `$stepText` not found in contexts");
}
protected function runStep(StepNode $stepNode)
{
$params = [];
if ($stepNode->hasArguments()) {
$args = $stepNode->getArguments();
$table = $args[0];
if ($table instanceof TableNode) {
$params = [$table->getTableAsString()];
}
}
$meta = new Meta($stepNode->getText(), $params);
$meta->setPrefix($stepNode->getKeyword());
$this->scenario->setMetaStep($meta); // enable metastep
$stepText = $stepNode->getText();
$hasPyStringArg = GherkinSnippets::stepHasPyStringArgument($stepNode);
if ($hasPyStringArg) {
// pretend it is inline argument
$stepText .= ' ""';
}
$this->getScenario()->comment(null); // make metastep to be printed even if no steps in it
foreach ($this->steps as $pattern => $context) {
$matches = [];
if (!preg_match($pattern, $stepText, $matches)) {
continue;
}
array_shift($matches);
if ($hasPyStringArg) {
// get rid off last fake argument
array_pop($matches);
}
if ($stepNode->hasArguments()) {
$matches = array_merge($matches, $stepNode->getArguments());
}
call_user_func_array($context, $matches); // execute the step
break;
}
$this->scenario->setMetaStep(null); // disable metastep
}
protected function makeContexts()
{
/** @var $di Di **/
$di = $this->getMetadata()->getService('di');
$di->set($this->getScenario());
$actorClass = $this->getMetadata()->getCurrent('actor');
if ($actorClass) {
$di->set(new $actorClass($this->getScenario()));
}
foreach ($this->steps as $pattern => $step) {
$di->instantiate($step[0]);
$this->steps[$pattern][0] = $di->get($step[0]);
}
}
public function toString()
{
return $this->featureNode->getTitle() . ': ' . $this->getFeature();
}
public function getFeature()
{
return $this->getMetadata()->getFeature();
}
/**
* @return \Codeception\Scenario
*/
public function getScenario()
{
return $this->scenario;
}
public function getScenarioText($format = 'text')
{
return file_get_contents($this->getFileName());
}
public function getSourceCode()
{
}
/**
* @return ScenarioNode
*/
public function getScenarioNode()
{
return $this->scenarioNode;
}
/**
* @return FeatureNode
*/
public function getFeatureNode()
{
return $this->featureNode;
}
/**
* Field values for XML/JSON/TAP reports
*
* @return array
*/
public function getReportFields()
{
return [
'file' => $this->getFileName(),
'name' => $this->toString(),
'feature' => $this->getFeature()
];
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Codeception\Test\Interfaces;
interface Dependent
{
public function fetchDependencies();
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Codeception\Test\Interfaces;
interface Descriptive extends \PHPUnit\Framework\SelfDescribing
{
public function getFileName();
public function getSignature();
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Codeception\Test\Interfaces;
/**
* TestCases that do not follow OOP
*/
interface Plain
{
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Codeception\Test\Interfaces;
interface Reported
{
/**
* Field values for XML/JSON/TAP reports
*
* @return array
*/
public function getReportFields();
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Codeception\Test\Interfaces;
interface ScenarioDriven
{
public function getFeature();
/**
* @return \Codeception\Scenario
*/
public function getScenario();
public function getScenarioText($format = 'text');
public function preload();
public function getSourceCode();
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Codeception\Test\Interfaces;
interface StrictCoverage
{
public function getLinesToBeCovered();
public function getLinesToBeUsed();
}

View File

@@ -0,0 +1,144 @@
<?php
namespace Codeception\Test;
use Codeception\Test\Loader\Cept as CeptLoader;
use Codeception\Test\Loader\Cest as CestLoader;
use Codeception\Test\Loader\Unit as UnitLoader;
use Codeception\Test\Loader\Gherkin as GherkinLoader;
use Symfony\Component\Finder\Finder;
/**
* Loads all Codeception supported test formats from a directory.
*
* ``` php
* <?php
* $testLoader = new \Codeception\TestLoader('tests/unit');
* $testLoader->loadTests();
* $tests = $testLoader->getTests();
* ?>
* ```
* You can load specific file
*
* ``` php
* <?php
* $testLoader = new \Codeception\TestLoader('tests/unit');
* $testLoader->loadTest('UserTest.php');
* $testLoader->loadTest('PostTest.php');
* $tests = $testLoader->getTests();
* ?>
* ```
* or a subdirectory
*
* ``` php
* <?php
* $testLoader = new \Codeception\TestLoader('tests/unit');
* $testLoader->loadTest('models'); // all tests from tests/unit/models
* $tests = $testLoader->getTests();
* ?>
* ```
*
*/
class Loader
{
protected $formats = [];
protected $tests = [];
protected $path;
public function __construct(array $suiteSettings)
{
$this->path = $suiteSettings['path'];
$this->formats = [
new CeptLoader(),
new CestLoader(),
new UnitLoader(),
new GherkinLoader($suiteSettings)
];
if (isset($suiteSettings['formats'])) {
foreach ($suiteSettings['formats'] as $format) {
$this->formats[] = new $format($suiteSettings);
}
}
}
public function getTests()
{
return $this->tests;
}
protected function relativeName($file)
{
return str_replace([$this->path, '\\'], ['', '/'], $file);
}
protected function findPath($path)
{
if (!file_exists($path)
&& substr($path, -strlen('.php')) !== '.php'
&& file_exists($newPath = $path . '.php')
) {
return $newPath;
}
return $path;
}
protected function makePath($originalPath)
{
$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");
}
return $path;
}
public function loadTest($path)
{
$path = $this->makePath($path);
foreach ($this->formats as $format) {
/** @var $format Loader **/
if (preg_match($format->getPattern(), $path)) {
$format->loadTests($path);
$this->tests = $format->getTests();
return;
}
}
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($fileName = null)
{
if ($fileName) {
return $this->loadTest($fileName);
}
$finder = Finder::create()->files()->sortByName()->in($this->path)->followLinks();
foreach ($this->formats as $format) {
/** @var $format Loader **/
$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());
}
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Codeception\Test\Loader;
use Codeception\Lib\Parser;
use Codeception\Test\Cept as CeptFormat;
class Cept implements LoaderInterface
{
protected $tests = [];
public function getPattern()
{
return '~Cept\.php$~';
}
function loadTests($file)
{
Parser::validate($file);
$name = basename($file, 'Cept.php');
$cept = new CeptFormat($name, $file);
$this->tests[] = $cept;
}
public function getTests()
{
return $this->tests;
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace Codeception\Test\Loader;
use Codeception\Exception\TestParseException;
use Codeception\Lib\Parser;
use Codeception\Test\Cest as CestFormat;
use Codeception\Util\Annotation;
use Codeception\Util\ReflectionHelper;
class Cest implements LoaderInterface
{
protected $tests = [];
public function getTests()
{
return $this->tests;
}
public function getPattern()
{
return '~Cest\.php$~';
}
public function loadTests($file)
{
Parser::load($file);
$testClasses = Parser::getClassesFromFile($file);
foreach ($testClasses as $testClass) {
if (substr($testClass, -strlen('Cest')) !== 'Cest') {
continue;
}
if (!(new \ReflectionClass($testClass))->isInstantiable()) {
continue;
}
$unit = new $testClass;
$methods = get_class_methods($testClass);
foreach ($methods as $method) {
if (strpos($method, '_') === 0) {
continue;
}
$examples = [];
// example Annotation
$rawExamples = Annotation::forMethod($unit, $method)->fetchAll('example');
if (count($rawExamples)) {
$examples = array_map(
function ($v) {
return Annotation::arrayValue($v);
},
$rawExamples
);
}
// dataProvider Annotation
$dataMethod = Annotation::forMethod($testClass, $method)->fetch('dataProvider');
// lowercase for back compatible
if (empty($dataMethod)) {
$dataMethod = Annotation::forMethod($testClass, $method)->fetch('dataprovider');
}
if (!empty($dataMethod)) {
try {
$data = ReflectionHelper::invokePrivateMethod($unit, $dataMethod);
// allow to mix example and dataprovider annotations
$examples = array_merge($examples, $data);
} catch (\ReflectionException $e) {
throw new TestParseException(
$file,
"DataProvider '$dataMethod' for $testClass->$method is invalid or not callable.\n" .
"Make sure that the dataprovider exist within the test class."
);
}
}
if (count($examples)) {
$dataProvider = new \PHPUnit\Framework\DataProviderTestSuite();
$index = 0;
foreach ($examples as $k => $example) {
if ($example === null) {
throw new TestParseException(
$file,
"Example for $testClass->$method contains invalid data:\n" .
$rawExamples[$k] . "\n" .
"Make sure this is a valid JSON (Hint: \"-char for strings) or a single-line annotation in Doctrine-style"
);
}
$test = new CestFormat($unit, $method, $file);
$test->getMetadata()->setCurrent(['example' => $example]);
$test->getMetadata()->setIndex($index);
$dataProvider->addTest($test);
$index++;
}
$this->tests[] = $dataProvider;
continue;
}
$this->tests[] = new CestFormat($unit, $method, $file);
}
}
}
}

View File

@@ -0,0 +1,198 @@
<?php
namespace Codeception\Test\Loader;
use Behat\Gherkin\Filter\RoleFilter;
use Behat\Gherkin\Keywords\ArrayKeywords as GherkinKeywords;
use Behat\Gherkin\Lexer as GherkinLexer;
use Behat\Gherkin\Node\ExampleNode;
use Behat\Gherkin\Node\OutlineNode;
use Behat\Gherkin\Node\ScenarioInterface;
use Behat\Gherkin\Node\ScenarioNode;
use Behat\Gherkin\Parser as GherkinParser;
use Codeception\Configuration;
use Codeception\Exception\ParseException;
use Codeception\Exception\TestParseException;
use Codeception\Test\Gherkin as GherkinFormat;
use Codeception\Util\Annotation;
class Gherkin implements LoaderInterface
{
protected static $defaultSettings = [
'namespace' => '',
'actor' => '',
'gherkin' => [
'contexts' => [
'default' => [],
'tag' => [],
'role' => []
]
]
];
protected $tests = [];
/**
* @var GherkinParser
*/
protected $parser;
protected $settings = [];
protected $steps = [];
public function __construct($settings = [])
{
$this->settings = Configuration::mergeConfigs(self::$defaultSettings, $settings);
if (!class_exists('Behat\Gherkin\Keywords\ArrayKeywords')) {
throw new TestParseException('Feature file can only be parsed with Behat\Gherkin library. Please install `behat/gherkin` with Composer');
}
$gherkin = new \ReflectionClass('Behat\Gherkin\Gherkin');
$gherkinClassPath = dirname($gherkin->getFileName());
$i18n = require $gherkinClassPath . '/../../../i18n.php';
$keywords = new GherkinKeywords($i18n);
$lexer = new GherkinLexer($keywords);
$this->parser = new GherkinParser($lexer);
$this->fetchGherkinSteps();
}
protected function fetchGherkinSteps()
{
$contexts = $this->settings['gherkin']['contexts'];
foreach ($contexts['tag'] as $tag => $tagContexts) {
$this->addSteps($tagContexts, "tag:$tag");
}
foreach ($contexts['role'] as $role => $roleContexts) {
$this->addSteps($roleContexts, "role:$role");
}
if (empty($this->steps) && empty($contexts['default']) && $this->settings['actor']) { // if no context is set, actor to be a context
$actorContext = $this->settings['namespace']
? rtrim($this->settings['namespace'] . '\\' . $this->settings['actor'], '\\')
: $this->settings['actor'];
if ($actorContext) {
$contexts['default'][] = $actorContext;
}
}
$this->addSteps($contexts['default']);
}
protected function addSteps(array $contexts, $group = 'default')
{
$this->steps[$group] = [];
foreach ($contexts as $context) {
$methods = get_class_methods($context);
if (!$methods) {
continue;
}
foreach ($methods as $method) {
$annotation = Annotation::forMethod($context, $method);
foreach (['Given', 'When', 'Then'] as $type) {
$patterns = $annotation->fetchAll($type);
foreach ($patterns as $pattern) {
if (!$pattern) {
continue;
}
$this->validatePattern($pattern);
$pattern = $this->makePlaceholderPattern($pattern);
$this->steps[$group][$pattern] = [$context, $method];
}
}
}
}
}
public function makePlaceholderPattern($pattern)
{
if (isset($this->settings['describe_steps'])) {
return $pattern;
}
if (strpos($pattern, '/') !== 0) {
$pattern = preg_quote($pattern);
$pattern = preg_replace('~(\w+)\/(\w+)~', '(?:$1|$2)', $pattern); // or
$pattern = preg_replace('~\\\\\((\w)\\\\\)~', '$1?', $pattern); // (s)
$replacePattern = sprintf(
'(?|\"%s\"|%s)',
"((?|[^\"\\\\\\]|\\\\\\.)*?)", // matching escaped string in ""
'[\D]{0,1}([\d\,\.]+)[\D]{0,1}'
); // or matching numbers with optional $ or € chars
// params converting from :param to match 11 and "aaa" and "aaa\"aaa"
$pattern = preg_replace('~"?\\\:(\w+)"?~', $replacePattern, $pattern);
$pattern = "/^$pattern$/u";
// validating this pattern is slow, so we skip it now
}
return $pattern;
}
private function validatePattern($pattern)
{
if (strpos($pattern, '/') !== 0) {
return; // not a user-regex but a string with placeholder
}
if (@preg_match($pattern, ' ') === false) {
throw new ParseException("Loading Gherkin step with regex\n \n$pattern\n \nfailed. This regular expression is invalid.");
}
}
public function loadTests($filename)
{
$featureNode = $this->parser->parse(file_get_contents($filename), $filename);
if (!$featureNode) {
return;
}
foreach ($featureNode->getScenarios() as $scenarioNode) {
/** @var $scenarioNode ScenarioInterface **/
$steps = $this->steps['default']; // load default context
foreach (array_merge($scenarioNode->getTags(), $featureNode->getTags()) as $tag) { // load tag contexts
if (isset($this->steps["tag:$tag"])) {
$steps = array_merge($steps, $this->steps["tag:$tag"]);
}
}
$roles = $this->settings['gherkin']['contexts']['role']; // load role contexts
foreach ($roles as $role => $context) {
$filter = new RoleFilter($role);
if ($filter->isFeatureMatch($featureNode)) {
$steps = array_merge($steps, $this->steps["role:$role"]);
break;
}
}
if ($scenarioNode instanceof OutlineNode) {
foreach ($scenarioNode->getExamples() as $example) {
/** @var $example ExampleNode **/
$params = implode(', ', $example->getTokens());
$exampleNode = new ScenarioNode($scenarioNode->getTitle() . " | $params", $scenarioNode->getTags(), $example->getSteps(), $example->getKeyword(), $example->getLine());
$this->tests[] = new GherkinFormat($featureNode, $exampleNode, $steps);
}
continue;
}
$this->tests[] = new GherkinFormat($featureNode, $scenarioNode, $steps);
}
}
public function getTests()
{
return $this->tests;
}
public function getPattern()
{
return '~\.feature$~';
}
/**
* @return array
*/
public function getSteps()
{
return $this->steps;
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Codeception\Test\Loader;
interface LoaderInterface
{
public function loadTests($filename);
public function getTests();
public function getPattern();
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Codeception\Test\Loader;
use Codeception\Lib\Parser;
use Codeception\Test\Descriptor;
use Codeception\Test\Unit as UnitFormat;
use Codeception\Util\Annotation;
class Unit implements LoaderInterface
{
protected $tests = [];
public function getPattern()
{
return '~Test\.php$~';
}
public function loadTests($path)
{
Parser::load($path);
$testClasses = Parser::getClassesFromFile($path);
foreach ($testClasses as $testClass) {
$reflected = new \ReflectionClass($testClass);
if (!$reflected->isInstantiable()) {
continue;
}
foreach ($reflected->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
$test = $this->createTestFromPhpUnitMethod($reflected, $method);
if (!$test) {
continue;
}
$this->tests[] = $test;
}
}
}
public function getTests()
{
return $this->tests;
}
protected function createTestFromPhpUnitMethod(\ReflectionClass $class, \ReflectionMethod $method)
{
if (!\PHPUnit\Framework\TestSuite::isTestMethod($method)) {
return;
}
$test = \PHPUnit\Framework\TestSuite::createTest($class, $method->name);
if ($test instanceof \PHPUnit\Framework\DataProviderTestSuite) {
foreach ($test->tests() as $t) {
$this->enhancePhpunitTest($t);
}
return $test;
}
$this->enhancePhpunitTest($test);
return $test;
}
protected function enhancePhpunitTest(\PHPUnit\Framework\Test $test)
{
$className = get_class($test);
$methodName = $test->getName(false);
$dependencies = \PHPUnit\Util\Test::getDependencies($className, $methodName);
$test->setDependencies($dependencies);
if ($test instanceof UnitFormat) {
$test->getMetadata()->setParamsFromAnnotations(Annotation::forMethod($test, $methodName)->raw());
$test->getMetadata()->setFilename(Descriptor::getTestFileName($test));
}
}
}

View File

@@ -0,0 +1,262 @@
<?php
namespace Codeception\Test;
use Codeception\Exception\InjectionException;
use Codeception\Util\Annotation;
class Metadata
{
protected $name;
protected $filename;
protected $feature;
protected $index;
protected $params = [
'env' => [],
'group' => [],
'depends' => [],
'skip' => null,
'incomplete' => null
];
protected $current = [];
protected $services = [];
protected $reports = [];
/**
* @return mixed
*/
public function getEnv()
{
return $this->params['env'];
}
/**
* @return array
*/
public function getGroups()
{
return array_unique($this->params['group']);
}
/**
* @param mixed $groups
*/
public function setGroups($groups)
{
$this->params['group'] = array_merge($this->params['group'], $groups);
}
/**
* @return mixed
*/
public function getSkip()
{
return $this->params['skip'];
}
/**
* @param mixed $skip
*/
public function setSkip($skip)
{
$this->params['skip'] = $skip;
}
/**
* @return mixed
*/
public function getIncomplete()
{
return $this->params['incomplete'];
}
/**
* @param mixed $incomplete
*/
public function setIncomplete($incomplete)
{
$this->params['incomplete'] = $incomplete;
}
/**
* @param string|null $key
* @return mixed
*/
public function getCurrent($key = null)
{
if ($key) {
if (isset($this->current[$key])) {
return $this->current[$key];
}
if ($key === 'name') {
return $this->getName();
}
return null;
}
return $this->current;
}
public function setCurrent(array $currents)
{
$this->current = array_merge($this->current, $currents);
}
/**
* @return mixed
*/
public function getName()
{
return $this->name;
}
/**
* @param mixed $name
*/
public function setName($name)
{
$this->name = $name;
}
/**
* @return mixed
*/
public function getFilename()
{
return $this->filename;
}
/**
* @param mixed $index
*/
public function setIndex($index)
{
$this->index = $index;
}
/**
* @return mixed
*/
public function getIndex()
{
return $this->index;
}
/**
* @param mixed $filename
*/
public function setFilename($filename)
{
$this->filename = $filename;
}
/**
* @return array
*/
public function getDependencies()
{
return $this->params['depends'];
}
public function isBlocked()
{
return $this->getSkip() !== null || $this->getIncomplete() !== null;
}
/**
* @return mixed
*/
public function getFeature()
{
return $this->feature;
}
/**
* @param mixed $feature
*/
public function setFeature($feature)
{
$this->feature = $feature;
}
/**
* @param $service
* @return array
* @throws InjectionException
*/
public function getService($service)
{
if (!isset($this->services[$service])) {
throw new InjectionException("Service $service is not defined and can't be accessed from a test");
}
return $this->services[$service];
}
/**
* @param array $services
*/
public function setServices($services)
{
$this->services = $services;
}
/**
* Returns all test reports
* @return array
*/
public function getReports()
{
return $this->reports;
}
/**
* @param $type
* @param $report
*/
public function addReport($type, $report)
{
$this->reports[$type] = $report;
}
/**
* Returns test params like: env, group, skip, incomplete, etc
* Can return by annotation or return all if no key passed
*
* @param null $key
* @return array|mixed|null
*/
public function getParam($key = null)
{
if ($key) {
if (isset($this->params[$key])) {
return $this->params[$key];
}
return null;
}
return $this->params;
}
/**
* @param mixed $annotations
*/
public function setParamsFromAnnotations($annotations)
{
$params = Annotation::fetchAllAnnotationsFromDocblock($annotations);
$this->params = array_merge_recursive($this->params, $params);
// set singular value for some params
foreach (['skip', 'incomplete'] as $single) {
$this->params[$single] = empty($this->params[$single]) ? null : (string) $this->params[$single][0];
}
}
/**
* @param $params
*/
public function setParams($params)
{
$this->params = array_merge_recursive($this->params, $params);
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace Codeception\Test;
use Codeception\TestInterface;
use Codeception\Util\ReflectionHelper;
use SebastianBergmann\Timer\Timer;
/**
* The most simple testcase (with only one test in it) which can be executed by PHPUnit/Codeception.
* It can be extended with included traits. Turning on/off a trait should not break class functionality.
*
* Class has exactly one method to be executed for testing, wrapped with before/after callbacks delivered from included traits.
* A trait providing before/after callback should contain corresponding protected methods: `{traitName}Start` and `{traitName}End`,
* then this trait should be enabled in `hooks` property.
*
* Inherited class must implement `test` method.
*/
abstract class Test implements TestInterface, Interfaces\Descriptive
{
use Feature\AssertionCounter;
use Feature\CodeCoverage;
use Feature\ErrorLogger;
use Feature\MetadataCollector;
use Feature\IgnoreIfMetadataBlocked;
private $testResult;
private $ignored = false;
/**
* Enabled traits with methods to be called before and after the test.
*
* @var array
*/
protected $hooks = [
'ignoreIfMetadataBlocked',
'codeCoverage',
'assertionCounter',
'errorLogger'
];
const STATUS_FAIL = 'fail';
const STATUS_ERROR = 'error';
const STATUS_OK = 'ok';
const STATUS_PENDING = 'pending';
/**
* Everything inside this method is treated as a test.
*
* @return mixed
*/
abstract public function test();
/**
* Test representation
*
* @return mixed
*/
abstract public function toString();
/**
* Runs a test and collects its result in a TestResult instance.
* Executes before/after hooks coming from traits.
*
* @param \PHPUnit\Framework\TestResult $result
* @return \PHPUnit\Framework\TestResult
*/
final public function run(\PHPUnit\Framework\TestResult $result = null)
{
$this->testResult = $result;
$status = self::STATUS_PENDING;
$time = 0;
$e = null;
$result->startTest($this);
foreach ($this->hooks as $hook) {
if (method_exists($this, $hook.'Start')) {
$this->{$hook.'Start'}();
}
}
$failedToStart = ReflectionHelper::readPrivateProperty($result, 'lastTestFailed');
if (!$this->ignored && !$failedToStart) {
Timer::start();
try {
$this->test();
$status = self::STATUS_OK;
} catch (\PHPUnit\Framework\AssertionFailedError $e) {
$status = self::STATUS_FAIL;
} catch (\PHPUnit\Framework\Exception $e) {
$status = self::STATUS_ERROR;
} catch (\Throwable $e) {
$e = new \PHPUnit\Framework\ExceptionWrapper($e);
$status = self::STATUS_ERROR;
} catch (\Exception $e) {
$e = new \PHPUnit\Framework\ExceptionWrapper($e);
$status = self::STATUS_ERROR;
}
$time = Timer::stop();
}
foreach (array_reverse($this->hooks) as $hook) {
if (method_exists($this, $hook.'End')) {
$this->{$hook.'End'}($status, $time, $e);
}
}
$result->endTest($this, $time);
return $result;
}
public function getTestResultObject()
{
return $this->testResult;
}
/**
* This class represents exactly one test
* @return int
*/
public function count()
{
return 1;
}
/**
* Should a test be skipped (can be set from hooks)
*
* @param boolean $ignored
*/
protected function ignore($ignored)
{
$this->ignored = $ignored;
}
}

View File

@@ -0,0 +1,172 @@
<?php
namespace Codeception\Test;
use Codeception\Configuration;
use Codeception\Exception\ModuleException;
use Codeception\Lib\Di;
use Codeception\Lib\Notification;
use Codeception\Scenario;
use Codeception\TestInterface;
/**
* Represents tests from PHPUnit compatible format.
*/
class Unit extends \PHPUnit\Framework\TestCase implements
Interfaces\Reported,
Interfaces\Dependent,
TestInterface
{
use \Codeception\Test\Feature\Stub;
/**
* @var Metadata
*/
private $metadata;
public function getMetadata()
{
if (!$this->metadata) {
$this->metadata = new Metadata();
}
return $this->metadata;
}
protected function setUp()
{
if ($this->getMetadata()->isBlocked()) {
if ($this->getMetadata()->getSkip() !== null) {
$this->markTestSkipped($this->getMetadata()->getSkip());
}
if ($this->getMetadata()->getIncomplete() !== null) {
$this->markTestIncomplete($this->getMetadata()->getIncomplete());
}
return;
}
/** @var $di Di **/
$di = $this->getMetadata()->getService('di');
$di->set(new Scenario($this));
// auto-inject $tester property
if (($this->getMetadata()->getCurrent('actor')) && ($property = lcfirst(Configuration::config()['actor_suffix']))) {
$this->$property = $di->instantiate($this->getMetadata()->getCurrent('actor'));
}
// Auto inject into the _inject method
$di->injectDependencies($this); // injecting dependencies
$this->_before();
}
/**
* @Override
*/
protected function _before()
{
}
protected function tearDown()
{
$this->_after();
}
/**
* @Override
*/
protected function _after()
{
}
/**
* If the method exists (PHPUnit 5) forward the call to the parent class, otherwise
* call `expectException` instead (PHPUnit 6)
*/
public function setExpectedException($exception, $message = null, $code = null)
{
if (is_callable('parent::setExpectedException')) {
parent::setExpectedException($exception, $message, $code);
} else {
Notification::deprecate('PHPUnit\Framework\TestCase::setExpectedException deprecated in favor of expectException, expectExceptionMessage, and expectExceptionCode');
$this->expectException($exception);
if ($message !== null) {
$this->expectExceptionMessage($message);
}
if ($code !== null) {
$this->expectExceptionCode($code);
}
}
}
/**
* @param $module
* @return \Codeception\Module
* @throws ModuleException
*/
public function getModule($module)
{
$modules = $this->getMetadata()->getCurrent('modules');
if (!isset($modules[$module])) {
throw new ModuleException($module, "Module can't be accessed");
}
return $modules[$module];
}
/**
* Returns current values
*/
public function getCurrent($current)
{
return $this->getMetadata()->getCurrent($current);
}
/**
* @return array
*/
public function getReportFields()
{
return [
'name' => $this->getName(),
'class' => get_class($this),
'file' => $this->getMetadata()->getFilename()
];
}
public function fetchDependencies()
{
$names = [];
foreach ($this->getMetadata()->getDependencies() as $required) {
if ((strpos($required, ':') === false) and method_exists($this, $required)) {
$required = get_class($this) . ":$required";
}
$names[] = $required;
}
return $names;
}
/**
* Reset PHPUnit's dependencies
* @return bool
*/
public function handleDependencies()
{
$dependencies = $this->fetchDependencies();
if (empty($dependencies)) {
return true;
}
$passed = $this->getTestResultObject()->passed();
$dependencyInput = [];
foreach ($dependencies as $dependency) {
$dependency = str_replace(':', '::', $dependency); // Codeception => PHPUnit format
if (strpos($dependency, '::') === false) { // check it is method of same class
$dependency = get_class($this) . '::' . $dependency;
}
if (isset($passed[$dependency])) {
$dependencyInput[] = $passed[$dependency]['result'];
} else {
$dependencyInput[] = null;
}
}
$this->setDependencyInput($dependencyInput);
return true;
}
}