uawdijnntqw1x1x1
IP : 216.73.216.136
Hostname : dhandapanilive
Kernel : Linux dhandapanilive 5.15.0-139-generic #149~20.04.1-Ubuntu SMP Wed Apr 16 08:29:56 UTC 2025 x86_64
Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
OS : Linux
PATH:
/
var
/
www
/
html
/
dhandapani
/
d6e06
/
..
/
setup
/
..
/
9245e
/
codeception.tar
/
/
phpunit-wrapper/src/Log/JUnit.php000077700000003662151323602320012776 0ustar00<?php namespace Codeception\PHPUnit\Log; use Codeception\Configuration; use Codeception\Test\Interfaces\Reported; use Codeception\Test\Test; use PHPUnit\Framework\TestCase; class JUnit extends \Codeception\PHPUnit\NonFinal\JUnit { protected $strictAttributes = ['file', 'name', 'class']; public function startTest(\PHPUnit\Framework\Test $test):void { if (!$test instanceof Reported) { parent::startTest($test); return; } $this->currentTestCase = $this->document->createElement('testcase'); $isStrict = Configuration::config()['settings']['strict_xml']; foreach ($test->getReportFields() as $attr => $value) { if ($isStrict and !in_array($attr, $this->strictAttributes)) { continue; } $this->currentTestCase->setAttribute($attr, $value); } } public function endTest(\PHPUnit\Framework\Test $test, float $time):void { if ($this->currentTestCase !== null and $test instanceof Test) { $numAssertions = $test->getNumAssertions(); $this->testSuiteAssertions[$this->testSuiteLevel] += $numAssertions; $this->currentTestCase->setAttribute( 'assertions', $numAssertions ); } if ($test instanceof TestCase) { parent::endTest($test, $time); return; } // since PhpUnit 7.4.0, parent::endTest ignores tests that aren't instances of TestCase // so I copied this code from PhpUnit 7.3.5 $this->currentTestCase->setAttribute( 'time', \sprintf('%F', $time) ); $this->testSuites[$this->testSuiteLevel]->appendChild( $this->currentTestCase ); $this->testSuiteTests[$this->testSuiteLevel]++; $this->testSuiteTimes[$this->testSuiteLevel] += $time; $this->currentTestCase = null; } } phpunit-wrapper/src/Log/PhpUnit.php000077700000010237151323602320013330 0ustar00<?php namespace Codeception\PHPUnit\Log; use Codeception\Configuration; use Codeception\Test\Interfaces\Reported; use Codeception\Test\Test; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestSuite; class PhpUnit extends \Codeception\PHPUnit\NonFinal\JUnit { const SUITE_LEVEL = 1; const FILE_LEVEL = 2; protected $strictAttributes = ['file', 'name', 'class']; private $currentFile; private $currentFileSuite; public function startTest(\PHPUnit\Framework\Test $test):void { if (method_exists($test, 'getFileName') ) { $filename = $test->getFileName(); } else { $reflector = new \ReflectionClass($test); $filename = $reflector->getFileName(); } if ($filename !== $this->currentFile) { if ($this->currentFile !== null) { parent::endTestSuite(new TestSuite()); } //initialize all values to avoid warnings $this->testSuiteAssertions[self::FILE_LEVEL] = 0; $this->testSuiteTests[self::FILE_LEVEL] = 0; $this->testSuiteTimes[self::FILE_LEVEL] = 0; $this->testSuiteErrors[self::FILE_LEVEL] = 0; $this->testSuiteFailures[self::FILE_LEVEL] = 0; $this->testSuiteSkipped[self::FILE_LEVEL] = 0; $this->testSuiteLevel = self::FILE_LEVEL; $this->currentFile = $filename; $this->currentFileSuite = $this->document->createElement('testsuite'); if ($test instanceof Reported) { $reportFields = $test->getReportFields(); $class = isset($reportFields['class']) ? $reportFields['class'] : $reportFields['name']; $this->currentFileSuite->setAttribute('name', $class); } else { $this->currentFileSuite->setAttribute('name', get_class($test)); } $this->currentFileSuite->setAttribute('file', $filename); $this->testSuites[self::SUITE_LEVEL]->appendChild($this->currentFileSuite); $this->testSuites[self::FILE_LEVEL] = $this->currentFileSuite; } if (!$test instanceof Reported) { parent::startTest($test); return; } $this->currentTestCase = $this->document->createElement('testcase'); $isStrict = Configuration::config()['settings']['strict_xml']; foreach ($test->getReportFields() as $attr => $value) { if ($isStrict and !in_array($attr, $this->strictAttributes)) { continue; } $this->currentTestCase->setAttribute($attr, $value); } } public function endTest(\PHPUnit\Framework\Test $test, float $time):void { if ($this->currentTestCase !== null && $test instanceof Test) { $numAssertions = $test->getNumAssertions(); $this->testSuiteAssertions[$this->testSuiteLevel] += $numAssertions; $this->currentTestCase->setAttribute( 'assertions', $numAssertions ); } if ($test instanceof TestCase) { parent::endTest($test, $time); return; } // In PhpUnit 7.4.*, parent::endTest ignores tests that aren't instances of TestCase // so I copied this code from PhpUnit 7.3.5 $this->currentTestCase->setAttribute( 'time', \sprintf('%F', $time) ); $this->testSuites[$this->testSuiteLevel]->appendChild( $this->currentTestCase ); $this->testSuiteTests[$this->testSuiteLevel]++; $this->testSuiteTimes[$this->testSuiteLevel] += $time; $this->currentTestCase = null; } /** * Cleans the mess caused by test suite manipulation in startTest */ public function endTestSuite(TestSuite $suite): void { if ($suite->getName()) { if ($this->currentFile) { //close last file in the test suite parent::endTestSuite(new TestSuite()); $this->currentFile = null; } $this->testSuiteLevel = self::SUITE_LEVEL; } parent::endTestSuite($suite); } } phpunit-wrapper/src/Log/.htaccess000077700000000177151323602320013030 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>phpunit-wrapper/src/DispatcherWrapper.php000077700000001717151323602320014652 0ustar00<?php namespace Codeception\PHPUnit; use Symfony\Component\EventDispatcher\Event; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as ContractsEventDispatcherInterface; trait DispatcherWrapper { /** * Compatibility wrapper for dispatcher change between Symfony 4 and 5 * @param EventDispatcher $dispatcher * @param string $eventType * @param Event $eventObject */ protected function dispatch(EventDispatcher $dispatcher, $eventType, Event $eventObject) { // The `EventDispatcherInterface` of `Symfony\Contracts` is only implemented in Symfony 4.3 or higher if ($dispatcher instanceof ContractsEventDispatcherInterface) { //Symfony 4.3 or higher $dispatcher->dispatch($eventObject, $eventType); } else { //Symfony 4.2 or lower $dispatcher->dispatch($eventType, $eventObject); } } } phpunit-wrapper/src/Overrides/.htaccess000077700000000177151323602320014251 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>phpunit-wrapper/src/Overrides/Filter.php000077700000006116151323602320014410 0ustar00<?php namespace PHPUnit\Util; // @codingStandardsIgnoreStart class Filter { // @codingStandardsIgnoreEnd protected static $filteredClassesPattern = [ 'Symfony\Component\Console', 'Codeception\Command\\', 'Codeception\TestCase\\', ]; public static function getFilteredStackTrace($e, $asString = true, $filter = true) { $stackTrace = $asString ? '' : []; $trace = $e->getPrevious() ? $e->getPrevious()->getTrace() : $e->getTrace(); if ($e instanceof \PHPUnit\Framework\ExceptionWrapper) { $trace = $e->getSerializableTrace(); } $eFile = $e->getFile(); $eLine = $e->getLine(); if (!self::frameExists($trace, $eFile, $eLine)) { array_unshift( $trace, ['file' => $eFile, 'line' => $eLine] ); } foreach ($trace as $step) { if (self::classIsFiltered($step) and $filter) { continue; } if (self::fileIsFiltered($step) and $filter) { continue; } if (!$asString) { $stackTrace[] = $step; continue; } if (!isset($step['file'])) { continue; } $stackTrace .= $step['file'] . ':' . $step['line'] . "\n"; } return $stackTrace; } protected static function classIsFiltered($step) { if (!isset($step['class'])) { return false; } $className = $step['class']; foreach (self::$filteredClassesPattern as $filteredClassName) { if (strpos($className, $filteredClassName) === 0) { return true; } } return false; } protected static function fileIsFiltered($step) { if (!isset($step['file'])) { return false; } if (strpos($step['file'], 'codecept.phar/') !== false) { return true; } if (strpos($step['file'], 'vendor' . DIRECTORY_SEPARATOR . 'phpunit') !== false) { return true; } if (strpos($step['file'], 'vendor' . DIRECTORY_SEPARATOR . 'codeception') !== false) { return true; } $modulePath = 'src' . DIRECTORY_SEPARATOR . 'Codeception' . DIRECTORY_SEPARATOR . 'Module'; if (strpos($step['file'], $modulePath) !== false) { return false; // don`t filter modules } if (strpos($step['file'], 'src' . DIRECTORY_SEPARATOR . 'Codeception' . DIRECTORY_SEPARATOR) !== false) { return true; } return false; } /** * @param array $trace * @param string $file * @param int $line * * @return bool */ private static function frameExists(array $trace, $file, $line) { foreach ($trace as $frame) { if (isset($frame['file']) && $frame['file'] == $file && isset($frame['line']) && $frame['line'] == $line) { return true; } } return false; } } phpunit-wrapper/src/NonFinal/TestRunner.php000077700000150533151323602320015041 0ustar00<?php declare(strict_types=1); /* * This file is part of PHPUnit. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Codeception\PHPUnit\NonFinal; use PHPUnit\Framework\Error\Deprecated; use PHPUnit\Framework\Error\Notice; use PHPUnit\Framework\Error\Warning; use PHPUnit\Framework\Exception; use PHPUnit\Framework\Test; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestListener; use PHPUnit\Framework\TestResult; use PHPUnit\Framework\TestSuite; use PHPUnit\Runner\AfterLastTestHook; use PHPUnit\Runner\BaseTestRunner; use PHPUnit\Runner\BeforeFirstTestHook; use PHPUnit\Runner\Filter\ExcludeGroupFilterIterator; use PHPUnit\Runner\Filter\Factory; use PHPUnit\Runner\Filter\IncludeGroupFilterIterator; use PHPUnit\Runner\Filter\NameFilterIterator; use PHPUnit\Runner\Hook; use PHPUnit\Runner\NullTestResultCache; use PHPUnit\Runner\ResultCacheExtension; use PHPUnit\Runner\StandardTestSuiteLoader; use PHPUnit\Runner\TestHook; use PHPUnit\Runner\TestListenerAdapter; use PHPUnit\Runner\TestResultCache; use PHPUnit\Runner\TestSuiteLoader; use PHPUnit\Runner\TestSuiteSorter; use PHPUnit\Runner\Version; use PHPUnit\TextUI\ResultPrinter; use PHPUnit\Util\Configuration; use PHPUnit\Util\Filesystem; use PHPUnit\Util\Log\JUnit; use PHPUnit\Util\Log\TeamCity; use PHPUnit\Util\Printer; use PHPUnit\Util\TestDox\CliTestDoxPrinter; use PHPUnit\Util\TestDox\HtmlResultPrinter; use PHPUnit\Util\TestDox\TextResultPrinter; use PHPUnit\Util\TestDox\XmlResultPrinter; use PHPUnit\Util\XdebugFilterScriptGenerator; use ReflectionClass; use SebastianBergmann\CodeCoverage\CodeCoverage; use SebastianBergmann\CodeCoverage\Exception as CodeCoverageException; use SebastianBergmann\CodeCoverage\Filter as CodeCoverageFilter; use SebastianBergmann\CodeCoverage\Report\Clover as CloverReport; use SebastianBergmann\CodeCoverage\Report\Crap4j as Crap4jReport; use SebastianBergmann\CodeCoverage\Report\Html\Facade as HtmlReport; use SebastianBergmann\CodeCoverage\Report\PHP as PhpReport; use SebastianBergmann\CodeCoverage\Report\Text as TextReport; use SebastianBergmann\CodeCoverage\Report\Xml\Facade as XmlReport; use SebastianBergmann\Comparator\Comparator; use SebastianBergmann\Environment\Runtime; use SebastianBergmann\Invoker\Invoker; /** * @internal This class is not covered by the backward compatibility promise for PHPUnit */ class TestRunner extends BaseTestRunner { public const SUCCESS_EXIT = 0; public const FAILURE_EXIT = 1; public const EXCEPTION_EXIT = 2; /** * @var bool */ protected static $versionStringPrinted = false; /** * @var CodeCoverageFilter */ protected $codeCoverageFilter; /** * @var TestSuiteLoader */ protected $loader; /** * @var ResultPrinter */ protected $printer; /** * @var Runtime */ private $runtime; /** * @var bool */ private $messagePrinted = false; /** * @var Hook[] */ private $extensions = []; /** * @param ReflectionClass|Test $test * @param bool $exit * * @throws \RuntimeException * @throws \InvalidArgumentException * @throws Exception * @throws \ReflectionException */ public static function run($test, array $arguments = [], $exit = true): TestResult { if ($test instanceof ReflectionClass) { $test = new TestSuite($test); } if ($test instanceof Test) { $aTestRunner = new self; return $aTestRunner->doRun( $test, $arguments, $exit ); } throw new Exception('No test case or test suite found.'); } public function __construct(TestSuiteLoader $loader = null, CodeCoverageFilter $filter = null) { if ($filter === null) { $filter = new CodeCoverageFilter; } $this->codeCoverageFilter = $filter; $this->loader = $loader; $this->runtime = new Runtime; } /** * @throws \PHPUnit\Runner\Exception * @throws Exception * @throws \InvalidArgumentException * @throws \RuntimeException * @throws \ReflectionException */ public function doRun(Test $suite, array $arguments = [], bool $exit = true): TestResult { if (isset($arguments['configuration'])) { $GLOBALS['__PHPUNIT_CONFIGURATION_FILE'] = $arguments['configuration']; } $this->handleConfiguration($arguments); if (\is_int($arguments['columns']) && $arguments['columns'] < 16) { $arguments['columns'] = 16; $tooFewColumnsRequested = true; } if (isset($arguments['bootstrap'])) { $GLOBALS['__PHPUNIT_BOOTSTRAP'] = $arguments['bootstrap']; } if ($suite instanceof TestCase || $suite instanceof TestSuite) { if ($arguments['backupGlobals'] === true) { $suite->setBackupGlobals(true); } if ($arguments['backupStaticAttributes'] === true) { $suite->setBackupStaticAttributes(true); } if ($arguments['beStrictAboutChangesToGlobalState'] === true) { $suite->setBeStrictAboutChangesToGlobalState(true); } } if ($arguments['executionOrder'] === TestSuiteSorter::ORDER_RANDOMIZED) { \mt_srand($arguments['randomOrderSeed']); } if ($arguments['cacheResult']) { if (isset($arguments['cacheResultFile'])) { $cache = new TestResultCache($arguments['cacheResultFile']); } else { $cache = new TestResultCache; } $this->addExtension(new ResultCacheExtension($cache)); } if ($arguments['executionOrder'] !== TestSuiteSorter::ORDER_DEFAULT || $arguments['executionOrderDefects'] !== TestSuiteSorter::ORDER_DEFAULT || $arguments['resolveDependencies']) { $cache = $cache ?? new NullTestResultCache; $cache->load(); $sorter = new TestSuiteSorter($cache); $sorter->reorderTestsInSuite($suite, $arguments['executionOrder'], $arguments['resolveDependencies'], $arguments['executionOrderDefects']); $originalExecutionOrder = $sorter->getOriginalExecutionOrder(); unset($sorter); } if (\is_int($arguments['repeat']) && $arguments['repeat'] > 0) { $_suite = new TestSuite; /* @noinspection PhpUnusedLocalVariableInspection */ foreach (\range(1, $arguments['repeat']) as $step) { $_suite->addTest($suite); } $suite = $_suite; unset($_suite); } $result = $this->createTestResult(); $listener = new TestListenerAdapter; $listenerNeeded = false; foreach ($this->extensions as $extension) { if ($extension instanceof TestHook) { $listener->add($extension); $listenerNeeded = true; } } if ($listenerNeeded) { $result->addListener($listener); } unset($listener, $listenerNeeded); if (!$arguments['convertErrorsToExceptions']) { $result->convertErrorsToExceptions(false); } if (!$arguments['convertDeprecationsToExceptions']) { Deprecated::$enabled = false; } if (!$arguments['convertNoticesToExceptions']) { Notice::$enabled = false; } if (!$arguments['convertWarningsToExceptions']) { Warning::$enabled = false; } if ($arguments['stopOnError']) { $result->stopOnError(true); } if ($arguments['stopOnFailure']) { $result->stopOnFailure(true); } if ($arguments['stopOnWarning']) { $result->stopOnWarning(true); } if ($arguments['stopOnIncomplete']) { $result->stopOnIncomplete(true); } if ($arguments['stopOnRisky']) { $result->stopOnRisky(true); } if ($arguments['stopOnSkipped']) { $result->stopOnSkipped(true); } if ($arguments['stopOnDefect']) { $result->stopOnDefect(true); } if ($arguments['registerMockObjectsFromTestArgumentsRecursively']) { $result->setRegisterMockObjectsFromTestArgumentsRecursively(true); } if ($this->printer === null) { if (isset($arguments['printer']) && $arguments['printer'] instanceof Printer) { $this->printer = $arguments['printer']; } else { $printerClass = ResultPrinter::class; if (isset($arguments['printer']) && \is_string($arguments['printer']) && \class_exists($arguments['printer'], false)) { $class = new ReflectionClass($arguments['printer']); if ($class->isSubclassOf(ResultPrinter::class)) { $printerClass = $arguments['printer']; } } $this->printer = new $printerClass( (isset($arguments['stderr']) && $arguments['stderr'] === true) ? 'php://stderr' : null, $arguments['verbose'], $arguments['colors'], $arguments['debug'], $arguments['columns'], $arguments['reverseList'] ); if (isset($originalExecutionOrder) && ($this->printer instanceof CliTestDoxPrinter)) { /* @var CliTestDoxPrinter */ $this->printer->setOriginalExecutionOrder($originalExecutionOrder); } } } $this->printer->write( Version::getVersionString() . "\n" ); self::$versionStringPrinted = true; if ($arguments['verbose']) { $this->writeMessage('Runtime', $this->runtime->getNameWithVersionAndCodeCoverageDriver()); if ($arguments['executionOrder'] === TestSuiteSorter::ORDER_RANDOMIZED) { $this->writeMessage( 'Random seed', (string) $arguments['randomOrderSeed'] ); } if (isset($arguments['configuration'])) { $this->writeMessage( 'Configuration', $arguments['configuration']->getFilename() ); } foreach ($arguments['loadedExtensions'] as $extension) { $this->writeMessage( 'Extension', $extension ); } foreach ($arguments['notLoadedExtensions'] as $extension) { $this->writeMessage( 'Extension', $extension ); } } if (isset($tooFewColumnsRequested)) { $this->writeMessage('Error', 'Less than 16 columns requested, number of columns set to 16'); } if ($this->runtime->discardsComments()) { $this->writeMessage('Warning', 'opcache.save_comments=0 set; annotations will not work'); } if (isset($arguments['configuration']) && $arguments['configuration']->hasValidationErrors()) { $this->write( "\n Warning - The configuration file did not pass validation!\n The following problems have been detected:\n" ); foreach ($arguments['configuration']->getValidationErrors() as $line => $errors) { $this->write(\sprintf("\n Line %d:\n", $line)); foreach ($errors as $msg) { $this->write(\sprintf(" - %s\n", $msg)); } } $this->write("\n Test results may not be as expected.\n\n"); } foreach ($arguments['listeners'] as $listener) { $result->addListener($listener); } $result->addListener($this->printer); $codeCoverageReports = 0; if (!isset($arguments['noLogging'])) { if (isset($arguments['testdoxHTMLFile'])) { $result->addListener( new HtmlResultPrinter( $arguments['testdoxHTMLFile'], $arguments['testdoxGroups'], $arguments['testdoxExcludeGroups'] ) ); } if (isset($arguments['testdoxTextFile'])) { $result->addListener( new TextResultPrinter( $arguments['testdoxTextFile'], $arguments['testdoxGroups'], $arguments['testdoxExcludeGroups'] ) ); } if (isset($arguments['testdoxXMLFile'])) { $result->addListener( new XmlResultPrinter( $arguments['testdoxXMLFile'] ) ); } if (isset($arguments['teamcityLogfile'])) { $result->addListener( new TeamCity($arguments['teamcityLogfile']) ); } if (isset($arguments['junitLogfile'])) { $result->addListener( new JUnit( $arguments['junitLogfile'], $arguments['reportUselessTests'] ) ); } if (isset($arguments['coverageClover'])) { $codeCoverageReports++; } if (isset($arguments['coverageCrap4J'])) { $codeCoverageReports++; } if (isset($arguments['coverageHtml'])) { $codeCoverageReports++; } if (isset($arguments['coveragePHP'])) { $codeCoverageReports++; } if (isset($arguments['coverageText'])) { $codeCoverageReports++; } if (isset($arguments['coverageXml'])) { $codeCoverageReports++; } } if (isset($arguments['noCoverage'])) { $codeCoverageReports = 0; } if ($codeCoverageReports > 0 && !$this->runtime->canCollectCodeCoverage()) { $this->writeMessage('Error', 'No code coverage driver is available'); $codeCoverageReports = 0; } if ($codeCoverageReports > 0 || isset($arguments['xdebugFilterFile'])) { $whitelistFromConfigurationFile = false; $whitelistFromOption = false; if (isset($arguments['whitelist'])) { $this->codeCoverageFilter->addDirectoryToWhitelist($arguments['whitelist']); $whitelistFromOption = true; } if (isset($arguments['configuration'])) { $filterConfiguration = $arguments['configuration']->getFilterConfiguration(); if (!empty($filterConfiguration['whitelist'])) { $whitelistFromConfigurationFile = true; } if (!empty($filterConfiguration['whitelist'])) { foreach ($filterConfiguration['whitelist']['include']['directory'] as $dir) { $this->codeCoverageFilter->addDirectoryToWhitelist( $dir['path'], $dir['suffix'], $dir['prefix'] ); } foreach ($filterConfiguration['whitelist']['include']['file'] as $file) { $this->codeCoverageFilter->addFileToWhitelist($file); } foreach ($filterConfiguration['whitelist']['exclude']['directory'] as $dir) { $this->codeCoverageFilter->removeDirectoryFromWhitelist( $dir['path'], $dir['suffix'], $dir['prefix'] ); } foreach ($filterConfiguration['whitelist']['exclude']['file'] as $file) { $this->codeCoverageFilter->removeFileFromWhitelist($file); } } } } if ($codeCoverageReports > 0) { $codeCoverage = new CodeCoverage( null, $this->codeCoverageFilter ); $codeCoverage->setUnintentionallyCoveredSubclassesWhitelist( [Comparator::class] ); $codeCoverage->setCheckForUnintentionallyCoveredCode( $arguments['strictCoverage'] ); $codeCoverage->setCheckForMissingCoversAnnotation( $arguments['strictCoverage'] ); if (isset($arguments['forceCoversAnnotation'])) { $codeCoverage->setForceCoversAnnotation( $arguments['forceCoversAnnotation'] ); } if (isset($arguments['ignoreDeprecatedCodeUnitsFromCodeCoverage'])) { $codeCoverage->setIgnoreDeprecatedCode( $arguments['ignoreDeprecatedCodeUnitsFromCodeCoverage'] ); } if (isset($arguments['disableCodeCoverageIgnore'])) { $codeCoverage->setDisableIgnoredLines(true); } if (!empty($filterConfiguration['whitelist'])) { $codeCoverage->setAddUncoveredFilesFromWhitelist( $filterConfiguration['whitelist']['addUncoveredFilesFromWhitelist'] ); $codeCoverage->setProcessUncoveredFilesFromWhitelist( $filterConfiguration['whitelist']['processUncoveredFilesFromWhitelist'] ); } if (!$this->codeCoverageFilter->hasWhitelist()) { if (!$whitelistFromConfigurationFile && !$whitelistFromOption) { $this->writeMessage('Error', 'No whitelist is configured, no code coverage will be generated.'); } else { $this->writeMessage('Error', 'Incorrect whitelist config, no code coverage will be generated.'); } $codeCoverageReports = 0; unset($codeCoverage); } } if (isset($arguments['xdebugFilterFile'], $filterConfiguration)) { $this->write("\n"); $script = (new XdebugFilterScriptGenerator)->generate($filterConfiguration['whitelist']); if ($arguments['xdebugFilterFile'] !== 'php://stdout' && $arguments['xdebugFilterFile'] !== 'php://stderr' && !Filesystem::createDirectory(\dirname($arguments['xdebugFilterFile']))) { $this->write(\sprintf('Cannot write Xdebug filter script to %s ' . \PHP_EOL, $arguments['xdebugFilterFile'])); exit(self::EXCEPTION_EXIT); } \file_put_contents($arguments['xdebugFilterFile'], $script); $this->write(\sprintf('Wrote Xdebug filter script to %s ' . \PHP_EOL, $arguments['xdebugFilterFile'])); exit(self::SUCCESS_EXIT); } $this->printer->write("\n"); if (isset($codeCoverage)) { $result->setCodeCoverage($codeCoverage); if ($codeCoverageReports > 1 && isset($arguments['cacheTokens'])) { $codeCoverage->setCacheTokens($arguments['cacheTokens']); } } $result->beStrictAboutTestsThatDoNotTestAnything($arguments['reportUselessTests']); $result->beStrictAboutOutputDuringTests($arguments['disallowTestOutput']); $result->beStrictAboutTodoAnnotatedTests($arguments['disallowTodoAnnotatedTests']); $result->beStrictAboutResourceUsageDuringSmallTests($arguments['beStrictAboutResourceUsageDuringSmallTests']); if ($arguments['enforceTimeLimit'] === true) { if (!\class_exists(Invoker::class)) { $this->writeMessage('Error', 'Package phpunit/php-invoker is required for enforcing time limits'); } if (!\extension_loaded('pcntl') || \strpos(\ini_get('disable_functions'), 'pcntl') !== false) { $this->writeMessage('Error', 'PHP extension pcntl is required for enforcing time limits'); } } $result->enforceTimeLimit($arguments['enforceTimeLimit']); $result->setDefaultTimeLimit($arguments['defaultTimeLimit']); $result->setTimeoutForSmallTests($arguments['timeoutForSmallTests']); $result->setTimeoutForMediumTests($arguments['timeoutForMediumTests']); $result->setTimeoutForLargeTests($arguments['timeoutForLargeTests']); if ($suite instanceof TestSuite) { $this->processSuiteFilters($suite, $arguments); $suite->setRunTestInSeparateProcess($arguments['processIsolation']); } foreach ($this->extensions as $extension) { if ($extension instanceof BeforeFirstTestHook) { $extension->executeBeforeFirstTest(); } } $suite->run($result); foreach ($this->extensions as $extension) { if ($extension instanceof AfterLastTestHook) { $extension->executeAfterLastTest(); } } $result->flushListeners(); if ($this->printer instanceof ResultPrinter) { $this->printer->printResult($result); } if (isset($codeCoverage)) { if (isset($arguments['coverageClover'])) { $this->printer->write( "\nGenerating code coverage report in Clover XML format ..." ); try { $writer = new CloverReport; $writer->process($codeCoverage, $arguments['coverageClover']); $this->printer->write(" done\n"); unset($writer); } catch (CodeCoverageException $e) { $this->printer->write( " failed\n" . $e->getMessage() . "\n" ); } } if (isset($arguments['coverageCrap4J'])) { $this->printer->write( "\nGenerating Crap4J report XML file ..." ); try { $writer = new Crap4jReport($arguments['crap4jThreshold']); $writer->process($codeCoverage, $arguments['coverageCrap4J']); $this->printer->write(" done\n"); unset($writer); } catch (CodeCoverageException $e) { $this->printer->write( " failed\n" . $e->getMessage() . "\n" ); } } if (isset($arguments['coverageHtml'])) { $this->printer->write( "\nGenerating code coverage report in HTML format ..." ); try { $writer = new HtmlReport( $arguments['reportLowUpperBound'], $arguments['reportHighLowerBound'], \sprintf( ' and <a href="https://phpunit.de/">PHPUnit %s</a>', Version::id() ) ); $writer->process($codeCoverage, $arguments['coverageHtml']); $this->printer->write(" done\n"); unset($writer); } catch (CodeCoverageException $e) { $this->printer->write( " failed\n" . $e->getMessage() . "\n" ); } } if (isset($arguments['coveragePHP'])) { $this->printer->write( "\nGenerating code coverage report in PHP format ..." ); try { $writer = new PhpReport; $writer->process($codeCoverage, $arguments['coveragePHP']); $this->printer->write(" done\n"); unset($writer); } catch (CodeCoverageException $e) { $this->printer->write( " failed\n" . $e->getMessage() . "\n" ); } } if (isset($arguments['coverageText'])) { if ($arguments['coverageText'] == 'php://stdout') { $outputStream = $this->printer; $colors = $arguments['colors'] && $arguments['colors'] != ResultPrinter::COLOR_NEVER; } else { $outputStream = new Printer($arguments['coverageText']); $colors = false; } $processor = new TextReport( $arguments['reportLowUpperBound'], $arguments['reportHighLowerBound'], $arguments['coverageTextShowUncoveredFiles'], $arguments['coverageTextShowOnlySummary'] ); $outputStream->write( $processor->process($codeCoverage, $colors) ); } if (isset($arguments['coverageXml'])) { $this->printer->write( "\nGenerating code coverage report in PHPUnit XML format ..." ); try { $writer = new XmlReport(Version::id()); $writer->process($codeCoverage, $arguments['coverageXml']); $this->printer->write(" done\n"); unset($writer); } catch (CodeCoverageException $e) { $this->printer->write( " failed\n" . $e->getMessage() . "\n" ); } } } if ($exit) { if ($result->wasSuccessful()) { if ($arguments['failOnRisky'] && !$result->allHarmless()) { exit(self::FAILURE_EXIT); } if ($arguments['failOnWarning'] && $result->warningCount() > 0) { exit(self::FAILURE_EXIT); } exit(self::SUCCESS_EXIT); } if ($result->errorCount() > 0) { exit(self::EXCEPTION_EXIT); } if ($result->failureCount() > 0) { exit(self::FAILURE_EXIT); } } return $result; } public function setPrinter(ResultPrinter $resultPrinter): void { $this->printer = $resultPrinter; } /** * Returns the loader to be used. */ public function getLoader(): TestSuiteLoader { if ($this->loader === null) { $this->loader = new StandardTestSuiteLoader; } return $this->loader; } public function addExtension(TestHook $extension): void { $this->extensions[] = $extension; } protected function createTestResult(): TestResult { return new TestResult; } /** * Override to define how to handle a failed loading of * a test suite. */ protected function runFailed(string $message): void { $this->write($message . \PHP_EOL); exit(self::FAILURE_EXIT); } protected function write(string $buffer): void { if (\PHP_SAPI != 'cli' && \PHP_SAPI != 'phpdbg') { $buffer = \htmlspecialchars($buffer); } if ($this->printer !== null) { $this->printer->write($buffer); } else { print $buffer; } } /** * @throws Exception */ protected function handleConfiguration(array &$arguments): void { if (isset($arguments['configuration']) && !$arguments['configuration'] instanceof Configuration) { $arguments['configuration'] = Configuration::getInstance( $arguments['configuration'] ); } $arguments['debug'] = $arguments['debug'] ?? false; $arguments['filter'] = $arguments['filter'] ?? false; $arguments['listeners'] = $arguments['listeners'] ?? []; if (isset($arguments['configuration'])) { $arguments['configuration']->handlePHPConfiguration(); $phpunitConfiguration = $arguments['configuration']->getPHPUnitConfiguration(); if (isset($phpunitConfiguration['backupGlobals']) && !isset($arguments['backupGlobals'])) { $arguments['backupGlobals'] = $phpunitConfiguration['backupGlobals']; } if (isset($phpunitConfiguration['backupStaticAttributes']) && !isset($arguments['backupStaticAttributes'])) { $arguments['backupStaticAttributes'] = $phpunitConfiguration['backupStaticAttributes']; } if (isset($phpunitConfiguration['beStrictAboutChangesToGlobalState']) && !isset($arguments['beStrictAboutChangesToGlobalState'])) { $arguments['beStrictAboutChangesToGlobalState'] = $phpunitConfiguration['beStrictAboutChangesToGlobalState']; } if (isset($phpunitConfiguration['bootstrap']) && !isset($arguments['bootstrap'])) { $arguments['bootstrap'] = $phpunitConfiguration['bootstrap']; } if (isset($phpunitConfiguration['cacheResult']) && !isset($arguments['cacheResult'])) { $arguments['cacheResult'] = $phpunitConfiguration['cacheResult']; } if (isset($phpunitConfiguration['cacheResultFile']) && !isset($arguments['cacheResultFile'])) { $arguments['cacheResultFile'] = $phpunitConfiguration['cacheResultFile']; } if (isset($phpunitConfiguration['cacheTokens']) && !isset($arguments['cacheTokens'])) { $arguments['cacheTokens'] = $phpunitConfiguration['cacheTokens']; } if (isset($phpunitConfiguration['cacheTokens']) && !isset($arguments['cacheTokens'])) { $arguments['cacheTokens'] = $phpunitConfiguration['cacheTokens']; } if (isset($phpunitConfiguration['colors']) && !isset($arguments['colors'])) { $arguments['colors'] = $phpunitConfiguration['colors']; } if (isset($phpunitConfiguration['convertDeprecationsToExceptions']) && !isset($arguments['convertDeprecationsToExceptions'])) { $arguments['convertDeprecationsToExceptions'] = $phpunitConfiguration['convertDeprecationsToExceptions']; } if (isset($phpunitConfiguration['convertErrorsToExceptions']) && !isset($arguments['convertErrorsToExceptions'])) { $arguments['convertErrorsToExceptions'] = $phpunitConfiguration['convertErrorsToExceptions']; } if (isset($phpunitConfiguration['convertNoticesToExceptions']) && !isset($arguments['convertNoticesToExceptions'])) { $arguments['convertNoticesToExceptions'] = $phpunitConfiguration['convertNoticesToExceptions']; } if (isset($phpunitConfiguration['convertWarningsToExceptions']) && !isset($arguments['convertWarningsToExceptions'])) { $arguments['convertWarningsToExceptions'] = $phpunitConfiguration['convertWarningsToExceptions']; } if (isset($phpunitConfiguration['processIsolation']) && !isset($arguments['processIsolation'])) { $arguments['processIsolation'] = $phpunitConfiguration['processIsolation']; } if (isset($phpunitConfiguration['stopOnDefect']) && !isset($arguments['stopOnDefect'])) { $arguments['stopOnDefect'] = $phpunitConfiguration['stopOnDefect']; } if (isset($phpunitConfiguration['stopOnError']) && !isset($arguments['stopOnError'])) { $arguments['stopOnError'] = $phpunitConfiguration['stopOnError']; } if (isset($phpunitConfiguration['stopOnFailure']) && !isset($arguments['stopOnFailure'])) { $arguments['stopOnFailure'] = $phpunitConfiguration['stopOnFailure']; } if (isset($phpunitConfiguration['stopOnWarning']) && !isset($arguments['stopOnWarning'])) { $arguments['stopOnWarning'] = $phpunitConfiguration['stopOnWarning']; } if (isset($phpunitConfiguration['stopOnIncomplete']) && !isset($arguments['stopOnIncomplete'])) { $arguments['stopOnIncomplete'] = $phpunitConfiguration['stopOnIncomplete']; } if (isset($phpunitConfiguration['stopOnRisky']) && !isset($arguments['stopOnRisky'])) { $arguments['stopOnRisky'] = $phpunitConfiguration['stopOnRisky']; } if (isset($phpunitConfiguration['stopOnSkipped']) && !isset($arguments['stopOnSkipped'])) { $arguments['stopOnSkipped'] = $phpunitConfiguration['stopOnSkipped']; } if (isset($phpunitConfiguration['failOnWarning']) && !isset($arguments['failOnWarning'])) { $arguments['failOnWarning'] = $phpunitConfiguration['failOnWarning']; } if (isset($phpunitConfiguration['failOnRisky']) && !isset($arguments['failOnRisky'])) { $arguments['failOnRisky'] = $phpunitConfiguration['failOnRisky']; } if (isset($phpunitConfiguration['timeoutForSmallTests']) && !isset($arguments['timeoutForSmallTests'])) { $arguments['timeoutForSmallTests'] = $phpunitConfiguration['timeoutForSmallTests']; } if (isset($phpunitConfiguration['timeoutForMediumTests']) && !isset($arguments['timeoutForMediumTests'])) { $arguments['timeoutForMediumTests'] = $phpunitConfiguration['timeoutForMediumTests']; } if (isset($phpunitConfiguration['timeoutForLargeTests']) && !isset($arguments['timeoutForLargeTests'])) { $arguments['timeoutForLargeTests'] = $phpunitConfiguration['timeoutForLargeTests']; } if (isset($phpunitConfiguration['reportUselessTests']) && !isset($arguments['reportUselessTests'])) { $arguments['reportUselessTests'] = $phpunitConfiguration['reportUselessTests']; } if (isset($phpunitConfiguration['strictCoverage']) && !isset($arguments['strictCoverage'])) { $arguments['strictCoverage'] = $phpunitConfiguration['strictCoverage']; } if (isset($phpunitConfiguration['ignoreDeprecatedCodeUnitsFromCodeCoverage']) && !isset($arguments['ignoreDeprecatedCodeUnitsFromCodeCoverage'])) { $arguments['ignoreDeprecatedCodeUnitsFromCodeCoverage'] = $phpunitConfiguration['ignoreDeprecatedCodeUnitsFromCodeCoverage']; } if (isset($phpunitConfiguration['disallowTestOutput']) && !isset($arguments['disallowTestOutput'])) { $arguments['disallowTestOutput'] = $phpunitConfiguration['disallowTestOutput']; } if (isset($phpunitConfiguration['defaultTimeLimit']) && !isset($arguments['defaultTimeLimit'])) { $arguments['defaultTimeLimit'] = $phpunitConfiguration['defaultTimeLimit']; } if (isset($phpunitConfiguration['enforceTimeLimit']) && !isset($arguments['enforceTimeLimit'])) { $arguments['enforceTimeLimit'] = $phpunitConfiguration['enforceTimeLimit']; } if (isset($phpunitConfiguration['disallowTodoAnnotatedTests']) && !isset($arguments['disallowTodoAnnotatedTests'])) { $arguments['disallowTodoAnnotatedTests'] = $phpunitConfiguration['disallowTodoAnnotatedTests']; } if (isset($phpunitConfiguration['beStrictAboutResourceUsageDuringSmallTests']) && !isset($arguments['beStrictAboutResourceUsageDuringSmallTests'])) { $arguments['beStrictAboutResourceUsageDuringSmallTests'] = $phpunitConfiguration['beStrictAboutResourceUsageDuringSmallTests']; } if (isset($phpunitConfiguration['verbose']) && !isset($arguments['verbose'])) { $arguments['verbose'] = $phpunitConfiguration['verbose']; } if (isset($phpunitConfiguration['reverseDefectList']) && !isset($arguments['reverseList'])) { $arguments['reverseList'] = $phpunitConfiguration['reverseDefectList']; } if (isset($phpunitConfiguration['forceCoversAnnotation']) && !isset($arguments['forceCoversAnnotation'])) { $arguments['forceCoversAnnotation'] = $phpunitConfiguration['forceCoversAnnotation']; } if (isset($phpunitConfiguration['disableCodeCoverageIgnore']) && !isset($arguments['disableCodeCoverageIgnore'])) { $arguments['disableCodeCoverageIgnore'] = $phpunitConfiguration['disableCodeCoverageIgnore']; } if (isset($phpunitConfiguration['registerMockObjectsFromTestArgumentsRecursively']) && !isset($arguments['registerMockObjectsFromTestArgumentsRecursively'])) { $arguments['registerMockObjectsFromTestArgumentsRecursively'] = $phpunitConfiguration['registerMockObjectsFromTestArgumentsRecursively']; } if (isset($phpunitConfiguration['executionOrder']) && !isset($arguments['executionOrder'])) { $arguments['executionOrder'] = $phpunitConfiguration['executionOrder']; } if (isset($phpunitConfiguration['executionOrderDefects']) && !isset($arguments['executionOrderDefects'])) { $arguments['executionOrderDefects'] = $phpunitConfiguration['executionOrderDefects']; } if (isset($phpunitConfiguration['resolveDependencies']) && !isset($arguments['resolveDependencies'])) { $arguments['resolveDependencies'] = $phpunitConfiguration['resolveDependencies']; } $groupCliArgs = []; if (!empty($arguments['groups'])) { $groupCliArgs = $arguments['groups']; } $groupConfiguration = $arguments['configuration']->getGroupConfiguration(); if (!empty($groupConfiguration['include']) && !isset($arguments['groups'])) { $arguments['groups'] = $groupConfiguration['include']; } if (!empty($groupConfiguration['exclude']) && !isset($arguments['excludeGroups'])) { $arguments['excludeGroups'] = \array_diff($groupConfiguration['exclude'], $groupCliArgs); } foreach ($arguments['configuration']->getExtensionConfiguration() as $extension) { if (!\class_exists($extension['class'], false) && $extension['file'] !== '') { require_once $extension['file']; } if (!\class_exists($extension['class'])) { throw new Exception( \sprintf( 'Class "%s" does not exist', $extension['class'] ) ); } $extensionClass = new ReflectionClass($extension['class']); if (!$extensionClass->implementsInterface(Hook::class)) { throw new Exception( \sprintf( 'Class "%s" does not implement a PHPUnit\Runner\Hook interface', $extension['class'] ) ); } if (\count($extension['arguments']) == 0) { $extensionObject = $extensionClass->newInstance(); } else { $extensionObject = $extensionClass->newInstanceArgs( $extension['arguments'] ); } \assert($extensionObject instanceof TestHook); $this->addExtension($extensionObject); } foreach ($arguments['configuration']->getListenerConfiguration() as $listener) { if (!\class_exists($listener['class'], false) && $listener['file'] !== '') { require_once $listener['file']; } if (!\class_exists($listener['class'])) { throw new Exception( \sprintf( 'Class "%s" does not exist', $listener['class'] ) ); } $listenerClass = new ReflectionClass($listener['class']); if (!$listenerClass->implementsInterface(TestListener::class)) { throw new Exception( \sprintf( 'Class "%s" does not implement the PHPUnit\Framework\TestListener interface', $listener['class'] ) ); } if (\count($listener['arguments']) == 0) { $listener = new $listener['class']; } else { $listener = $listenerClass->newInstanceArgs( $listener['arguments'] ); } $arguments['listeners'][] = $listener; } $loggingConfiguration = $arguments['configuration']->getLoggingConfiguration(); if (isset($loggingConfiguration['coverage-clover']) && !isset($arguments['coverageClover'])) { $arguments['coverageClover'] = $loggingConfiguration['coverage-clover']; } if (isset($loggingConfiguration['coverage-crap4j']) && !isset($arguments['coverageCrap4J'])) { $arguments['coverageCrap4J'] = $loggingConfiguration['coverage-crap4j']; if (isset($loggingConfiguration['crap4jThreshold']) && !isset($arguments['crap4jThreshold'])) { $arguments['crap4jThreshold'] = $loggingConfiguration['crap4jThreshold']; } } if (isset($loggingConfiguration['coverage-html']) && !isset($arguments['coverageHtml'])) { if (isset($loggingConfiguration['lowUpperBound']) && !isset($arguments['reportLowUpperBound'])) { $arguments['reportLowUpperBound'] = $loggingConfiguration['lowUpperBound']; } if (isset($loggingConfiguration['highLowerBound']) && !isset($arguments['reportHighLowerBound'])) { $arguments['reportHighLowerBound'] = $loggingConfiguration['highLowerBound']; } $arguments['coverageHtml'] = $loggingConfiguration['coverage-html']; } if (isset($loggingConfiguration['coverage-php']) && !isset($arguments['coveragePHP'])) { $arguments['coveragePHP'] = $loggingConfiguration['coverage-php']; } if (isset($loggingConfiguration['coverage-text']) && !isset($arguments['coverageText'])) { $arguments['coverageText'] = $loggingConfiguration['coverage-text']; if (isset($loggingConfiguration['coverageTextShowUncoveredFiles'])) { $arguments['coverageTextShowUncoveredFiles'] = $loggingConfiguration['coverageTextShowUncoveredFiles']; } else { $arguments['coverageTextShowUncoveredFiles'] = false; } if (isset($loggingConfiguration['coverageTextShowOnlySummary'])) { $arguments['coverageTextShowOnlySummary'] = $loggingConfiguration['coverageTextShowOnlySummary']; } else { $arguments['coverageTextShowOnlySummary'] = false; } } if (isset($loggingConfiguration['coverage-xml']) && !isset($arguments['coverageXml'])) { $arguments['coverageXml'] = $loggingConfiguration['coverage-xml']; } if (isset($loggingConfiguration['plain'])) { $arguments['listeners'][] = new ResultPrinter( $loggingConfiguration['plain'], true ); } if (isset($loggingConfiguration['teamcity']) && !isset($arguments['teamcityLogfile'])) { $arguments['teamcityLogfile'] = $loggingConfiguration['teamcity']; } if (isset($loggingConfiguration['junit']) && !isset($arguments['junitLogfile'])) { $arguments['junitLogfile'] = $loggingConfiguration['junit']; } if (isset($loggingConfiguration['testdox-html']) && !isset($arguments['testdoxHTMLFile'])) { $arguments['testdoxHTMLFile'] = $loggingConfiguration['testdox-html']; } if (isset($loggingConfiguration['testdox-text']) && !isset($arguments['testdoxTextFile'])) { $arguments['testdoxTextFile'] = $loggingConfiguration['testdox-text']; } if (isset($loggingConfiguration['testdox-xml']) && !isset($arguments['testdoxXMLFile'])) { $arguments['testdoxXMLFile'] = $loggingConfiguration['testdox-xml']; } $testdoxGroupConfiguration = $arguments['configuration']->getTestdoxGroupConfiguration(); if (isset($testdoxGroupConfiguration['include']) && !isset($arguments['testdoxGroups'])) { $arguments['testdoxGroups'] = $testdoxGroupConfiguration['include']; } if (isset($testdoxGroupConfiguration['exclude']) && !isset($arguments['testdoxExcludeGroups'])) { $arguments['testdoxExcludeGroups'] = $testdoxGroupConfiguration['exclude']; } } $arguments['addUncoveredFilesFromWhitelist'] = $arguments['addUncoveredFilesFromWhitelist'] ?? true; $arguments['backupGlobals'] = $arguments['backupGlobals'] ?? null; $arguments['backupStaticAttributes'] = $arguments['backupStaticAttributes'] ?? null; $arguments['beStrictAboutChangesToGlobalState'] = $arguments['beStrictAboutChangesToGlobalState'] ?? null; $arguments['beStrictAboutResourceUsageDuringSmallTests'] = $arguments['beStrictAboutResourceUsageDuringSmallTests'] ?? false; $arguments['cacheResult'] = $arguments['cacheResult'] ?? true; $arguments['cacheTokens'] = $arguments['cacheTokens'] ?? false; $arguments['colors'] = $arguments['colors'] ?? ResultPrinter::COLOR_DEFAULT; $arguments['columns'] = $arguments['columns'] ?? 80; $arguments['convertDeprecationsToExceptions'] = $arguments['convertDeprecationsToExceptions'] ?? true; $arguments['convertErrorsToExceptions'] = $arguments['convertErrorsToExceptions'] ?? true; $arguments['convertNoticesToExceptions'] = $arguments['convertNoticesToExceptions'] ?? true; $arguments['convertWarningsToExceptions'] = $arguments['convertWarningsToExceptions'] ?? true; $arguments['crap4jThreshold'] = $arguments['crap4jThreshold'] ?? 30; $arguments['disallowTestOutput'] = $arguments['disallowTestOutput'] ?? false; $arguments['disallowTodoAnnotatedTests'] = $arguments['disallowTodoAnnotatedTests'] ?? false; $arguments['defaultTimeLimit'] = $arguments['defaultTimeLimit'] ?? 0; $arguments['enforceTimeLimit'] = $arguments['enforceTimeLimit'] ?? false; $arguments['excludeGroups'] = $arguments['excludeGroups'] ?? []; $arguments['failOnRisky'] = $arguments['failOnRisky'] ?? false; $arguments['failOnWarning'] = $arguments['failOnWarning'] ?? false; $arguments['executionOrderDefects'] = $arguments['executionOrderDefects'] ?? TestSuiteSorter::ORDER_DEFAULT; $arguments['groups'] = $arguments['groups'] ?? []; $arguments['processIsolation'] = $arguments['processIsolation'] ?? false; $arguments['processUncoveredFilesFromWhitelist'] = $arguments['processUncoveredFilesFromWhitelist'] ?? false; $arguments['randomOrderSeed'] = $arguments['randomOrderSeed'] ?? \time(); $arguments['registerMockObjectsFromTestArgumentsRecursively'] = $arguments['registerMockObjectsFromTestArgumentsRecursively'] ?? false; $arguments['repeat'] = $arguments['repeat'] ?? false; $arguments['reportHighLowerBound'] = $arguments['reportHighLowerBound'] ?? 90; $arguments['reportLowUpperBound'] = $arguments['reportLowUpperBound'] ?? 50; $arguments['reportUselessTests'] = $arguments['reportUselessTests'] ?? true; $arguments['reverseList'] = $arguments['reverseList'] ?? false; $arguments['executionOrder'] = $arguments['executionOrder'] ?? TestSuiteSorter::ORDER_DEFAULT; $arguments['resolveDependencies'] = $arguments['resolveDependencies'] ?? true; $arguments['stopOnError'] = $arguments['stopOnError'] ?? false; $arguments['stopOnFailure'] = $arguments['stopOnFailure'] ?? false; $arguments['stopOnIncomplete'] = $arguments['stopOnIncomplete'] ?? false; $arguments['stopOnRisky'] = $arguments['stopOnRisky'] ?? false; $arguments['stopOnSkipped'] = $arguments['stopOnSkipped'] ?? false; $arguments['stopOnWarning'] = $arguments['stopOnWarning'] ?? false; $arguments['stopOnDefect'] = $arguments['stopOnDefect'] ?? false; $arguments['strictCoverage'] = $arguments['strictCoverage'] ?? false; $arguments['testdoxExcludeGroups'] = $arguments['testdoxExcludeGroups'] ?? []; $arguments['testdoxGroups'] = $arguments['testdoxGroups'] ?? []; $arguments['timeoutForLargeTests'] = $arguments['timeoutForLargeTests'] ?? 60; $arguments['timeoutForMediumTests'] = $arguments['timeoutForMediumTests'] ?? 10; $arguments['timeoutForSmallTests'] = $arguments['timeoutForSmallTests'] ?? 1; $arguments['verbose'] = $arguments['verbose'] ?? false; } /** * @throws \ReflectionException * @throws \InvalidArgumentException */ private function processSuiteFilters(TestSuite $suite, array $arguments): void { if (!$arguments['filter'] && empty($arguments['groups']) && empty($arguments['excludeGroups'])) { return; } $filterFactory = new Factory; if (!empty($arguments['excludeGroups'])) { $filterFactory->addFilter( new ReflectionClass(ExcludeGroupFilterIterator::class), $arguments['excludeGroups'] ); } if (!empty($arguments['groups'])) { $filterFactory->addFilter( new ReflectionClass(IncludeGroupFilterIterator::class), $arguments['groups'] ); } if ($arguments['filter']) { $filterFactory->addFilter( new ReflectionClass(NameFilterIterator::class), $arguments['filter'] ); } $suite->injectFilter($filterFactory); } private function writeMessage(string $type, string $message): void { if (!$this->messagePrinted) { $this->write("\n"); } $this->write( \sprintf( "%-15s%s\n", $type . ':', $message ) ); $this->messagePrinted = true; } } phpunit-wrapper/src/NonFinal/JUnit.php000077700000027047151323602320013764 0ustar00<?php declare(strict_types=1); /* * This file is part of PHPUnit. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Codeception\PHPUnit\NonFinal; use DOMDocument; use DOMElement; use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\ExceptionWrapper; use PHPUnit\Framework\SelfDescribing; use PHPUnit\Framework\Test; use PHPUnit\Framework\TestFailure; use PHPUnit\Framework\TestListener; use PHPUnit\Framework\TestSuite; use PHPUnit\Framework\Warning; use PHPUnit\Util\Filter; use PHPUnit\Util\Printer; use PHPUnit\Util\Xml; use ReflectionClass; use ReflectionException; /** * @internal This class is not covered by the backward compatibility promise for PHPUnit */ class JUnit extends Printer implements TestListener { /** * @var DOMDocument */ protected $document; /** * @var DOMElement */ protected $root; /** * @var bool */ protected $reportUselessTests = false; /** * @var bool */ protected $writeDocument = true; /** * @var DOMElement[] */ protected $testSuites = []; /** * @var int[] */ protected $testSuiteTests = [0]; /** * @var int[] */ protected $testSuiteAssertions = [0]; /** * @var int[] */ protected $testSuiteErrors = [0]; /** * @var int[] */ protected $testSuiteFailures = [0]; /** * @var int[] */ protected $testSuiteSkipped = [0]; /** * @var int[] */ protected $testSuiteTimes = [0]; /** * @var int */ protected $testSuiteLevel = 0; /** * @var DOMElement */ protected $currentTestCase; /** * Constructor. * * @param null|mixed $out * * @throws \PHPUnit\Framework\Exception */ public function __construct($out = null, bool $reportUselessTests = false) { $this->document = new DOMDocument('1.0', 'UTF-8'); $this->document->formatOutput = true; $this->root = $this->document->createElement('testsuites'); $this->document->appendChild($this->root); parent::__construct($out); $this->reportUselessTests = $reportUselessTests; } /** * Flush buffer and close output. */ public function flush(): void { if ($this->writeDocument === true) { $this->write($this->getXML()); } parent::flush(); } /** * An error occurred. * * @throws \InvalidArgumentException * @throws ReflectionException */ public function addError(Test $test, \Throwable $t, float $time): void { $this->doAddFault($test, $t, $time, 'error'); $this->testSuiteErrors[$this->testSuiteLevel]++; } /** * A warning occurred. * * @throws \InvalidArgumentException * @throws ReflectionException */ public function addWarning(Test $test, Warning $e, float $time): void { $this->doAddFault($test, $e, $time, 'warning'); $this->testSuiteFailures[$this->testSuiteLevel]++; } /** * A failure occurred. * * @throws \InvalidArgumentException * @throws ReflectionException */ public function addFailure(Test $test, AssertionFailedError $e, float $time): void { $this->doAddFault($test, $e, $time, 'failure'); $this->testSuiteFailures[$this->testSuiteLevel]++; } /** * Incomplete test. */ public function addIncompleteTest(Test $test, \Throwable $t, float $time): void { $this->doAddSkipped($test); } /** * Risky test. * * @throws ReflectionException */ public function addRiskyTest(Test $test, \Throwable $t, float $time): void { if (!$this->reportUselessTests || $this->currentTestCase === null) { return; } $error = $this->document->createElement( 'error', Xml::prepareString( "Risky Test\n" . Filter::getFilteredStacktrace($t) ) ); $error->setAttribute('type', \get_class($t)); $this->currentTestCase->appendChild($error); $this->testSuiteErrors[$this->testSuiteLevel]++; } /** * Skipped test. */ public function addSkippedTest(Test $test, \Throwable $t, float $time): void { $this->doAddSkipped($test); } /** * A testsuite started. */ public function startTestSuite(TestSuite $suite): void { $testSuite = $this->document->createElement('testsuite'); $testSuite->setAttribute('name', $suite->getName()); if (\class_exists($suite->getName(), false)) { try { $class = new ReflectionClass($suite->getName()); $testSuite->setAttribute('file', $class->getFileName()); } catch (ReflectionException $e) { } } if ($this->testSuiteLevel > 0) { $this->testSuites[$this->testSuiteLevel]->appendChild($testSuite); } else { $this->root->appendChild($testSuite); } $this->testSuiteLevel++; $this->testSuites[$this->testSuiteLevel] = $testSuite; $this->testSuiteTests[$this->testSuiteLevel] = 0; $this->testSuiteAssertions[$this->testSuiteLevel] = 0; $this->testSuiteErrors[$this->testSuiteLevel] = 0; $this->testSuiteFailures[$this->testSuiteLevel] = 0; $this->testSuiteSkipped[$this->testSuiteLevel] = 0; $this->testSuiteTimes[$this->testSuiteLevel] = 0; } /** * A testsuite ended. */ public function endTestSuite(TestSuite $suite): void { $this->testSuites[$this->testSuiteLevel]->setAttribute( 'tests', (string) $this->testSuiteTests[$this->testSuiteLevel] ); $this->testSuites[$this->testSuiteLevel]->setAttribute( 'assertions', (string) $this->testSuiteAssertions[$this->testSuiteLevel] ); $this->testSuites[$this->testSuiteLevel]->setAttribute( 'errors', (string) $this->testSuiteErrors[$this->testSuiteLevel] ); $this->testSuites[$this->testSuiteLevel]->setAttribute( 'failures', (string) $this->testSuiteFailures[$this->testSuiteLevel] ); $this->testSuites[$this->testSuiteLevel]->setAttribute( 'skipped', (string) $this->testSuiteSkipped[$this->testSuiteLevel] ); $this->testSuites[$this->testSuiteLevel]->setAttribute( 'time', \sprintf('%F', $this->testSuiteTimes[$this->testSuiteLevel]) ); if ($this->testSuiteLevel > 1) { $this->testSuiteTests[$this->testSuiteLevel - 1] += $this->testSuiteTests[$this->testSuiteLevel]; $this->testSuiteAssertions[$this->testSuiteLevel - 1] += $this->testSuiteAssertions[$this->testSuiteLevel]; $this->testSuiteErrors[$this->testSuiteLevel - 1] += $this->testSuiteErrors[$this->testSuiteLevel]; $this->testSuiteFailures[$this->testSuiteLevel - 1] += $this->testSuiteFailures[$this->testSuiteLevel]; $this->testSuiteSkipped[$this->testSuiteLevel - 1] += $this->testSuiteSkipped[$this->testSuiteLevel]; $this->testSuiteTimes[$this->testSuiteLevel - 1] += $this->testSuiteTimes[$this->testSuiteLevel]; } $this->testSuiteLevel--; } /** * A test started. * * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException * @throws ReflectionException */ public function startTest(Test $test): void { $usesDataprovider = false; if (\method_exists($test, 'usesDataProvider')) { $usesDataprovider = $test->usesDataProvider(); } $testCase = $this->document->createElement('testcase'); $testCase->setAttribute('name', $test->getName()); $class = new ReflectionClass($test); $methodName = $test->getName(!$usesDataprovider); if ($class->hasMethod($methodName)) { $method = $class->getMethod($methodName); $testCase->setAttribute('class', $class->getName()); $testCase->setAttribute('classname', \str_replace('\\', '.', $class->getName())); $testCase->setAttribute('file', $class->getFileName()); $testCase->setAttribute('line', (string) $method->getStartLine()); } $this->currentTestCase = $testCase; } /** * A test ended. */ public function endTest(Test $test, float $time): void { $numAssertions = 0; if (\method_exists($test, 'getNumAssertions')) { $numAssertions = $test->getNumAssertions(); } $this->testSuiteAssertions[$this->testSuiteLevel] += $numAssertions; $this->currentTestCase->setAttribute( 'assertions', (string) $numAssertions ); $this->currentTestCase->setAttribute( 'time', \sprintf('%F', $time) ); $this->testSuites[$this->testSuiteLevel]->appendChild( $this->currentTestCase ); $this->testSuiteTests[$this->testSuiteLevel]++; $this->testSuiteTimes[$this->testSuiteLevel] += $time; $testOutput = ''; if (\method_exists($test, 'hasOutput') && \method_exists($test, 'getActualOutput')) { $testOutput = $test->hasOutput() ? $test->getActualOutput() : ''; } if (!empty($testOutput)) { $systemOut = $this->document->createElement( 'system-out', Xml::prepareString($testOutput) ); $this->currentTestCase->appendChild($systemOut); } $this->currentTestCase = null; } /** * Returns the XML as a string. */ public function getXML(): string { return $this->document->saveXML(); } /** * Enables or disables the writing of the document * in flush(). * * This is a "hack" needed for the integration of * PHPUnit with Phing. */ public function setWriteDocument(/*bool*/ $flag): void { if (\is_bool($flag)) { $this->writeDocument = $flag; } } /** * Method which generalizes addError() and addFailure() * * @throws \InvalidArgumentException * @throws ReflectionException */ private function doAddFault(Test $test, \Throwable $t, float $time, $type): void { if ($this->currentTestCase === null) { return; } if ($test instanceof SelfDescribing) { $buffer = $test->toString() . "\n"; } else { $buffer = ''; } $buffer .= TestFailure::exceptionToString($t) . "\n" . Filter::getFilteredStacktrace($t); $fault = $this->document->createElement( $type, Xml::prepareString($buffer) ); if ($t instanceof ExceptionWrapper) { $fault->setAttribute('type', $t->getClassName()); } else { $fault->setAttribute('type', \get_class($t)); } $this->currentTestCase->appendChild($fault); } private function doAddSkipped(Test $test): void { if ($this->currentTestCase === null) { return; } $skipped = $this->document->createElement('skipped'); $this->currentTestCase->appendChild($skipped); $this->testSuiteSkipped[$this->testSuiteLevel]++; } } phpunit-wrapper/src/NonFinal/NameFilterIterator.php000077700000006636151323602320016474 0ustar00<?php declare(strict_types=1); /* * This file is part of PHPUnit. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Codeception\PHPUnit\NonFinal; use PHPUnit\Framework\TestSuite; use PHPUnit\Framework\WarningTestCase; use PHPUnit\Util\RegularExpression; use RecursiveFilterIterator; use RecursiveIterator; /** * @internal This class is not covered by the backward compatibility promise for PHPUnit */ class NameFilterIterator extends RecursiveFilterIterator { /** * @var string */ protected $filter; /** * @var int */ protected $filterMin; /** * @var int */ protected $filterMax; /** * @throws \Exception */ public function __construct(RecursiveIterator $iterator, string $filter) { parent::__construct($iterator); $this->setFilter($filter); } /** * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException */ public function accept(): bool { $test = $this->getInnerIterator()->current(); if ($test instanceof TestSuite) { return true; } $tmp = \PHPUnit\Util\Test::describe($test); if ($test instanceof WarningTestCase) { $name = $test->getMessage(); } else { if ($tmp[0] !== '') { $name = \implode('::', $tmp); } else { $name = $tmp[1]; } } $accepted = @\preg_match($this->filter, $name, $matches); if ($accepted && isset($this->filterMax)) { $set = \end($matches); $accepted = $set >= $this->filterMin && $set <= $this->filterMax; } return (bool) $accepted; } /** * @throws \Exception */ protected function setFilter(string $filter): void { if (RegularExpression::safeMatch($filter, '') === false) { // Handles: // * testAssertEqualsSucceeds#4 // * testAssertEqualsSucceeds#4-8 if (\preg_match('/^(.*?)#(\d+)(?:-(\d+))?$/', $filter, $matches)) { if (isset($matches[3]) && $matches[2] < $matches[3]) { $filter = \sprintf( '%s.*with data set #(\d+)$', $matches[1] ); $this->filterMin = $matches[2]; $this->filterMax = $matches[3]; } else { $filter = \sprintf( '%s.*with data set #%s$', $matches[1], $matches[2] ); } } // Handles: // * testDetermineJsonError@JSON_ERROR_NONE // * testDetermineJsonError@JSON.* elseif (\preg_match('/^(.*?)@(.+)$/', $filter, $matches)) { $filter = \sprintf( '%s.*with data set "%s"$', $matches[1], $matches[2] ); } // Escape delimiters in regular expression. Do NOT use preg_quote, // to keep magic characters. $filter = \sprintf('/%s/i', \str_replace( '/', '\\/', $filter )); } $this->filter = $filter; } } phpunit-wrapper/src/NonFinal/.htaccess000077700000000177151323602320014013 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>phpunit-wrapper/src/TestCase.php000077700000002707151323602320012736 0ustar00<?php namespace Codeception\PHPUnit; abstract class TestCase extends \PHPUnit\Framework\TestCase { protected function setUp(): void { if (method_exists($this, '_setUp')) { $this->_setUp(); } } protected function tearDown(): void { if (method_exists($this, '_tearDown')) { $this->_tearDown(); } } public static function setUpBeforeClass(): void { if (method_exists(get_called_class(), '_setUpBeforeClass')) { static::_setUpBeforeClass(); } } public static function tearDownAfterClass(): void { if (method_exists(get_called_class(), '_tearDownAfterClass')) { static::_tearDownAfterClass(); } } public function expectExceptionMessageRegExp(string $regularExpression): void { $this->expectExceptionMessageMatches($regularExpression); } public static function assertRegExp(string $pattern, string $string, string $message = ''): void { parent::assertMatchesRegularExpression($pattern, $string, $message); } public static function assertNotRegExp(string $pattern, string $string, string $message = ''): void { parent::assertDoesNotMatchRegularExpression($pattern, $string, $message); } public static function assertFileNotExists(string $filename, string $message = ''): void { parent::assertFileDoesNotExist($filename, $message); } } phpunit-wrapper/src/phpunit5-loggers.php000077700000045674151323602320014451 0ustar00<?php // @codingStandardsIgnoreStart /* * This file is part of PHPUnit. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace { if (!class_exists('PHPUnit_Util_String')) { /** * String helpers. */ class PHPUnit_Util_String { /** * Converts a string to UTF-8 encoding. * * @param string $string * * @return string */ public static function convertToUtf8($string) { return mb_convert_encoding($string, 'UTF-8'); } /** * Checks a string for UTF-8 encoding. * * @param string $string * * @return bool */ protected static function isUtf8($string) { $length = strlen($string); for ($i = 0; $i < $length; $i++) { if (ord($string[$i]) < 0x80) { $n = 0; } elseif ((ord($string[$i]) & 0xE0) == 0xC0) { $n = 1; } elseif ((ord($string[$i]) & 0xF0) == 0xE0) { $n = 2; } elseif ((ord($string[$i]) & 0xF0) == 0xF0) { $n = 3; } else { return false; } for ($j = 0; $j < $n; $j++) { if ((++$i == $length) || ((ord($string[$i]) & 0xC0) != 0x80)) { return false; } } } return true; } } } } namespace PHPUnit\Util\Log { /* * This file is part of PHPUnit. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Codeception\Test\Descriptor; /** * A TestListener that generates JSON messages. */ if (!class_exists('\PHPUnit\Util\Log\JSON')) { class JSON extends \PHPUnit\Util\Printer implements \PHPUnit\Framework\TestListener { /** * @var string */ protected $currentTestSuiteName = ''; /** * @var string */ protected $currentTestName = ''; /** * @var bool */ protected $currentTestPass = true; /** * @var array */ protected $logEvents = []; /** * An error occurred. * * @param \PHPUnit\Framework\Test $test * @param \Throwable $e * @param float $time */ public function addError(\PHPUnit\Framework\Test $test, \Throwable $e, float $time): void { $this->writeCase( 'error', $time, \PHPUnit\Util\Filter::getFilteredStacktrace($e, false), \PHPUnit\Framework\TestFailure::exceptionToString($e), $test ); $this->currentTestPass = false; } /** * A warning occurred. * * @param \PHPUnit\Framework\Test $test * @param \PHPUnit\Framework\Warning $e * @param float $time */ public function addWarning(\PHPUnit\Framework\Test $test, \PHPUnit\Framework\Warning $e, float $time): void { $this->writeCase( 'warning', $time, \PHPUnit\Util\Filter::getFilteredStacktrace($e, false), \PHPUnit\Framework\TestFailure::exceptionToString($e), $test ); $this->currentTestPass = false; } /** * A failure occurred. * * @param \PHPUnit\Framework\Test $test * @param \PHPUnit\Framework\AssertionFailedError $e * @param float $time */ public function addFailure( \PHPUnit\Framework\Test $test, \PHPUnit\Framework\AssertionFailedError $e, float $time ): void{ $this->writeCase( 'fail', $time, \PHPUnit\Util\Filter::getFilteredStacktrace($e, false), \PHPUnit\Framework\TestFailure::exceptionToString($e), $test ); $this->currentTestPass = false; } /** * Incomplete test. * * @param \PHPUnit\Framework\Test $test * @param Throwable $e * @param float $time */ public function addIncompleteTest(\PHPUnit\Framework\Test $test, \Throwable $e, float $time): void { $this->writeCase( 'error', $time, \PHPUnit\Util\Filter::getFilteredStacktrace($e, false), 'Incomplete Test: ' . $e->getMessage(), $test ); $this->currentTestPass = false; } /** * Risky test. * * @param \PHPUnit\Framework\Test $test * @param Throwable $e * @param float $time */ public function addRiskyTest(\PHPUnit\Framework\Test $test, \Throwable $e, float $time): void { $this->writeCase( 'error', $time, \PHPUnit\Util\Filter::getFilteredStacktrace($e, false), 'Risky Test: ' . $e->getMessage(), $test ); $this->currentTestPass = false; } /** * Skipped test. * * @param \PHPUnit\Framework\Test $test * @param Throwable $e * @param float $time */ public function addSkippedTest(\PHPUnit\Framework\Test $test, \Throwable $e, float $time): void { $this->writeCase( 'error', $time, \PHPUnit\Util\Filter::getFilteredStacktrace($e, false), 'Skipped Test: ' . $e->getMessage(), $test ); $this->currentTestPass = false; } /** * A testsuite started. * * @param \PHPUnit\Framework\TestSuite $suite */ public function startTestSuite(\PHPUnit\Framework\TestSuite $suite): void { $this->currentTestSuiteName = $suite->getName(); $this->currentTestName = ''; $this->addLogEvent( [ 'event' => 'suiteStart', 'suite' => $this->currentTestSuiteName, 'tests' => count($suite) ] ); } /** * A testsuite ended. * * @param \PHPUnit\Framework\TestSuite $suite */ public function endTestSuite(\PHPUnit\Framework\TestSuite $suite): void { $this->currentTestSuiteName = ''; $this->currentTestName = ''; $this->writeArray($this->logEvents); } /** * A test started. * * @param \PHPUnit\Framework\Test $test */ public function startTest(\PHPUnit\Framework\Test $test): void { $this->currentTestName = \PHPUnit\Util\Test::describe($test); $this->currentTestPass = true; $this->addLogEvent( [ 'event' => 'testStart', 'suite' => $this->currentTestSuiteName, 'test' => $this->currentTestName ] ); } /** * A test ended. * * @param \PHPUnit\Framework\Test $test * @param float $time */ public function endTest(\PHPUnit\Framework\Test $test, float $time): void { if ($this->currentTestPass) { $this->writeCase('pass', $time, [], '', $test); } } /** * @param string $status * @param float $time * @param array $trace * @param string $message * @param \PHPUnit\Framework\TestCase|null $test */ protected function writeCase($status, float $time, array $trace = [], $message = '', $test = null): void { $output = ''; // take care of TestSuite producing error (e.g. by running into exception) as TestSuite doesn't have hasOutput if ($test !== null && method_exists($test, 'hasOutput') && $test->hasOutput()) { $output = $test->getActualOutput(); } $this->addLogEvent( [ 'event' => 'test', 'suite' => $this->currentTestSuiteName, 'test' => $this->currentTestName, 'status' => $status, 'time' => $time, 'trace' => $trace, 'message' => \PHPUnit_Util_String::convertToUtf8($message), 'output' => $output, ] ); } /** * @param array $event_data */ protected function addLogEvent($event_data = []): void { if (count($event_data)) { array_push($this->logEvents, $event_data); } } /** * @param array $buffer */ public function writeArray($buffer) { array_walk_recursive( $buffer, function (&$input){ if (is_string($input)) { $input = \PHPUnit_Util_String::convertToUtf8($input); } } ); $this->write(json_encode($buffer, JSON_PRETTY_PRINT)); } } } /* * This file is part of PHPUnit. * * (c) Sebastian Bergmann <sebastian@phpunit.de> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (!class_exists('\PHPUnit\Util\Log\TAP')) { /** * A TestListener that generates a logfile of the * test execution using the Test Anything Protocol (TAP). */ class TAP extends \PHPUnit\Util\Printer implements \PHPUnit\Framework\TestListener { /** * @var int */ protected $testNumber = 0; /** * @var int */ protected $testSuiteLevel = 0; /** * @var bool */ protected $testSuccessful = true; /** * Constructor. * * @param mixed $out * * @throws \PHPUnit\Framework\Throwable */ public function __construct($out = null) { parent::__construct($out); $this->write("TAP version 13\n"); } /** * An error occurred. * * @param \PHPUnit\Framework\Test $test * @param Throwable $e * @param float $time */ public function addError(\PHPUnit\Framework\Test $test, \Throwable $e, float $time): void { $this->writeNotOk($test, 'Error'); } /** * A warning occurred. * * @param \PHPUnit\Framework\Test $test * @param \PHPUnit\Framework\Warning $e * @param float $time */ public function addWarning(\PHPUnit\Framework\Test $test, \PHPUnit\Framework\Warning $e, float $time): void { $this->writeNotOk($test, 'Warning'); } /** * A failure occurred. * * @param \PHPUnit\Framework\Test $test * @param \PHPUnit\Framework\AssertionFailedError $e * @param float $time */ public function addFailure( \PHPUnit\Framework\Test $test, \PHPUnit\Framework\AssertionFailedError $e, float $time ): void{ $this->writeNotOk($test, 'Failure'); $message = explode( "\n", \PHPUnit\Framework\TestFailure::exceptionToString($e) ); $diagnostic = [ 'message' => $message[0], 'severity' => 'fail' ]; if ($e instanceof \PHPUnit\Framework\ExpectationFailedThrowable) { $cf = $e->getComparisonFailure(); if ($cf !== null) { $diagnostic['data'] = [ 'got' => $cf->getActual(), 'expected' => $cf->getExpected() ]; } } $yaml = new \Symfony\Component\Yaml\Dumper; $this->write( sprintf( " ---\n%s ...\n", $yaml->dump($diagnostic, 2, 2) ) ); } /** * Incomplete test. * * @param \PHPUnit\Framework\Test $test * @param \Throwable $e * @param float $time */ public function addIncompleteTest(\PHPUnit\Framework\Test $test, \Throwable $e, float $time): void { $this->writeNotOk($test, '', 'TODO Incomplete Test'); } /** * Risky test. * * @param \PHPUnit\Framework\Test $test * @param Throwable $e * @param float $time */ public function addRiskyTest(\PHPUnit\Framework\Test $test, \Throwable $e, float $time): void { $this->write( sprintf( "ok %d - # RISKY%s\n", $this->testNumber, $e->getMessage() != '' ? ' ' . $e->getMessage() : '' ) ); $this->testSuccessful = false; } /** * Skipped test. * * @param \PHPUnit\Framework\Test $test * @param Throwable $e * @param float $time */ public function addSkippedTest(\PHPUnit\Framework\Test $test, \Throwable $e, float $time): void { $this->write( sprintf( "ok %d - # SKIP%s\n", $this->testNumber, $e->getMessage() != '' ? ' ' . $e->getMessage() : '' ) ); $this->testSuccessful = false; } /** * A testsuite started. * * @param \PHPUnit\Framework\TestSuite $suite */ public function startTestSuite(\PHPUnit\Framework\TestSuite $suite): void { $this->testSuiteLevel++; } /** * A testsuite ended. * * @param \PHPUnit\Framework\TestSuite $suite */ public function endTestSuite(\PHPUnit\Framework\TestSuite $suite): void { $this->testSuiteLevel--; if ($this->testSuiteLevel == 0) { $this->write(sprintf("1..%d\n", $this->testNumber)); } } /** * A test started. * * @param \PHPUnit\Framework\Test $test */ public function startTest(\PHPUnit\Framework\Test $test): void { $this->testNumber++; $this->testSuccessful = true; } /** * A test ended. * * @param \PHPUnit\Framework\Test $test * @param float $time */ public function endTest(\PHPUnit\Framework\Test $test, float $time): void { if ($this->testSuccessful === true) { $this->write( sprintf( "ok %d - %s\n", $this->testNumber, Descriptor::getTestSignature($test) ) ); } $this->writeDiagnostics($test); } /** * @param \PHPUnit\Framework\Test $test * @param string $prefix * @param string $directive */ protected function writeNotOk(\PHPUnit\Framework\Test $test, $prefix = '', $directive = '') { $this->write( sprintf( "not ok %d - %s%s%s\n", $this->testNumber, $prefix != '' ? $prefix . ': ' : '', \PHPUnit\Util\Test::describeAsString($test), $directive != '' ? ' # ' . $directive : '' ) ); $this->testSuccessful = false; } /** * @param \PHPUnit\Framework\Test $test */ private function writeDiagnostics(\PHPUnit\Framework\Test $test) { if (!$test instanceof \PHPUnit\Framework\TestCase) { return; } if (!$test->hasOutput()) { return; } foreach (explode("\n", trim($test->getActualOutput())) as $line) { $this->write( sprintf( "# %s\n", $line ) ); } } } } } // @codingStandardsIgnoreEnd phpunit-wrapper/src/Listener.php000077700000011177151323602320013011 0ustar00<?php namespace Codeception\PHPUnit; use Codeception\PHPUnit\DispatcherWrapper; use Codeception\Event\FailEvent; use Codeception\Event\SuiteEvent; use Codeception\Event\TestEvent; use Codeception\Events; use Codeception\TestInterface; use Symfony\Component\EventDispatcher\EventDispatcher; class Listener implements \PHPUnit\Framework\TestListener { use DispatcherWrapper; /** * @var \Symfony\Component\EventDispatcher\EventDispatcher */ protected $dispatcher; protected $unsuccessfulTests = []; protected $skippedTests = []; protected $startedTests = []; public function __construct(EventDispatcher $dispatcher) { $this->dispatcher = $dispatcher; } /** * Risky test. * * @param PHPUnit\Framework\Test $test * @param \Throwable $e * @param float $time * @since Method available since Release 4.0.0 */ public function addRiskyTest(\PHPUnit\Framework\Test $test, \Throwable $e, float $time) : void { } public function addFailure(\PHPUnit\Framework\Test $test, \PHPUnit\Framework\AssertionFailedError $e, float $time) : void { $this->unsuccessfulTests[] = spl_object_hash($test); $this->fire(Events::TEST_FAIL, new FailEvent($test, $time, $e)); } public function addError(\PHPUnit\Framework\Test $test, \Throwable $e, float $time) : void { $this->unsuccessfulTests[] = spl_object_hash($test); $this->fire(Events::TEST_ERROR, new FailEvent($test, $time, $e)); } // This method was added in PHPUnit 6 public function addWarning(\PHPUnit\Framework\Test $test, \PHPUnit\Framework\Warning $e, float $time) : void { $this->unsuccessfulTests[] = spl_object_hash($test); $this->fire(Events::TEST_WARNING, new FailEvent($test, $time, $e)); } public function addIncompleteTest(\PHPUnit\Framework\Test $test, \Throwable $e, float $time) : void { if (in_array(spl_object_hash($test), $this->skippedTests)) { return; } $this->unsuccessfulTests[] = spl_object_hash($test); $this->fire(Events::TEST_INCOMPLETE, new FailEvent($test, $time, $e)); $this->skippedTests[] = spl_object_hash($test); } public function addSkippedTest(\PHPUnit\Framework\Test $test, \Throwable $e, float $time) : void { if (in_array(spl_object_hash($test), $this->skippedTests)) { return; } $this->unsuccessfulTests[] = spl_object_hash($test); $this->fire(Events::TEST_SKIPPED, new FailEvent($test, $time, $e)); $this->skippedTests[] = spl_object_hash($test); } public function startTestSuite(\PHPUnit\Framework\TestSuite $suite) : void { $this->dispatch($this->dispatcher, 'suite.start', new SuiteEvent($suite)); } public function endTestSuite(\PHPUnit\Framework\TestSuite $suite) : void { $this->dispatch($this->dispatcher, 'suite.end', new SuiteEvent($suite)); } public function startTest(\PHPUnit\Framework\Test $test) : void { $this->dispatch($this->dispatcher, Events::TEST_START, new TestEvent($test)); if (!$test instanceof TestInterface) { return; } if ($test->getMetadata()->isBlocked()) { return; } try { $this->startedTests[] = spl_object_hash($test); $this->fire(Events::TEST_BEFORE, new TestEvent($test)); } catch (\PHPUnit\Framework\IncompleteTestError $e) { $test->getTestResultObject()->addFailure($test, $e, 0); } catch (\PHPUnit\Framework\SkippedTestError $e) { $test->getTestResultObject()->addFailure($test, $e, 0); } catch (\Throwable $e) { $test->getTestResultObject()->addError($test, $e, 0); } } public function endTest(\PHPUnit\Framework\Test $test, float $time) : void { $hash = spl_object_hash($test); if (!in_array($hash, $this->unsuccessfulTests)) { $this->fire(Events::TEST_SUCCESS, new TestEvent($test, $time)); } if (in_array($hash, $this->startedTests)) { $this->fire(Events::TEST_AFTER, new TestEvent($test, $time)); } $this->dispatch($this->dispatcher, Events::TEST_END, new TestEvent($test, $time)); } protected function fire($event, TestEvent $eventType) { $test = $eventType->getTest(); if ($test instanceof TestInterface) { foreach ($test->getMetadata()->getGroups() as $group) { $this->dispatch($this->dispatcher, $event . '.' . $group, $eventType); } } $this->dispatch($this->dispatcher, $event, $eventType); } } phpunit-wrapper/src/ResultPrinter.php000077700000005503151323602320014042 0ustar00<?php namespace Codeception\PHPUnit; use \PHPUnit\Framework\AssertionFailedError; use \PHPUnit\Framework\Test; use PHPUnit\Framework\TestResult; use \PHPUnit\Runner\BaseTestRunner; class ResultPrinter extends \PHPUnit\Util\TestDox\ResultPrinter { /** * An error occurred. * * @param \PHPUnit\Framework\Test $test * @param \Throwable $e * @param float $time */ public function addError(\PHPUnit\Framework\Test $test, \Throwable $e, float $time) : void { $this->testStatus = \PHPUnit\Runner\BaseTestRunner::STATUS_ERROR; $this->failed++; } /** * A failure occurred. * * @param \PHPUnit\Framework\Test $test * @param \PHPUnit\Framework\AssertionFailedError $e * @param float $time */ public function addFailure(\PHPUnit\Framework\Test $test, \PHPUnit\Framework\AssertionFailedError $e, float $time) : void { $this->testStatus = \PHPUnit\Runner\BaseTestRunner::STATUS_FAILURE; $this->failed++; } /** * A warning occurred. * * @param \PHPUnit\Framework\Test $test * @param \PHPUnit\Framework\Warning $e * @param float $time */ public function addWarning(\PHPUnit\Framework\Test $test, \PHPUnit\Framework\Warning $e, float $time): void { $this->testStatus = \PHPUnit\Runner\BaseTestRunner::STATUS_WARNING; $this->warned++; } /** * Incomplete test. * * @param \PHPUnit\Framework\Test $test * @param \Throwable $e * @param float $time */ public function addIncompleteTest(\PHPUnit\Framework\Test $test, \Throwable $e, float $time) : void { $this->testStatus = \PHPUnit\Runner\BaseTestRunner::STATUS_INCOMPLETE; $this->incomplete++; } /** * Risky test. * * @param \PHPUnit\Framework\Test $test * @param \Throwable $e * @param float $time * * @since Method available since Release 4.0.0 */ public function addRiskyTest(\PHPUnit\Framework\Test $test, \Throwable $e, float $time) : void { $this->testStatus = \PHPUnit\Runner\BaseTestRunner::STATUS_RISKY; $this->risky++; } /** * Skipped test. * * @param \PHPUnit\Framework\Test $test * @param \Throwable $e * @param float $time * * @since Method available since Release 3.0.0 */ public function addSkippedTest(\PHPUnit\Framework\Test $test, \Throwable $e, float $time) : void { $this->testStatus = \PHPUnit\Runner\BaseTestRunner::STATUS_SKIPPED; $this->skipped++; } public function startTest(\PHPUnit\Framework\Test $test) : void { $this->testStatus = \PHPUnit\Runner\BaseTestRunner::STATUS_PASSED; } public function printResult(TestResult $result): void { // TODO: Implement printResult() method. } } phpunit-wrapper/src/Runner.php000077700000014531151323602320012472 0ustar00<?php namespace Codeception\PHPUnit; use Codeception\Configuration; use Codeception\Exception\ConfigurationException; class Runner extends NonFinal\TestRunner { public static $persistentListeners = []; protected $defaultListeners = [ 'xml' => false, 'phpunit-xml' => false, 'html' => false, 'tap' => false, 'json' => false, 'report' => false ]; protected $config = []; protected $logDir = null; public function __construct() { $this->config = Configuration::config(); $this->logDir = Configuration::outputDir(); // prepare log dir $this->phpUnitOverriders(); parent::__construct(); } public function phpUnitOverriders() { require_once __DIR__ . DIRECTORY_SEPARATOR . 'Overrides/Filter.php'; } /** * @return null|\PHPUnit\TextUI\ResultPrinter */ public function getPrinter() { return $this->printer; } public function prepareSuite(\PHPUnit\Framework\Test $suite, array &$arguments) { $this->handleConfiguration($arguments); $filterAdded = false; $filterFactory = new \PHPUnit\Runner\Filter\Factory(); if ($arguments['groups']) { $filterAdded = true; $filterFactory->addFilter( new \ReflectionClass('PHPUnit\Runner\Filter\IncludeGroupFilterIterator'), $arguments['groups'] ); } if ($arguments['excludeGroups']) { $filterAdded = true; $filterFactory->addFilter( new \ReflectionClass('PHPUnit\Runner\Filter\ExcludeGroupFilterIterator'), $arguments['excludeGroups'] ); } if ($arguments['filter']) { $filterAdded = true; $filterFactory->addFilter( new \ReflectionClass('Codeception\PHPUnit\FilterTest'), $arguments['filter'] ); } if ($filterAdded) { $suite->injectFilter($filterFactory); } } public function doEnhancedRun( \PHPUnit\Framework\Test $suite, \PHPUnit\Framework\TestResult $result, array $arguments = [] ) { unset($GLOBALS['app']); // hook for not to serialize globals $result->convertErrorsToExceptions(false); if (isset($arguments['report_useless_tests'])) { $result->beStrictAboutTestsThatDoNotTestAnything((bool)$arguments['report_useless_tests']); } if (isset($arguments['disallow_test_output'])) { $result->beStrictAboutOutputDuringTests((bool)$arguments['disallow_test_output']); } if (empty(self::$persistentListeners)) { $this->applyReporters($result, $arguments); } if (class_exists('\Symfony\Bridge\PhpUnit\SymfonyTestsListener')) { $arguments['listeners'] = isset($arguments['listeners']) ? $arguments['listeners'] : []; $listener = new \Symfony\Bridge\PhpUnit\SymfonyTestsListener(); $listener->globalListenerDisabled(); $arguments['listeners'][] = $listener; } $arguments['listeners'][] = $this->printer; // clean up listeners between suites foreach ($arguments['listeners'] as $listener) { $result->addListener($listener); } $suite->run($result); unset($suite); foreach ($arguments['listeners'] as $listener) { $result->removeListener($listener); } return $result; } /** * @param \PHPUnit\Framework\TestResult $result * @param array $arguments * * @return array */ protected function applyReporters(\PHPUnit\Framework\TestResult $result, array $arguments) { foreach ($this->defaultListeners as $listener => $value) { if (!isset($arguments[$listener])) { $arguments[$listener] = $value; } } if ($arguments['report']) { self::$persistentListeners[] = $this->instantiateReporter('report'); } if ($arguments['html']) { codecept_debug('Printing HTML report into ' . $arguments['html']); self::$persistentListeners[] = $this->instantiateReporter( 'html', [$this->absolutePath($arguments['html'])] ); } if ($arguments['xml']) { codecept_debug('Printing JUNIT report into ' . $arguments['xml']); self::$persistentListeners[] = $this->instantiateReporter( 'xml', [$this->absolutePath($arguments['xml']), (bool)$arguments['log_incomplete_skipped']] ); } if ($arguments['phpunit-xml']) { codecept_debug('Printing PHPUNIT report into ' . $arguments['phpunit-xml']); self::$persistentListeners[] = $this->instantiateReporter( 'phpunit-xml', [$this->absolutePath($arguments['phpunit-xml']), (bool)$arguments['log_incomplete_skipped']] ); } if ($arguments['tap']) { codecept_debug('Printing TAP report into ' . $arguments['tap']); self::$persistentListeners[] = $this->instantiateReporter('tap', [$this->absolutePath($arguments['tap'])]); } if ($arguments['json']) { codecept_debug('Printing JSON report into ' . $arguments['json']); self::$persistentListeners[] = $this->instantiateReporter( 'json', [$this->absolutePath($arguments['json'])] ); } foreach (self::$persistentListeners as $listener) { if ($listener instanceof ConsolePrinter) { $this->printer = $listener; continue; } $result->addListener($listener); } } protected function instantiateReporter($name, $args = []) { if (!isset($this->config['reporters'][$name])) { throw new ConfigurationException("Reporter $name not defined"); } return (new \ReflectionClass($this->config['reporters'][$name]))->newInstanceArgs($args); } private function absolutePath($path) { if ((strpos($path, '/') === 0) or (strpos($path, ':') === 1)) { // absolute path return $path; } return $this->logDir . $path; } } phpunit-wrapper/src/Constraint/Page.php000077700000004040151323602320014213 0ustar00<?php namespace Codeception\PHPUnit\Constraint; use Codeception\Lib\Console\Message; class Page extends \PHPUnit\Framework\Constraint\Constraint { protected $uri; protected $string; public function __construct($string, $uri = '') { $this->string = $this->normalizeText((string)$string); $this->uri = $uri; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) : bool { $other = $this->normalizeText($other); return mb_stripos($other, $this->string, null, 'UTF-8') !== false; } /** * @param $text * @return string */ private function normalizeText($text) { $text = strtr($text, "\r\n", " "); return trim(preg_replace('/\\s{2,}/', ' ', $text)); } /** * Returns a string representation of the constraint. * * @return string */ public function toString() : string { return sprintf( 'contains "%s"', $this->string ); } protected function failureDescription($pageContent) : string { $message = $this->uriMessage('on page'); $message->append("\n--> "); $message->append(mb_substr($pageContent, 0, 300, 'utf-8')); if (mb_strlen($pageContent, 'utf-8') > 300) { $debugMessage = new Message( "[Content too long to display. See complete response in '" . codecept_output_dir() . "' directory]" ); $message->append("\n")->append($debugMessage); } $message->append("\n--> "); return $message->getMessage() . $this->toString(); } protected function uriMessage($onPage = "") { if (!$this->uri) { return new Message(''); } $message = new Message($this->uri); $message->prepend(" $onPage "); return $message; } } phpunit-wrapper/src/Constraint/WebDriver.php000077700000004402151323602320015232 0ustar00<?php namespace Codeception\PHPUnit\Constraint; use Codeception\Exception\ElementNotFound; use Codeception\Lib\Console\Message; use Codeception\Util\Locator; use SebastianBergmann\Comparator\ComparisonFailure; class WebDriver extends Page { protected function matches($nodes) : bool { if (!count($nodes)) { return false; } if ($this->string === '') { return true; } foreach ($nodes as $node) { /** @var $node \WebDriverElement * */ if (!$node->isDisplayed()) { continue; } if (parent::matches(htmlspecialchars_decode($node->getText()))) { return true; } } return false; } protected function fail($nodes, $selector, ComparisonFailure $comparisonFailure = null) : void { if (!count($nodes)) { throw new ElementNotFound($selector, 'Element located either by name, CSS or XPath'); } $output = "Failed asserting that any element by " . Locator::humanReadableString($selector); $output .= $this->uriMessage('on page'); if (count($nodes) < 5) { $output .= "\nElements: "; $output .= $this->nodesList($nodes); } else { $message = new Message("[total %s elements]"); $output .= $message->with(count($nodes)); } $output .= "\ncontains text '" . $this->string . "'"; throw new \PHPUnit\Framework\ExpectationFailedException( $output, $comparisonFailure ); } protected function failureDescription($nodes) : string { $desc = ''; foreach ($nodes as $node) { $desc .= parent::failureDescription($node->getText()); } return $desc; } protected function nodesList($nodes, $contains = null) { $output = ""; foreach ($nodes as $node) { if ($contains && strpos($node->getText(), $contains) === false) { continue; } /** @var $node \WebDriverElement * */ $message = new Message("\n+ <%s> %s"); $output .= $message->with($node->getTagName(), $node->getText()); } return $output; } } phpunit-wrapper/src/Constraint/CrawlerNot.php000077700000002113151323602320015416 0ustar00<?php namespace Codeception\PHPUnit\Constraint; use SebastianBergmann\Comparator\ComparisonFailure; class CrawlerNot extends Crawler { protected function matches($nodes) : bool { return !parent::matches($nodes); } protected function fail($nodes, $selector, ComparisonFailure $comparisonFailure = null) : void { if (!$this->string) { throw new \PHPUnit\Framework\ExpectationFailedException( "Element '$selector' was found", $comparisonFailure ); } /** @var $nodes DomCrawler * */ $output = "There was '$selector' element"; $output .= $this->uriMessage('on page'); $output .= $this->nodesList($nodes, $this->string); $output .= "\ncontaining '{$this->string}'"; throw new \PHPUnit\Framework\ExpectationFailedException( $output, $comparisonFailure ); } public function toString() : string { if ($this->string) { return 'that contains text "' . $this->string . '"'; } } } phpunit-wrapper/src/Constraint/WebDriverNot.php000077700000002324151323602320015714 0ustar00<?php namespace Codeception\PHPUnit\Constraint; use SebastianBergmann\Comparator\ComparisonFailure; use Codeception\Util\Locator; class WebDriverNot extends WebDriver { protected function matches($nodes) : bool { return !parent::matches($nodes); } protected function fail($nodes, $selector, ComparisonFailure $comparisonFailure = null) : void { if (!is_string($selector) || strpos($selector, "'") === false) { $selector = Locator::humanReadableString($selector); } if (!$this->string) { throw new \PHPUnit\Framework\ExpectationFailedException( "Element $selector was found", $comparisonFailure ); } $output = "There was $selector element"; $output .= $this->uriMessage("on page"); $output .= $this->nodesList($nodes, $this->string); $output .= "\ncontaining '{$this->string}'"; throw new \PHPUnit\Framework\ExpectationFailedException( $output, $comparisonFailure ); } public function toString() : string { if ($this->string) { return 'that contains text "' . $this->string . '"'; } } } phpunit-wrapper/src/Constraint/JsonType.php000077700000003025151323602320015114 0ustar00<?php namespace Codeception\PHPUnit\Constraint; use Codeception\Util\JsonType as JsonTypeUtil; use Codeception\Util\JsonArray; class JsonType extends \PHPUnit\Framework\Constraint\Constraint { protected $jsonType; private $match; public function __construct(array $jsonType, $match = true) { $this->jsonType = $jsonType; $this->match = $match; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $jsonArray Value or object to evaluate. * * @return bool */ protected function matches($jsonArray) : bool { if ($jsonArray instanceof JsonArray) { $jsonArray = $jsonArray->toArray(); } $matched = (new JsonTypeUtil($jsonArray))->matches($this->jsonType); if ($this->match) { if ($matched !== true) { throw new \PHPUnit\Framework\ExpectationFailedException($matched); } } else { if ($matched === true) { throw new \PHPUnit\Framework\ExpectationFailedException('Unexpectedly response matched: ' . json_encode($jsonArray)); } } return true; } /** * Returns a string representation of the constraint. * * @return string */ public function toString() : string { //unused return ''; } protected function failureDescription($other) : string { //unused return ''; } } phpunit-wrapper/src/Constraint/JsonContains.php000077700000003424151323602320015754 0ustar00<?php namespace Codeception\PHPUnit\Constraint; use SebastianBergmann\Comparator\ComparisonFailure; use SebastianBergmann\Comparator\ArrayComparator; use SebastianBergmann\Comparator\Factory; use Codeception\Util\JsonArray; class JsonContains extends \PHPUnit\Framework\Constraint\Constraint { /** * @var */ protected $expected; public function __construct(array $expected) { $this->expected = $expected; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) : bool { $jsonResponseArray = new JsonArray($other); if (!is_array($jsonResponseArray->toArray())) { throw new \PHPUnit\Framework\AssertionFailedError('JSON response is not an array: ' . $other); } if ($jsonResponseArray->containsArray($this->expected)) { return true; } $comparator = new ArrayComparator(); $comparator->setFactory(new Factory); try { $comparator->assertEquals($this->expected, $jsonResponseArray->toArray()); } catch (ComparisonFailure $failure) { throw new \PHPUnit\Framework\ExpectationFailedException( "Response JSON does not contain the provided JSON\n", $failure ); } return false; } /** * Returns a string representation of the constraint. * * @return string */ public function toString() : string { //unused return ''; } protected function failureDescription($other) : string { //unused return ''; } } phpunit-wrapper/src/Constraint/.htaccess000077700000000177151323602320014433 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>phpunit-wrapper/src/Constraint/Crawler.php000077700000004061151323602320014741 0ustar00<?php namespace Codeception\PHPUnit\Constraint; use Codeception\Exception\ElementNotFound; use Codeception\Lib\Console\Message; use Symfony\Component\DomCrawler\Crawler as DomCrawler; use SebastianBergmann\Comparator\ComparisonFailure; class Crawler extends Page { protected function matches($nodes) : bool { /** @var $nodes DomCrawler * */ if (!$nodes->count()) { return false; } if ($this->string === '') { return true; } foreach ($nodes as $node) { if (parent::matches($node->nodeValue)) { return true; } } return false; } protected function fail($nodes, $selector, ComparisonFailure $comparisonFailure = null):void { /** @var $nodes DomCrawler * */ if (!$nodes->count()) { throw new ElementNotFound($selector, 'Element located either by name, CSS or XPath'); } $output = "Failed asserting that any element by '$selector'"; $output .= $this->uriMessage('on page'); $output .= " "; if ($nodes->count() < 10) { $output .= $this->nodesList($nodes); } else { $message = new Message("[total %s elements]"); $output .= $message->with($nodes->count())->getMessage(); } $output .= "\ncontains text '{$this->string}'"; throw new \PHPUnit\Framework\ExpectationFailedException( $output, $comparisonFailure ); } protected function failureDescription($other) : string { $desc = ''; foreach ($other as $o) { $desc .= parent::failureDescription($o->textContent); } return $desc; } protected function nodesList(DomCrawler $nodes, $contains = null) { $output = ""; foreach ($nodes as $node) { if ($contains && strpos($node->nodeValue, $contains) === false) { continue; } $output .= "\n+ " . $node->C14N(); } return $output; } } phpunit-wrapper/src/phpunit7-interfaces.php000077700000000707151323602320015120 0ustar00<?php // @codingStandardsIgnoreStart // PHPUnit 6 compatibility namespace PHPUnit\Framework { if (!interface_exists(Test::class, false)) { interface Test extends \Countable { public function run(TestResult $result = null); } } if (!interface_exists(SelfDescribing::class, false)) { interface SelfDescribing { public function toString(); } } } // @codingStandardsIgnoreEnd phpunit-wrapper/src/Init.php000077700000000420151323602320012114 0ustar00<?php namespace Codeception\PHPUnit; class Init { /** * @api */ public static function init() { require_once __DIR__ . DIRECTORY_SEPARATOR . 'phpunit7-interfaces.php'; require_once __DIR__ . DIRECTORY_SEPARATOR . 'shim.php'; } }phpunit-wrapper/src/FilterTest.php000077700000002475151323602320013312 0ustar00<?php namespace Codeception\PHPUnit; use Codeception\PHPUnit\NonFinal\NameFilterIterator; use Codeception\Test\Descriptor; /** * Extended Filter Test from PHPUnit to use Codeception's Descriptor to locate tests. * * Class FilterTest * @package Codeception\PHPUnit */ class FilterTest extends NameFilterIterator { public function accept():bool { $test = $this->getInnerIterator()->current(); if ($test instanceof \PHPUnit\Framework\TestSuite) { return true; } $name = Descriptor::getTestSignature($test); $index = Descriptor::getTestDataSetIndex($test); if (!is_null($index)) { $name .= " with data set #{$index}"; } $accepted = preg_match($this->filter, $name, $matches); // This fix the issue when an invalid dataprovider method generate a warning // See issue https://github.com/Codeception/Codeception/issues/4888 if($test instanceof \PHPUnit\Framework\WarningTestCase) { $message = $test->getMessage(); $accepted = preg_match($this->filter, $message, $matches); } if ($accepted && isset($this->filterMax)) { $set = end($matches); $accepted = $set >= $this->filterMin && $set <= $this->filterMax; } return $accepted; } } phpunit-wrapper/src/ConsolePrinter.php000077700000000566151323602320014172 0ustar00<?php namespace Codeception\PHPUnit; /** * Printer implementing this interface prints output to console, thus should be marked as printer and not just a logger * * Interface ConsolePrinter * @package Codeception\PHPUnit */ interface ConsolePrinter { public function write(string $buffer); public function printResult(\PHPUnit\Framework\TestResult $result); } phpunit-wrapper/src/.htaccess000077700000000177151323602320012307 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>phpunit-wrapper/src/shim.php000077700000001234151323602320012155 0ustar00<?php // @codingStandardsIgnoreStart // Add aliases for PHPUnit 6 namespace { if (!class_exists('PHPUnit_Framework_Assert')) { class_alias('PHPUnit\Framework\Assert', 'PHPUnit_Framework_Assert'); } if (!class_exists('PHPUnit_Framework_TestCase')) { class_alias('PHPUnit\Framework\TestCase', 'PHPUnit_Framework_TestCase'); } if (!class_exists('PHPUnit\Util\Log\JSON') || !class_exists('PHPUnit\Util\Log\TAP')) { if (class_exists('PHPUnit\Util\Printer')) { require_once __DIR__ . '/phpunit5-loggers.php'; // TAP and JSON loggers were removed in PHPUnit 6 } } } // @codingStandardsIgnoreEnd phpunit-wrapper/src/ResultPrinter/HTML.php000077700000022774151323602320014617 0ustar00<?php namespace Codeception\PHPUnit\ResultPrinter; use Codeception\PHPUnit\ResultPrinter as CodeceptionResultPrinter; use Codeception\Step; use Codeception\Step\Meta; use Codeception\Test\Descriptor; use Codeception\Test\Interfaces\ScenarioDriven; use Codeception\TestInterface; use Codeception\Util\PathResolver; use PHPUnit\Framework\TestResult; use SebastianBergmann\Template\Template; class HTML extends CodeceptionResultPrinter { /** * @var boolean */ protected $printsHTML = true; /** * @var integer */ protected $id = 0; /** * @var string */ protected $scenarios = ''; /** * @var string */ protected $templatePath; /** * @var int */ protected $timeTaken = 0; protected $failures = []; /** * Constructor. * * @param mixed $out * @throws InvalidArgumentException */ public function __construct($out = null) { parent::__construct($out); $this->templatePath = sprintf( '%s%stemplate%s', __DIR__, DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR ); } /** * Handler for 'start class' event. * * @param string $name */ protected function startClass(string $name):void { } public function endTest(\PHPUnit\Framework\Test $test, float $time) : void { $steps = []; $success = ($this->testStatus == \PHPUnit\Runner\BaseTestRunner::STATUS_PASSED); if ($success) { $this->successful++; } if ($test instanceof ScenarioDriven) { $steps = $test->getScenario()->getSteps(); } $this->timeTaken += $time; switch ($this->testStatus) { case \PHPUnit\Runner\BaseTestRunner::STATUS_FAILURE: $scenarioStatus = 'scenarioFailed'; break; case \PHPUnit\Runner\BaseTestRunner::STATUS_SKIPPED: $scenarioStatus = 'scenarioSkipped'; break; case \PHPUnit\Runner\BaseTestRunner::STATUS_INCOMPLETE: $scenarioStatus = 'scenarioIncomplete'; break; case \PHPUnit\Runner\BaseTestRunner::STATUS_ERROR: $scenarioStatus = 'scenarioFailed'; break; default: $scenarioStatus = 'scenarioSuccess'; } $stepsBuffer = ''; $subStepsRendered = []; foreach ($steps as $step) { if ($step->getMetaStep()) { $key = $step->getMetaStep()->getLine() . $step->getMetaStep()->getAction(); $subStepsRendered[$key][] = $this->renderStep($step); } } foreach ($steps as $step) { if ($step->getMetaStep()) { $key = $step->getMetaStep()->getLine() . $step->getMetaStep()->getAction(); if (! empty($subStepsRendered[$key])) { $subStepsBuffer = implode('', $subStepsRendered[$key]); unset($subStepsRendered[$key]); $stepsBuffer .= $this->renderSubsteps($step->getMetaStep(), $subStepsBuffer); } } else { $stepsBuffer .= $this->renderStep($step); } } $scenarioTemplate = new Template( $this->templatePath . 'scenario.html' ); $failures = ''; $name = Descriptor::getTestSignatureUnique($test); if (isset($this->failures[$name])) { $failTemplate = new Template( $this->templatePath . 'fail.html' ); foreach ($this->failures[$name] as $failure) { $failTemplate->setVar(['fail' => nl2br($failure)]); $failures .= $failTemplate->render() . PHP_EOL; } $this->failures[$name] = []; } $png = ''; $html = ''; if ($test instanceof TestInterface) { $reports = $test->getMetadata()->getReports(); if (isset($reports['png'])) { $localPath = PathResolver::getRelativeDir($reports['png'], codecept_output_dir()); $png = "<tr><td class='error'><div class='screenshot'><img src='$localPath' alt='failure screenshot'></div></td></tr>"; } if (isset($reports['html'])) { $localPath = PathResolver::getRelativeDir($reports['html'], codecept_output_dir()); $html = "<tr><td class='error'>See <a href='$localPath' target='_blank'>HTML snapshot</a> of a failed page</td></tr>"; } } $toggle = $stepsBuffer ? '<span class="toggle">+</span>' : ''; $testString = htmlspecialchars(ucfirst(Descriptor::getTestAsString($test))); $testString = preg_replace('~^([\s\w\\\]+):\s~', '<span class="quiet">$1 »</span> ', $testString); $scenarioTemplate->setVar( [ 'id' => ++$this->id, 'name' => $testString, 'scenarioStatus' => $scenarioStatus, 'steps' => $stepsBuffer, 'toggle' => $toggle, 'failure' => $failures, 'png' => $png, 'html' => $html, 'time' => round($time, 2) ] ); $this->scenarios .= $scenarioTemplate->render(); } public function startTestSuite(\PHPUnit\Framework\TestSuite $suite) : void { $suiteTemplate = new Template( $this->templatePath . 'suite.html' ); if (!$suite->getName()) { return; } $suiteTemplate->setVar(['suite' => ucfirst($suite->getName())]); $this->scenarios .= $suiteTemplate->render(); } /** * Handler for 'end run' event. */ protected function endRun():void { $scenarioHeaderTemplate = new Template( $this->templatePath . 'scenario_header.html' ); $status = !$this->failed ? '<span style="color: green">OK</span>' : '<span style="color: #e74c3c">FAILED</span>'; $scenarioHeaderTemplate->setVar( [ 'name' => 'Codeception Results', 'status' => $status, 'time' => round($this->timeTaken, 1) ] ); $header = $scenarioHeaderTemplate->render(); $scenariosTemplate = new Template( $this->templatePath . 'scenarios.html' ); $scenariosTemplate->setVar( [ 'header' => $header, 'scenarios' => $this->scenarios, 'successfulScenarios' => $this->successful, 'failedScenarios' => $this->failed, 'skippedScenarios' => $this->skipped, 'incompleteScenarios' => $this->incomplete ] ); $this->write($scenariosTemplate->render()); } /** * An error occurred. * * @param \PHPUnit\Framework\Test $test * @param \Exception $e * @param float $time */ public function addError(\PHPUnit\Framework\Test $test, \Throwable $e, float $time) : void { $this->failures[Descriptor::getTestSignatureUnique($test)][] = $this->cleanMessage($e); parent::addError($test, $e, $time); } /** * A failure occurred. * * @param \PHPUnit\Framework\Test $test * @param \PHPUnit\Framework\AssertionFailedError $e * @param float $time */ public function addFailure(\PHPUnit\Framework\Test $test, \PHPUnit\Framework\AssertionFailedError $e, float $time) : void { $this->failures[Descriptor::getTestSignatureUnique($test)][] = $this->cleanMessage($e); parent::addFailure($test, $e, $time); } /** * Starts test * * @param \PHPUnit\Framework\Test $test */ public function startTest(\PHPUnit\Framework\Test $test):void { $name = Descriptor::getTestSignatureUnique($test); if (isset($this->failures[$name])) { // test failed in before hook return; } // start test and mark initialize as passed parent::startTest($test); } /** * @param $step * @return string */ protected function renderStep(Step $step) { $stepTemplate = new Template($this->templatePath . 'step.html'); $stepTemplate->setVar(['action' => $step->getHtml(), 'error' => $step->hasFailed() ? 'failedStep' : '']); return $stepTemplate->render(); } /** * @param $metaStep * @param $substepsBuffer * @return string */ protected function renderSubsteps(Meta $metaStep, $substepsBuffer) { $metaTemplate = new Template($this->templatePath . 'substeps.html'); $metaTemplate->setVar(['metaStep' => $metaStep->getHtml(), 'error' => $metaStep->hasFailed() ? 'failedStep' : '', 'steps' => $substepsBuffer, 'id' => uniqid()]); return $metaTemplate->render(); } private function cleanMessage($exception) { $msg = $exception->getMessage(); if ($exception instanceof \PHPUnit\Framework\ExpectationFailedException && $exception->getComparisonFailure()) { $msg .= $exception->getComparisonFailure()->getDiff(); } $msg = str_replace(['<info>','</info>','<bold>','</bold>'], ['','','',''], $msg); return htmlentities($msg); } public function printResult(TestResult $result): void { } } phpunit-wrapper/src/ResultPrinter/Report.php000077700000003622151323602320015315 0ustar00<?php namespace Codeception\PHPUnit\ResultPrinter; use Codeception\Lib\Console\Output; use Codeception\PHPUnit\ConsolePrinter; use Codeception\PHPUnit\ResultPrinter; use Codeception\Test\Descriptor; class Report extends ResultPrinter implements ConsolePrinter { /** * @param \PHPUnit\Framework\Test $test * @param float $time */ public function endTest(\PHPUnit\Framework\Test $test, float $time) : void { $name = Descriptor::getTestAsString($test); $success = ($this->testStatus == \PHPUnit\Runner\BaseTestRunner::STATUS_PASSED); if ($success) { $this->successful++; } if ($this->testStatus == \PHPUnit\Runner\BaseTestRunner::STATUS_FAILURE) { $status = "\033[41;37mFAIL\033[0m"; } elseif ($this->testStatus == \PHPUnit\Runner\BaseTestRunner::STATUS_SKIPPED) { $status = 'Skipped'; } elseif ($this->testStatus == \PHPUnit\Runner\BaseTestRunner::STATUS_INCOMPLETE) { $status = 'Incomplete'; } elseif ($this->testStatus == \PHPUnit\Runner\BaseTestRunner::STATUS_ERROR) { $status = 'ERROR'; } else { $status = 'Ok'; } if (strlen($name) > 75) { $name = substr($name, 0, 70); } $line = $name . str_repeat('.', 75 - strlen($name)); $line .= $status; $this->write($line . "\n"); } protected function endRun() : void { $this->write("\nCodeception Results\n"); $this->write(sprintf( "Successful: %s. Failed: %s. Incomplete: %s. Skipped: %s", $this->successful, $this->failed, $this->skipped, $this->incomplete ) . "\n"); } public function printResult(\PHPUnit\Framework\TestResult $result): void { } public function write(string $buffer) : void { parent::write($buffer); } } phpunit-wrapper/src/ResultPrinter/template/substeps.html.dist000077700000000443151323602320020642 0ustar00<tr> <td class="stepName {error}" ><p onclick="showHide('{id}', this)"><span class="toggle">+</span> {metaStep}</p> </td> </tr> <tr> <td class="nostyle"> <table border="0" width="100%" class="substeps scenarioStepsTable" id="stepContainer{id}"> {steps} </table> </td> </tr> phpunit-wrapper/src/ResultPrinter/template/scenario_header.html.dist000077700000000063151323602320022103 0ustar00<h1>{name} <small>{status} ({time}s)</small></h1> phpunit-wrapper/src/ResultPrinter/template/scenarios.html.dist000077700000015435151323602320020767 0ustar00<html> <head> <title>Test results</title> <meta charset='utf-8'> <link href='https://fonts.googleapis.com/css?family=Varela+Round&v2' rel='stylesheet' type='text/css'> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> .layout { margin: 0 auto; max-width: 1000px; } body { font-family: arial, serif; margin: 0; padding: 0; background: #ecf0f1; font-size: 20px; } h1,h2,h3 { font-family: arial, serif; color: #7f8c8d; } h1 { font-size: 2.5em; } h2 { font-size: 1.3em; } h3 { font-size: 1em; color: #84BBDD; margin: 0.5em 0; } table { border: none; margin: 0; padding: 0; font-size: 0.9em;} .scenarioStepsTable .stepName { padding: 5px; } .scenarioStepsTable td { background: #fff; } .quiet { color: #333; font-size: 0.8em; } .screenshot { max-height: 400px; overflow-y: scroll; display: block; } .screenshot img { zoom: 0.5; } @media (max-width: 1200px) { #toolbar-filter { display: none !important; } } .scenarioStepsTable .nostyle { background: none; border: none; } p { cursor: pointer; } .scenarioRow>td>p { padding: 5px; } .scenarioStepsTable .failedStep { padding: 10px; background: #ecf0f1; border: 2px solid #e74c3c; border-radius: 0px; color: #e74c3c; } .scenarioStepsTable .error { background: #999; padding: 10px; color: #fff; border-radius: 0px; } .scenarioStepsTable .error a { color: #eef; } .scenarioStepsTable.substeps td { background: #bdc3c7; } .header { font-size: large; font-weight: bold; } p.scenarioSuccess { background: rgb(157,213,58); /* Old browsers */ } .scenario { color: black; } p.scenarioFailed, p.scenarioError { color: black; background: #e74c3c } table.scenarioFailed tr:last-child { font-weight: bold; } td.scenarioSuccess { color: green } td.scenarioFailed { color: red } .scenarioSkipped { color: teal; } .scenarioIncomplete { color: gray; } .scenarioStepsTable { margin-left: 10px; display: none; color: #333; } #stepContainerSummary { background: white; -webkit-border-radius: 5px; -moz-border-radius: 5px; border-radius: 5px; padding: 20px; } .toggle { background: rgba(255,255,255,0.5); border-radius: 10px; display: inline-block; width: 20px; height: 20px; text-align: center; margin: auto; color: #666 } ul#toolbar-filter { display: block; position: fixed; top: 20px; left: 0px; padding: 0px; } ul#toolbar-filter li { list-style: none; text-align: center; padding: 20px; background-color: #3498db; } ul#toolbar-filter li a, ul#toolbar-filter li a:hover, ul#toolbar-filter li a:visited { color: #34495e; text-decoration: none; } ul#toolbar-filter li.disabled { background-color: #bdc3c7; } </style> <script type="text/javascript"> var showAll = true; function showHide(nodeId, linkObj) { var subObj = document.getElementById('stepContainer' + nodeId); var toggle = linkObj.childNodes[0]; if (toggle.innerHTML != '-') { toggle.innerHTML = '-'; subObj.style.display='block'; subObj.style.width = '100%'; } else { toggle.innerHTML = '+'; subObj.style.display='none'; } } function showAllScenarios() { var toolbar = document.getElementById('toolbar-filter'); for (var i = 0; i < toolbar.children.length; i++) { toolbar.children[i].className = ''; } var trs = document.getElementsByTagName('tr'); for(var z = 0; z < trs.length; z++) { trs[z].style.display = ''; } showAll = true; } function toggleScenarios(name, linkObj) { var links = document.getElementById('toolbar-filter').children; var rows = document.getElementsByClassName('scenarioRow'); if (showAll) { for (var i = 0; i < links.length; i++) { links[i].className = 'disabled'; } for (var i = 0; i < rows.length; i++) { rows[i].style.display = 'none'; } } showAll = false; if (linkObj.className == '') { linkObj.className = 'disabled'; for (var i = 0; i < rows.length; i++) { if (rows[i].classList.contains(name)) { rows[i].style.display = 'none'; } } return; } if (linkObj.className == 'disabled') { linkObj.className = ''; for (var i = 0; i < rows.length; i++) { if (rows[i].classList.contains(name)) { rows[i].style.display = 'table-row'; } } return; } } </script> </head> <body> <ul id="toolbar-filter"> <li> <a href="#" title="Show all" onClick="showAllScenarios()">◯</a></li> <li> <a href="#" title="Successful" onClick="toggleScenarios('scenarioSuccess', this.parentElement)"><strong>✔</strong> {successfulScenarios}</a></li> <li> <a href="#" title="Failed" onClick="toggleScenarios('scenarioFailed', this.parentElement)"><strong>✗</strong> {failedScenarios}</a></li> <li> <a href="#" title="Skipped" onClick="toggleScenarios('scenarioSkipped', this.parentElement)"><strong>S</strong> {skippedScenarios}</a></li> <li> <a href="#" title="Incomplete" onClick="toggleScenarios('scenarioIncomplete', this.parentElement)"><strong>I</strong> {incompleteScenarios}</a></li> </ul> <div class="layout"> {header} <table border="0" style="width: 100%;"> {scenarios} <tr> <td> <h2>Summary</h2> <div id="stepContainerSummary"> <table border="0"> <tr> <td width="250" class="scenarioSuccess">Successful scenarios:</td> <td class="scenarioSuccessValue"><strong>{successfulScenarios}</strong></td> </tr> <tr> <td class="scenarioFailed">Failed scenarios:</td> <td class="scenarioFailedValue"><strong>{failedScenarios}</strong></td> </tr> <tr> <td class="scenarioSkipped">Skipped scenarios:</td> <td class="scenarioSkippedValue"><strong>{skippedScenarios}</strong></td> </tr> <tr> <td class="scenarioIncomplete">Incomplete scenarios:</td> <td class="scenarioIncompleteValue"><strong>{incompleteScenarios}</strong></td> </tr> </table> </div> </td> </tr> </table> </div> </body> </html> phpunit-wrapper/src/ResultPrinter/template/fail.html.dist000077700000000073151323602320017704 0ustar00<tr > <td class="error"> {fail} </td> </tr>phpunit-wrapper/src/ResultPrinter/template/scenario.html.dist000077700000000745151323602320020602 0ustar00 <tr class="scenarioRow {scenarioStatus}"> <td> <p class="{scenarioStatus}" onclick="showHide('{id}', this)">{toggle} {name} <span style="color: #34495e; font-size: 70%;">{time}s</span></p> </td> </tr> <tr class="scenarioRow {scenarioStatus}"> <td> <table border="0" width="100%" class="{scenarioStatus} scenarioStepsTable" id="stepContainer{id}"> {steps} {failure} {png} {html} </table> </td> </tr> phpunit-wrapper/src/ResultPrinter/template/suite.html.dist000077700000000063151323602320020121 0ustar00<tr> <td> <h3>{suite} Tests</h3> </td> </tr>phpunit-wrapper/src/ResultPrinter/template/.htaccess000077700000000177151323602320016744 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>phpunit-wrapper/src/ResultPrinter/template/step.html.dist000077700000000142151323602320017741 0ustar00 <tr> <td class="stepName {error}"> {action}</td> </tr> phpunit-wrapper/src/ResultPrinter/UI.php000077700000006264151323602320014364 0ustar00<?php namespace Codeception\PHPUnit\ResultPrinter; use Codeception\Event\FailEvent; use Codeception\Events; use Codeception\PHPUnit\DispatcherWrapper; use Codeception\Test\Unit; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\EventDispatcher\EventDispatcher; class UI extends \PHPUnit\TextUI\DefaultResultPrinter { use DispatcherWrapper; /** * @var EventDispatcher */ protected $dispatcher; public function __construct(EventDispatcher $dispatcher, $options, $out = null) { parent::__construct($out, $options['verbosity'] > OutputInterface::VERBOSITY_NORMAL, $options['colors'] ? 'always' : 'never'); $this->dispatcher = $dispatcher; } protected function printDefect(\PHPUnit\Framework\TestFailure $defect, int $count): void { $this->write("\n---------\n"); $this->dispatch( $this->dispatcher, Events::TEST_FAIL_PRINT, new FailEvent($defect->failedTest(), null, $defect->thrownException(), $count) ); } /** * @param \PHPUnit\Framework\TestFailure $defect */ protected function printDefectTrace(\PHPUnit\Framework\TestFailure $defect): void { $this->write($defect->getExceptionAsString()); $this->writeNewLine(); $stackTrace = \PHPUnit\Util\Filter::getFilteredStacktrace($defect->thrownException()); foreach ($stackTrace as $i => $frame) { if (!isset($frame['file'])) { continue; } $this->write( sprintf( "#%d %s(%s)", $i + 1, $frame['file'], isset($frame['line']) ? $frame['line'] : '?' ) ); $this->writeNewLine(); } } public function startTest(\PHPUnit\Framework\Test $test) : void { if ($test instanceof Unit) { parent::startTest($test); } } public function endTest(\PHPUnit\Framework\Test $test, float $time) : void { if ($test instanceof \PHPUnit\Framework\TestCase or $test instanceof \Codeception\Test\Test) { $this->numAssertions += $test->getNumAssertions(); } $this->lastTestFailed = false; } public function addError(\PHPUnit\Framework\Test $test, \Throwable $e, float $time) : void { $this->lastTestFailed = true; } public function addFailure(\PHPUnit\Framework\Test $test, \PHPUnit\Framework\AssertionFailedError $e, float $time) : void { $this->lastTestFailed = true; } public function addWarning(\PHPUnit\Framework\Test $test, \PHPUnit\Framework\Warning $e, float $time) : void { $this->lastTestFailed = true; } public function addIncompleteTest(\PHPUnit\Framework\Test $test, \Throwable $e, float $time) : void { $this->lastTestFailed = true; } public function addRiskyTest(\PHPUnit\Framework\Test $test, \Throwable $e, float $time) : void { $this->lastTestFailed = true; } public function addSkippedTest(\PHPUnit\Framework\Test $test, \Throwable $e, float $time) : void { $this->lastTestFailed = true; } } phpunit-wrapper/src/ResultPrinter/.htaccess000077700000000177151323602320015131 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>phpunit-wrapper/.gitignore000077700000000100151323602320011674 0ustar00/.idea /composer.lock /vendor /tests /codecept /codeception.yml phpunit-wrapper/README.md000077700000001367151323602320011203 0ustar00# PHPUnit Wrapper Builds: * 9.0  * 8.0  * 7.1  * 6.5  * 6.0  Codeception heavily relies on PHPUnit for running and managing tests. Not all PHPUnit classes fit the needs of Codeception, that's why they were extended or redefined. Releases follow major PHPUnit versions. phpunit-wrapper/RoboFile.php000077700000003601151323602320012127 0ustar00<?php /** * This is project's console commands configuration for Robo task runner. * * @see http://robo.li/ */ class RoboFile extends \Robo\Tasks { // define public methods as commands public function prepareDependencies() { $config = json_decode(file_get_contents(__DIR__ . '/composer.json'), true); $config['name'] = 'codeception/phpunit-wrapper-test'; $config['require-dev']['codeception/codeception'] = getenv('CODECEPTION_VERSION'); $config['require-dev']['codeception/module-asserts'] = 'dev-master'; $config['require-dev']['codeception/module-cli'] = '*'; $config['require-dev']['codeception/module-db'] = '*'; $config['require-dev']['codeception/module-filesystem'] = '*'; $config['require-dev']['codeception/module-phpbrowser'] = '*'; $config['require-dev']['codeception/util-universalframework'] = '*'; $config['replace'] = ['codeception/phpunit-wrapper' => '*']; file_put_contents(__DIR__ . '/composer.json', json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); } public function prepareTests() { $this->_copyDir(__DIR__ . '/vendor/codeception/codeception/tests', __DIR__ . '/tests'); $this->_copy(__DIR__ . '/vendor/codeception/codeception/codeception.yml', __DIR__ .'/codeception.yml'); $this->_symlink(__DIR__ . '/vendor/bin/codecept', __DIR__ . '/codecept'); } public function prepareTestAutoloading() { $config = json_decode(file_get_contents(__DIR__ . '/composer.json'), true); $config['autoload-dev'] = [ 'classmap' => [ 'tests/cli/_steps', 'tests/data/DummyClass.php', 'tests/data/claypit/tests/_data' ] ]; file_put_contents(__DIR__ . '/composer.json', json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); } } phpunit-wrapper/.github/workflows/build.yml000077700000001565151323602320015143 0ustar00name: CI on: [push, pull_request] jobs: tests: runs-on: ubuntu-latest strategy: matrix: php: [7.3, 7.4, 8.0] env: CODECEPTION_VERSION: '4.1.x-dev' steps: - name: Checkout code uses: actions/checkout@v2 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} coverage: xdebug - name: Prepare dependencies run: | composer update php ./vendor/bin/robo prepare:dependencies composer update php ./vendor/bin/robo prepare:tests php ./vendor/bin/robo prepare:test-autoloading composer dump-autoload - name: Run test suite run: | php ./codecept run -c vendor/codeception/module-asserts/ php ./codecept run unit -g core php ./codecept run cli phpunit-wrapper/.github/workflows/.htaccess000077700000000177151323602320015115 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>phpunit-wrapper/.github/.htaccess000077700000000177151323602320013060 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>phpunit-wrapper/composer.json000077700000001211151323602320012432 0ustar00{ "name": "codeception/phpunit-wrapper", "description": "PHPUnit classes used by Codeception", "type": "library", "license": "MIT", "authors": [ { "name": "Davert", "email": "davert.php@resend.cc" }, { "name": "Naktibalda" } ], "require": { "php": ">=7.2", "phpunit/phpunit": "^9.0" }, "autoload": { "psr-4": { "Codeception\\PHPUnit\\": "src/" } }, "require-dev": { "vlucas/phpdotenv": "^3.0", "codeception/specify": "*", "consolidation/robo": "^3.0.0-alpha3" } } phpunit-wrapper/.htaccess000077700000000177151323602320011520 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>lib-asserts/src/Codeception/.htaccess000077700000000177151323602320013626 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>lib-asserts/src/Codeception/Util/Shared/Asserts.php000077700000005760151323602320016313 0ustar00<?php namespace Codeception\Util\Shared; use Codeception\PHPUnit\TestCase; use PHPUnit\Framework\Constraint\Constraint; use PHPUnit\Framework\Constraint\LogicalNot; trait Asserts { use InheritedAsserts; /** * @param $arguments * @param bool $not */ protected function assert($arguments, $not = false) { $not = $not ? 'Not' : ''; $method = ucfirst(array_shift($arguments)); if (($method === 'True') && $not) { $method = 'False'; $not = ''; } if (($method === 'False') && $not) { $method = 'True'; $not = ''; } call_user_func_array(['\PHPUnit\Framework\Assert', 'assert' . $not . $method], $arguments); } protected function assertNot($arguments) { $this->assert($arguments, true); } /** * Asserts that a file does not exist. * * @param string $filename * @param string $message */ protected function assertFileNotExists($filename, $message = '') { TestCase::assertFileNotExists($filename, $message); } /** * Asserts that a value is greater than or equal to another value. * * @param $expected * @param $actual * @param string $message */ protected function assertGreaterOrEquals($expected, $actual, $message = '') { TestCase::assertGreaterThanOrEqual($expected, $actual, $message); } /** * Asserts that a variable is empty. * * @param $actual * @param string $message */ protected function assertIsEmpty($actual, $message = '') { TestCase::assertEmpty($actual, $message); } /** * Asserts that a value is smaller than or equal to another value. * * @param $expected * @param $actual * @param string $message */ protected function assertLessOrEquals($expected, $actual, $message = '') { TestCase::assertLessThanOrEqual($expected, $actual, $message); } /** * Asserts that a string does not match a given regular expression. * * @param string $pattern * @param string $string * @param string $message */ protected function assertNotRegExp($pattern, $string, $message = '') { TestCase::assertNotRegExp($pattern, $string, $message); } /** * Asserts that a string matches a given regular expression. * * @param string $pattern * @param string $string * @param string $message */ protected function assertRegExp($pattern, $string, $message = '') { TestCase::assertRegExp($pattern, $string, $message); } /** * Evaluates a PHPUnit\Framework\Constraint matcher object. * * @param $value * @param Constraint $constraint * @param string $message */ protected function assertThatItsNot($value, $constraint, $message = '') { $constraint = new LogicalNot($constraint); TestCase::assertThat($value, $constraint, $message); } } lib-asserts/src/Codeception/Util/Shared/.htaccess000077700000000177151323602320015751 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>lib-asserts/src/Codeception/Util/Shared/InheritedAsserts.php000077700000116746151323602320020156 0ustar00<?php namespace Codeception\Util\Shared; use ArrayAccess; use Codeception\PHPUnit\TestCase; use Countable; use DOMDocument; use PHPUnit\Framework\Assert; use PHPUnit\Framework\Constraint\Constraint; trait InheritedAsserts { /** * Asserts that an array has a specified key. * * @param int|string $key * @param array|ArrayAccess $array * @param string $message */ protected function assertArrayHasKey($key, $array, $message = '') { Assert::assertArrayHasKey($key, $array, $message); } /** * Asserts that an array does not have a specified key. * * @param int|string $key * @param array|ArrayAccess $array * @param string $message */ protected function assertArrayNotHasKey($key, $array, $message = '') { Assert::assertArrayNotHasKey($key, $array, $message); } /** * Asserts that a class has a specified attribute. * * @param string $attributeName * @param string $className * @param string $message */ protected function assertClassHasAttribute($attributeName, $className, $message = '') { Assert::assertClassHasAttribute($attributeName, $className, $message); } /** * Asserts that a class has a specified static attribute. * * @param string $attributeName * @param string $className * @param string $message */ protected function assertClassHasStaticAttribute($attributeName, $className, $message = '') { Assert::assertClassHasStaticAttribute($attributeName, $className, $message); } /** * Asserts that a class does not have a specified attribute. * * @param string $attributeName * @param string $className * @param string $message */ protected function assertClassNotHasAttribute($attributeName, $className, $message = '') { Assert::assertClassNotHasAttribute($attributeName, $className, $message); } /** * Asserts that a class does not have a specified static attribute. * * @param string $attributeName * @param string $className * @param string $message */ protected function assertClassNotHasStaticAttribute($attributeName, $className, $message = '') { Assert::assertClassNotHasStaticAttribute($attributeName, $className, $message); } /** * Asserts that a haystack contains a needle. * * @param $needle * @param $haystack * @param string $message */ protected function assertContains($needle, $haystack, $message = '') { Assert::assertContains($needle, $haystack, $message); } /** * @param $needle * @param $haystack * @param string $message */ protected function assertContainsEquals($needle, $haystack, $message = '') { Assert::assertContainsEquals($needle, $haystack, $message); } /** * Asserts that a haystack contains only values of a given type. * * @param string $type * @param $haystack * @param bool|null $isNativeType * @param string $message */ protected function assertContainsOnly($type, $haystack, $isNativeType = null, $message = '') { Assert::assertContainsOnly($type, $haystack, $isNativeType, $message); } /** * Asserts that a haystack contains only instances of a given class name. * * @param string $className * @param $haystack * @param string $message */ protected function assertContainsOnlyInstancesOf($className, $haystack, $message = '') { Assert::assertContainsOnlyInstancesOf($className, $haystack, $message); } /** * Asserts the number of elements of an array, Countable or Traversable. * * @param int $expectedCount * @param Countable|iterable $haystack * @param string $message */ protected function assertCount($expectedCount, $haystack, $message = '') { Assert::assertCount($expectedCount, $haystack, $message); } /** * Asserts that a directory does not exist. * * @param string $directory * @param string $message */ protected function assertDirectoryDoesNotExist($directory, $message = '') { Assert::assertDirectoryDoesNotExist($directory, $message); } /** * Asserts that a directory exists. * * @param string $directory * @param string $message */ protected function assertDirectoryExists($directory, $message = '') { Assert::assertDirectoryExists($directory, $message); } /** * Asserts that a directory exists and is not readable. * * @param string $directory * @param string $message */ protected function assertDirectoryIsNotReadable($directory, $message = '') { Assert::assertDirectoryIsNotReadable($directory, $message); } /** * Asserts that a directory exists and is not writable. * * @param string $directory * @param string $message */ protected function assertDirectoryIsNotWritable($directory, $message = '') { Assert::assertDirectoryIsNotWritable($directory, $message); } /** * Asserts that a directory exists and is readable. * * @param string $directory * @param string $message */ protected function assertDirectoryIsReadable($directory, $message = '') { Assert::assertDirectoryIsReadable($directory, $message); } /** * Asserts that a directory exists and is writable. * * @param string $directory * @param string $message */ protected function assertDirectoryIsWritable($directory, $message = '') { Assert::assertDirectoryIsWritable($directory, $message); } /** * Asserts that a string does not match a given regular expression. * * @param string $pattern * @param string $string * @param string $message */ protected function assertDoesNotMatchRegularExpression($pattern, $string, $message = '') { TestCase::assertNotRegExp($pattern, $string, $message); } /** * Asserts that a variable is empty. * * @param $actual * @param string $message */ protected function assertEmpty($actual, $message = '') { Assert::assertEmpty($actual, $message); } /** * Asserts that two variables are equal. * * @param $expected * @param $actual * @param string $message */ protected function assertEquals($expected, $actual, $message = '') { Assert::assertEquals($expected, $actual, $message); } /** * Asserts that two variables are equal (canonicalizing). * * @param $expected * @param $actual * @param string $message */ protected function assertEqualsCanonicalizing($expected, $actual, $message = '') { TestCase::assertEqualsCanonicalizing($expected, $actual, $message); } /** * Asserts that two variables are equal (ignoring case). * * @param $expected * @param $actual * @param string $message */ protected function assertEqualsIgnoringCase($expected, $actual, $message = '') { TestCase::assertEqualsIgnoringCase($expected, $actual, $message); } /** * Asserts that two variables are equal (with delta). * * @param $expected * @param $actual * @param float $delta * @param string $message */ protected function assertEqualsWithDelta($expected, $actual, $delta, $message = '') { TestCase::assertEqualsWithDelta($expected, $actual, $delta, $message); } /** * Asserts that a condition is false. * * @param $condition * @param string $message */ protected function assertFalse($condition, $message = '') { Assert::assertFalse($condition, $message); } /** * Asserts that a file does not exist. * * @param string $filename * @param string $message */ protected function assertFileDoesNotExist($filename, $message = '') { TestCase::assertFileNotExists($filename, $message); } /** * Asserts that the contents of one file is equal to the contents of another file. * * @param string $expected * @param string $actual * @param string $message */ protected function assertFileEquals($expected, $actual, $message = '') { Assert::assertFileEquals($expected, $actual, $message); } /** * Asserts that the contents of one file is equal to the contents of another file (canonicalizing). * * @param $expected * @param $actual * @param string $message */ protected function assertFileEqualsCanonicalizing($expected, $actual, $message = '') { Assert::assertFileEqualsCanonicalizing($expected, $actual, $message); } /** * Asserts that the contents of one file is equal to the contents of another file (ignoring case). * * @param $expected * @param $actual * @param string $message */ protected function assertFileEqualsIgnoringCase($expected, $actual, $message = '') { Assert::assertFileEqualsIgnoringCase($expected, $actual, $message); } /** * Asserts that a file exists. * * @param string $filename * @param string $message */ protected function assertFileExists($filename, $message = '') { Assert::assertFileExists($filename, $message); } /** * Asserts that a file exists and is not readable. * * @param string $file * @param string $message */ protected function assertFileIsNotReadable($file, $message = '') { Assert::assertFileIsNotReadable($file, $message); } /** * Asserts that a file exists and is not writable. * * @param string $file * @param string $message */ protected function assertFileIsNotWritable($file, $message = '') { Assert::assertFileIsNotWritable($file, $message); } /** * Asserts that a file exists and is readable. * * @param string $file * @param string $message */ protected function assertFileIsReadable($file, $message = '') { Assert::assertFileIsReadable($file, $message); } /** * Asserts that a file exists and is writable. * * @param string $file * @param string $message */ protected function assertFileIsWritable($file, $message = '') { Assert::assertFileIsWritable($file, $message); } /** * Asserts that the contents of one file is not equal to the contents of another file. * * @param $expected * @param $actual * @param string $message */ protected function assertFileNotEquals($expected, $actual, $message = '') { Assert::assertFileNotEquals($expected, $actual, $message); } /** * Asserts that the contents of one file is not equal to the contents of another file (canonicalizing). * * @param $expected * @param $actual * @param string $message */ protected function assertFileNotEqualsCanonicalizing($expected, $actual, $message = '') { Assert::assertFileNotEqualsCanonicalizing($expected, $actual, $message); } /** * Asserts that the contents of one file is not equal to the contents of another file (ignoring case). * * @param $expected * @param $actual * @param string $message */ protected function assertFileNotEqualsIgnoringCase($expected, $actual, $message = '') { Assert::assertFileNotEqualsIgnoringCase($expected, $actual, $message); } /** * Asserts that a variable is finite. * * @param $actual * @param string $message */ protected function assertFinite($actual, $message = '') { Assert::assertFinite($actual, $message); } /** * Asserts that a value is greater than another value. * * @param $expected * @param $actual * @param string $message */ protected function assertGreaterThan($expected, $actual, $message = '') { Assert::assertGreaterThan($expected, $actual, $message); } /** * Asserts that a value is greater than or equal to another value. * * @param $expected * @param $actual * @param string $message */ protected function assertGreaterThanOrEqual($expected, $actual, $message = '') { Assert::assertGreaterThanOrEqual($expected, $actual, $message); } /** * Asserts that a variable is infinite. * * @param $actual * @param string $message */ protected function assertInfinite($actual, $message = '') { Assert::assertInfinite($actual, $message); } /** * Asserts that a variable is of a given type. * * @param $expected * @param $actual * @param string $message */ protected function assertInstanceOf($expected, $actual, $message = '') { Assert::assertInstanceOf($expected, $actual, $message); } /** * Asserts that a variable is of type array. * * @param $actual * @param string $message */ protected function assertIsArray($actual, $message = '') { TestCase::assertIsArray($actual, $message); } /** * Asserts that a variable is of type bool. * * @param $actual * @param string $message */ protected function assertIsBool($actual, $message = '') { TestCase::assertIsBool($actual, $message); } /** * Asserts that a variable is of type callable. * * @param $actual * @param string $message */ protected function assertIsCallable($actual, $message = '') { TestCase::assertIsCallable($actual, $message); } /** * Asserts that a variable is of type resource and is closed. * * @param $actual * @param string $message */ protected function assertIsClosedResource($actual, $message = '') { TestCase::assertIsClosedResource($actual, $message); } /** * Asserts that a variable is of type float. * * @param $actual * @param string $message */ protected function assertIsFloat($actual, $message = '') { TestCase::assertIsFloat($actual, $message); } /** * Asserts that a variable is of type int. * * @param $actual * @param string $message */ protected function assertIsInt($actual, $message = '') { TestCase::assertIsInt($actual, $message); } /** * Asserts that a variable is of type iterable. * * @param $actual * @param string $message */ protected function assertIsIterable($actual, $message = '') { TestCase::assertIsIterable($actual, $message); } /** * Asserts that a variable is not of type array. * * @param $actual * @param string $message */ protected function assertIsNotArray($actual, $message = '') { TestCase::assertIsNotArray($actual, $message); } /** * Asserts that a variable is not of type bool. * * @param $actual * @param string $message */ protected function assertIsNotBool($actual, $message = '') { TestCase::assertIsNotBool($actual, $message); } /** * Asserts that a variable is not of type callable. * * @param $actual * @param string $message */ protected function assertIsNotCallable($actual, $message = '') { TestCase::assertIsNotCallable($actual, $message); } /** * Asserts that a variable is not of type resource. * * @param $actual * @param string $message */ protected function assertIsNotClosedResource($actual, $message = '') { TestCase::assertIsNotClosedResource($actual, $message); } /** * Asserts that a variable is not of type float. * * @param $actual * @param string $message */ protected function assertIsNotFloat($actual, $message = '') { TestCase::assertIsNotFloat($actual, $message); } /** * Asserts that a variable is not of type int. * * @param $actual * @param string $message */ protected function assertIsNotInt($actual, $message = '') { TestCase::assertIsNotInt($actual, $message); } /** * Asserts that a variable is not of type iterable. * * @param $actual * @param string $message */ protected function assertIsNotIterable($actual, $message = '') { TestCase::assertIsNotIterable($actual, $message); } /** * Asserts that a variable is not of type numeric. * * @param $actual * @param string $message */ protected function assertIsNotNumeric($actual, $message = '') { TestCase::assertIsNotNumeric($actual, $message); } /** * Asserts that a variable is not of type object. * * @param $actual * @param string $message */ protected function assertIsNotObject($actual, $message = '') { TestCase::assertIsNotObject($actual, $message); } /** * Asserts that a file/dir exists and is not readable. * * @param string $filename * @param string $message */ protected function assertIsNotReadable($filename, $message = '') { TestCase::assertIsNotReadable($filename, $message); } /** * Asserts that a variable is not of type resource. * * @param $actual * @param string $message */ protected function assertIsNotResource($actual, $message = '') { TestCase::assertIsNotResource($actual, $message); } /** * Asserts that a variable is not of type scalar. * * @param $actual * @param string $message */ protected function assertIsNotScalar($actual, $message = '') { TestCase::assertIsNotScalar($actual, $message); } /** * Asserts that a variable is not of type string. * * @param $actual * @param string $message */ protected function assertIsNotString($actual, $message = '') { TestCase::assertIsNotString($actual, $message); } /** * Asserts that a file/dir exists and is not writable. * * @param $filename * @param string $message */ protected function assertIsNotWritable($filename, $message = '') { TestCase::assertIsNotWritable($filename, $message); } /** * Asserts that a variable is of type numeric. * * @param $actual * @param string $message */ protected function assertIsNumeric($actual, $message = '') { TestCase::assertIsNumeric($actual, $message); } /** * Asserts that a variable is of type object. * * @param $actual * @param string $message */ protected function assertIsObject($actual, $message = '') { TestCase::assertIsObject($actual, $message); } /** * Asserts that a file/dir is readable. * * @param $filename * @param string $message */ protected function assertIsReadable($filename, $message = '') { TestCase::assertIsReadable($filename, $message); } /** * Asserts that a variable is of type resource. * * @param $actual * @param string $message */ protected function assertIsResource($actual, $message = '') { TestCase::assertIsResource($actual, $message); } /** * Asserts that a variable is of type scalar. * * @param $actual * @param string $message */ protected function assertIsScalar($actual, $message = '') { TestCase::assertIsScalar($actual, $message); } /** * Asserts that a variable is of type string. * * @param $actual * @param string $message */ protected function assertIsString($actual, $message = '') { TestCase::assertIsString($actual, $message); } /** * Asserts that a file/dir exists and is writable. * * @param $filename * @param string $message */ protected function assertIsWritable($filename, $message = '') { TestCase::assertIsWritable($filename, $message); } /** * Asserts that a string is a valid JSON string. * * @param string $actualJson * @param string $message */ protected function assertJson($actualJson, $message = '') { Assert::assertJson($actualJson, $message); } /** * Asserts that two JSON files are equal. * * @param string $expectedFile * @param string $actualFile * @param string $message */ protected function assertJsonFileEqualsJsonFile($expectedFile, $actualFile, $message = '') { Assert::assertJsonFileEqualsJsonFile($expectedFile, $actualFile, $message); } /** * Asserts that two JSON files are not equal. * * @param string $expectedFile * @param string $actualFile * @param string $message */ protected function assertJsonFileNotEqualsJsonFile($expectedFile, $actualFile, $message = '') { Assert::assertJsonFileNotEqualsJsonFile($expectedFile, $actualFile, $message); } /** * Asserts that the generated JSON encoded object and the content of the given file are equal. * * @param string $expectedFile * @param string $actualJson * @param string $message */ protected function assertJsonStringEqualsJsonFile($expectedFile, $actualJson, $message = '') { Assert::assertJsonStringEqualsJsonFile($expectedFile, $actualJson, $message); } /** * Asserts that two given JSON encoded objects or arrays are equal. * * @param string $expectedJson * @param string $actualJson * @param string $message */ protected function assertJsonStringEqualsJsonString($expectedJson, $actualJson, $message = '') { Assert::assertJsonStringEqualsJsonString($expectedJson, $actualJson, $message); } /** * Asserts that the generated JSON encoded object and the content of the given file are not equal. * * @param string $expectedFile * @param string $actualJson * @param string $message */ protected function assertJsonStringNotEqualsJsonFile($expectedFile, $actualJson, $message = '') { Assert::assertJsonStringNotEqualsJsonFile($expectedFile, $actualJson, $message); } /** * Asserts that two given JSON encoded objects or arrays are not equal. * * @param string $expectedJson * @param string $actualJson * @param string $message */ protected function assertJsonStringNotEqualsJsonString($expectedJson, $actualJson, $message = '') { Assert::assertJsonStringNotEqualsJsonString($expectedJson, $actualJson, $message); } /** * Asserts that a value is smaller than another value. * * @param $expected * @param $actual * @param string $message */ protected function assertLessThan($expected, $actual, $message = '') { Assert::assertLessThan($expected, $actual, $message); } /** * Asserts that a value is smaller than or equal to another value. * * @param $expected * @param $actual * @param string $message */ protected function assertLessThanOrEqual($expected, $actual, $message = '') { Assert::assertLessThanOrEqual($expected, $actual, $message); } /** * Asserts that a string matches a given regular expression. * * @param string $pattern * @param string $string * @param string $message */ protected function assertMatchesRegularExpression($pattern, $string, $message = '') { TestCase::assertRegExp($pattern, $string, $message); } /** * Asserts that a variable is nan. * * @param $actual * @param string $message */ protected function assertNan($actual, $message = '') { Assert::assertNan($actual, $message); } /** * Asserts that a haystack does not contain a needle. * * @param $needle * @param $haystack * @param string $message */ protected function assertNotContains($needle, $haystack, $message = '') { Assert::assertNotContains($needle, $haystack, $message); } protected function assertNotContainsEquals($needle, $haystack, $message = '') { Assert::assertNotContainsEquals($needle, $haystack, $message); } /** * Asserts that a haystack does not contain only values of a given type. * * @param string $type * @param $haystack * @param bool|null $isNativeType * @param string $message */ protected function assertNotContainsOnly($type, $haystack, $isNativeType = null, $message = '') { Assert::assertNotContainsOnly($type, $haystack, $isNativeType, $message); } /** * Asserts the number of elements of an array, Countable or Traversable. * * @param int $expectedCount * @param Countable|iterable $haystack * @param string $message */ protected function assertNotCount($expectedCount, $haystack, $message = '') { Assert::assertNotCount($expectedCount, $haystack, $message); } /** * Asserts that a variable is not empty. * * @param $actual * @param string $message */ protected function assertNotEmpty($actual, $message = '') { Assert::assertNotEmpty($actual, $message); } /** * Asserts that two variables are not equal. * * @param $expected * @param $actual * @param string $message */ protected function assertNotEquals($expected, $actual, $message = '') { TestCase::assertNotEquals($expected, $actual, $message); } /** * Asserts that two variables are not equal (canonicalizing). * * @param $expected * @param $actual * @param string $message */ protected function assertNotEqualsCanonicalizing($expected, $actual, $message = '') { TestCase::assertNotEqualsCanonicalizing($expected, $actual, $message); } /** * Asserts that two variables are not equal (ignoring case). * * @param $expected * @param $actual * @param string $message */ protected function assertNotEqualsIgnoringCase($expected, $actual, $message = '') { TestCase::assertNotEqualsIgnoringCase($expected, $actual, $message); } /** * Asserts that two variables are not equal (with delta). * * @param $expected * @param $actual * @param float $delta * @param string $message */ protected function assertNotEqualsWithDelta($expected, $actual, $delta, $message = '') { TestCase::assertNotEqualsWithDelta($expected, $actual, $delta, $message); } /** * Asserts that a condition is not false. * * @param $condition * @param string $message */ protected function assertNotFalse($condition, $message = '') { Assert::assertNotFalse($condition, $message); } /** * Asserts that a variable is not of a given type. * * @param $expected * @param $actual * @param string $message */ protected function assertNotInstanceOf($expected, $actual, $message = '') { Assert::assertNotInstanceOf($expected, $actual, $message); } /** * Asserts that a variable is not null. * * @param $actual * @param string $message */ protected function assertNotNull($actual, $message = '') { Assert::assertNotNull($actual, $message); } /** * Asserts that two variables do not have the same type and value. * * @param $expected * @param $actual * @param string $message */ protected function assertNotSame($expected, $actual, $message = '') { Assert::assertNotSame($expected, $actual, $message); } /** * Assert that the size of two arrays (or `Countable` or `Traversable` objects) is not the same. * * @param Countable|iterable $expected * @param Countable|iterable $actual * @param string $message */ protected function assertNotSameSize($expected, $actual, $message = '') { Assert::assertNotSameSize($expected, $actual, $message); } /** * Asserts that a condition is not true. * * @param $condition * @param string $message */ protected function assertNotTrue($condition, $message = '') { Assert::assertNotTrue($condition, $message); } /** * Asserts that a variable is null. * * @param $actual * @param string $message */ protected function assertNull($actual, $message = '') { Assert::assertNull($actual, $message); } /** * Asserts that an object has a specified attribute. * * @param string $attributeName * @param object $object * @param string $message */ protected function assertObjectHasAttribute($attributeName, $object, $message = '') { Assert::assertObjectHasAttribute($attributeName, $object, $message); } /** * Asserts that an object does not have a specified attribute. * * @param string $attributeName * @param object $object * @param string $message */ protected function assertObjectNotHasAttribute($attributeName, $object, $message = '') { Assert::assertObjectNotHasAttribute($attributeName, $object, $message); } /** * Asserts that two variables have the same type and value. * * @param $expected * @param $actual * @param string $message */ protected function assertSame($expected, $actual, $message = '') { Assert::assertSame($expected, $actual, $message); } /** * Assert that the size of two arrays (or `Countable` or `Traversable` objects) is the same. * * @param Countable|iterable $expected * @param Countable|iterable $actual * @param string $message */ protected function assertSameSize($expected, $actual, $message = '') { Assert::assertSameSize($expected, $actual, $message); } /** * @param string $needle * @param string $haystack * @param string $message */ protected function assertStringContainsString($needle, $haystack, $message = '') { TestCase::assertStringContainsString($needle, $haystack, $message); } protected function assertStringContainsStringIgnoringCase($needle, $haystack, $message = '') { TestCase::assertStringContainsStringIgnoringCase($needle, $haystack, $message); } /** * Asserts that a string ends not with a given suffix. * * @param string $suffix * @param string $string * @param string $message */ protected function assertStringEndsNotWith($suffix, $string, $message = '') { TestCase::assertStringEndsNotWith($suffix, $string, $message); } /** * Asserts that a string ends with a given suffix. * * @param string $suffix * @param string $string * @param string $message */ protected function assertStringEndsWith($suffix, $string, $message = '') { TestCase::assertStringEndsWith($suffix, $string, $message); } /** * Asserts that the contents of a string is equal to the contents of a file. * * @param string $expectedFile * @param string $actualString * @param string $message */ protected function assertStringEqualsFile($expectedFile, $actualString, $message = '') { Assert::assertStringEqualsFile($expectedFile, $actualString, $message); } /** * Asserts that the contents of a string is equal to the contents of a file (canonicalizing). * * @param string $expectedFile * @param string $actualString * @param string $message */ protected function assertStringEqualsFileCanonicalizing($expectedFile, $actualString, $message = '') { Assert::assertStringEqualsFileCanonicalizing($expectedFile, $actualString, $message); } /** * Asserts that the contents of a string is equal to the contents of a file (ignoring case). * * @param string $expectedFile * @param string $actualString * @param string $message */ protected function assertStringEqualsFileIgnoringCase($expectedFile, $actualString, $message = '') { Assert::assertStringEqualsFileIgnoringCase($expectedFile, $actualString, $message); } /** * Asserts that a string matches a given format string. * * @param string $format * @param string $string * @param string $message */ protected function assertStringMatchesFormat($format, $string, $message = '') { Assert::assertStringMatchesFormat($format, $string, $message); } /** * Asserts that a string matches a given format file. * * @param string $formatFile * @param string $string * @param string $message */ protected function assertStringMatchesFormatFile($formatFile, $string, $message = '') { Assert::assertStringMatchesFormatFile($formatFile, $string, $message); } /** * @param string $needle * @param string $haystack * @param string $message */ protected function assertStringNotContainsString($needle, $haystack, $message = '') { TestCase::assertStringNotContainsString($needle, $haystack, $message); } /** * @param string $needle * @param string $haystack * @param string $message */ protected function assertStringNotContainsStringIgnoringCase($needle, $haystack, $message = '') { TestCase::assertStringNotContainsStringIgnoringCase($needle, $haystack, $message); } /** * Asserts that the contents of a string is not equal to the contents of a file. * * @param string $expectedFile * @param string $actualString * @param string $message */ protected function assertStringNotEqualsFile($expectedFile, $actualString, $message = '') { Assert::assertStringNotEqualsFile($expectedFile, $actualString, $message); } /** * Asserts that the contents of a string is not equal to the contents of a file (canonicalizing). * @param string $expectedFile * @param string $actualString * @param string $message */ protected function assertStringNotEqualsFileCanonicalizing($expectedFile, $actualString, $message = '') { Assert::assertStringNotEqualsFileCanonicalizing($expectedFile, $actualString, $message); } /** * Asserts that the contents of a string is not equal to the contents of a file (ignoring case). * * @param string $expectedFile * @param string $actualString * @param string $message */ protected function assertStringNotEqualsFileIgnoringCase($expectedFile, $actualString, $message = '') { Assert::assertStringNotEqualsFileIgnoringCase($expectedFile, $actualString, $message); } /** * Asserts that a string does not match a given format string. * * @param string $format * @param string $string * @param string $message */ protected function assertStringNotMatchesFormat($format, $string, $message = '') { Assert::assertStringNotMatchesFormat($format, $string, $message); } /** * Asserts that a string does not match a given format string. * * @param string $formatFile * @param string $string * @param string $message */ protected function assertStringNotMatchesFormatFile($formatFile, $string, $message = '') { Assert::assertStringNotMatchesFormatFile($formatFile, $string, $message); } /** * Asserts that a string starts not with a given prefix. * * @param string $prefix * @param string $string * @param string $message */ protected function assertStringStartsNotWith($prefix, $string, $message = '') { Assert::assertStringStartsNotWith($prefix, $string, $message); } /** * Asserts that a string starts with a given prefix. * * @param string $prefix * @param string $string * @param string $message */ protected function assertStringStartsWith($prefix, $string, $message = '') { Assert::assertStringStartsWith($prefix, $string, $message); } /** * Evaluates a PHPUnit\Framework\Constraint matcher object. * * @param $value * @param Constraint $constraint * @param string $message */ protected function assertThat($value, $constraint, $message = '') { Assert::assertThat($value, $constraint, $message); } /** * Asserts that a condition is true. * * @param $condition * @param string $message */ protected function assertTrue($condition, $message = '') { Assert::assertTrue($condition, $message); } /** * Asserts that two XML files are equal. * * @param string $expectedFile * @param string $actualFile * @param string $message */ protected function assertXmlFileEqualsXmlFile($expectedFile, $actualFile, $message = '') { Assert::assertXmlFileEqualsXmlFile($expectedFile, $actualFile, $message); } /** * Asserts that two XML files are not equal. * * @param string $expectedFile * @param string $actualFile * @param string $message */ protected function assertXmlFileNotEqualsXmlFile($expectedFile, $actualFile, $message = '') { Assert::assertXmlFileNotEqualsXmlFile($expectedFile, $actualFile, $message); } /** * Asserts that two XML documents are equal. * * @param string $expectedFile * @param DOMDocument|string $actualXml * @param string $message */ protected function assertXmlStringEqualsXmlFile($expectedFile, $actualXml, $message = '') { Assert::assertXmlStringEqualsXmlFile($expectedFile, $actualXml, $message); } /** * Asserts that two XML documents are equal. * * @param DOMDocument|string $expectedXml * @param DOMDocument|string $actualXml * @param string $message */ protected function assertXmlStringEqualsXmlString($expectedXml, $actualXml, $message = '') { Assert::assertXmlStringEqualsXmlString($expectedXml, $actualXml, $message); } /** * Asserts that two XML documents are not equal. * * @param string $expectedFile * @param DOMDocument|string $actualXml * @param string $message */ protected function assertXmlStringNotEqualsXmlFile($expectedFile, $actualXml, $message = '') { Assert::assertXmlStringNotEqualsXmlFile($expectedFile, $actualXml, $message); } /** * Asserts that two XML documents are not equal. * * @param DOMDocument|string $expectedXml * @param DOMDocument|string $actualXml * @param string $message */ protected function assertXmlStringNotEqualsXmlString($expectedXml, $actualXml, $message = '') { Assert::assertXmlStringNotEqualsXmlString($expectedXml, $actualXml, $message); } /** * Fails a test with the given message. * * @param string $message */ protected function fail($message = '') { Assert::fail($message); } /** * Mark the test as incomplete. * * @param string $message */ protected function markTestIncomplete($message = '') { Assert::markTestIncomplete($message); } /** * Mark the test as skipped. * * @param string $message */ protected function markTestSkipped($message = '') { Assert::markTestSkipped($message); } }lib-asserts/src/Codeception/Util/.htaccess000077700000000177151323602320014543 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>lib-asserts/src/.htaccess000077700000000177151323602320011372 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>lib-asserts/LICENSE000077700000002116151323602320010005 0ustar00The MIT License (MIT) Copyright (c) 2011 Michael Bodnarchuk and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. lib-asserts/composer.json000077700000001602151323602320011521 0ustar00{ "name":"codeception/lib-asserts", "description":"Assertion methods used by Codeception core and Asserts module", "keywords":["codeception"], "homepage":"https://codeception.com/", "type":"library", "license":"MIT", "authors":[ { "name":"Michael Bodnarchuk", "email":"davert@mail.ua", "homepage":"http://codegyre.com" }, { "name":"Gintautas Miselis" }, { "name":"Gustavo Nieves", "homepage": "https://medium.com/@ganieves" } ], "minimum-stability": "RC", "require": { "php": ">=5.6.0 <9.0", "ext-dom": "*", "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.0.3 | ^9.0" }, "autoload":{ "classmap": ["src/"] }, "config": { "classmap-authoritative": true } } lib-asserts/docs/supported_asserts.md000077700000005777151323602320014063 0ustar00# List of supported asserts ## Codeception ``` assertFileNotExists assertGreaterOrEquals assertIsEmpty assertLessOrEquals assertNotRegExp assertRegExp assertThatItsNot ``` ## PHPUnit ``` assertArrayHasKey assertArrayNotHasKey assertClassHasAttribute assertClassHasStaticAttribute assertClassNotHasAttribute assertClassNotHasStaticAttribute assertContains assertContainsEquals assertContainsOnly assertContainsOnlyInstancesOf assertCount assertDirectoryDoesNotExist assertDirectoryExists assertDirectoryIsNotReadable assertDirectoryIsNotWritable assertDirectoryIsReadable assertDirectoryIsWritable assertDoesNotMatchRegularExpression assertEmpty assertEquals assertEqualsCanonicalizing assertEqualsIgnoringCase assertEqualsWithDelta assertFalse assertFileDoesNotExist assertFileEquals assertFileEqualsCanonicalizing assertFileEqualsIgnoringCase assertFileExists assertFileIsNotReadable assertFileIsNotWritable assertFileIsReadable assertFileIsWritable assertFileNotEquals assertFileNotEqualsCanonicalizing assertFileNotEqualsIgnoringCase assertFinite assertGreaterThan assertGreaterThanOrEqual assertInfinite assertInstanceOf assertIsArray assertIsBool assertIsCallable assertIsClosedResource assertIsFloat assertIsInt assertIsIterable assertIsNotArray assertIsNotBool assertIsNotCallable assertIsNotClosedResource assertIsNotFloat assertIsNotInt assertIsNotIterable assertIsNotNumeric assertIsNotObject assertIsNotReadable assertIsNotResource assertIsNotScalar assertIsNotString assertIsNotWritable assertIsNumeric assertIsObject assertIsReadable assertIsResource assertIsScalar assertIsString assertIsWritable assertJson assertJsonFileEqualsJsonFile assertJsonFileNotEqualsJsonFile assertJsonStringEqualsJsonFile assertJsonStringEqualsJsonString assertJsonStringNotEqualsJsonFile assertJsonStringNotEqualsJsonString assertLessThan assertLessThanOrEqual assertMatchesRegularExpression assertNan assertNotContains assertNotContainsEquals assertNotContainsOnly assertNotCount assertNotEmpty assertNotEquals assertNotEqualsCanonicalizing assertNotEqualsIgnoringCase assertNotEqualsWithDelta assertNotFalse assertNotInstanceOf assertNotNull assertNotSame assertNotSameSize assertNotTrue assertNull assertObjectHasAttribute assertObjectNotHasAttribute assertSame assertSameSize assertStringContainsString assertStringContainsStringIgnoringCase assertStringEndsNotWith assertStringEndsWith assertStringEqualsFile assertStringEqualsFileCanonicalizing assertStringEqualsFileIgnoringCase assertStringMatchesFormat assertStringMatchesFormatFile assertStringNotContainsString assertStringNotContainsStringIgnoringCase assertStringNotEqualsFile assertStringNotEqualsFileCanonicalizing assertStringNotEqualsFileIgnoringCase assertStringNotMatchesFormat assertStringNotMatchesFormatFile assertStringStartsNotWith assertStringStartsWith assertThat assertTrue assertXmlFileEqualsXmlFile assertXmlFileNotEqualsXmlFile assertXmlStringEqualsXmlFile assertXmlStringEqualsXmlString assertXmlStringNotEqualsXmlFile assertXmlStringNotEqualsXmlString fail markTestIncomplete markTestSkipped ```lib-asserts/docs/.htaccess000077700000000177151323602320011533 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>lib-asserts/.htaccess000077700000000177151323602320010603 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>module-sequence/src/Codeception/.htaccess000077700000000177151323602320014471 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>module-sequence/src/Codeception/Util/sq.php000077700000001464151323602320014744 0ustar00<?php use Codeception\Module\Sequence; if (!function_exists('sq')) { function sq($id = null) { if ($id and isset(Sequence::$hash[$id])) { return Sequence::$hash[$id]; } $prefix = str_replace('{id}', $id, Sequence::$prefix); $sequence = $prefix . uniqid($id); if ($id) { Sequence::$hash[$id] = $sequence; } return $sequence; } } if (!function_exists('sqs')) { function sqs($id = null) { if ($id and isset(Sequence::$suiteHash[$id])) { return Sequence::$suiteHash[$id]; } $prefix = str_replace('{id}', $id, Sequence::$prefix); $sequence = $prefix . uniqid($id); if ($id) { Sequence::$suiteHash[$id] = $sequence; } return $sequence; } } module-sequence/src/Codeception/Util/.htaccess000077700000000177151323602320015406 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>module-sequence/src/Codeception/Module/Sequence.php000077700000005636151323602320016406 0ustar00<?php namespace Codeception\Module; use Codeception\Module as CodeceptionModule; use Codeception\Exception\ModuleException; use Codeception\TestInterface; /** * Sequence solves data cleanup issue in alternative way. * Instead cleaning up the database between tests, * you can use generated unique names, that should not conflict. * When you create article on a site, for instance, you can assign it a unique name and then check it. * * This module has no actions, but introduces a function `sq` for generating unique sequences within test and * `sqs` for generating unique sequences across suite. * * ### Usage * * Function `sq` generates sequence, the only parameter it takes, is id. * You can get back to previously generated sequence using that id: * * ``` php * <?php * sq('post1'); // post1_521fbc63021eb * sq('post2'); // post2_521fbc6302266 * sq('post1'); // post1_521fbc63021eb * ``` * * Example: * * ``` php * <?php * $I->wantTo('create article'); * $I->click('New Article'); * $I->fillField('Title', sq('Article')); * $I->fillField('Body', 'Demo article with Lorem Ipsum'); * $I->click('save'); * $I->see(sq('Article') ,'#articles') * ``` * * Populating Database: * * ``` php * <?php * * for ($i = 0; $i<10; $i++) { * $I->haveInDatabase('users', array('login' => sq("user$i"), 'email' => sq("user$i").'@email.com'); * } * ?> * ``` * * Cest Suite tests: * * ``` php * <?php * class UserTest * { * public function createUser(AcceptanceTester $I) * { * $I->createUser(sqs('user') . '@mailserver.com', sqs('login'), sqs('pwd')); * } * * public function checkEmail(AcceptanceTester $I) * { * $I->seeInEmailTo(sqs('user') . '@mailserver.com', sqs('login')); * } * * public function removeUser(AcceptanceTester $I) * { * $I->removeUser(sqs('user') . '@mailserver.com'); * } * } * ?> * ``` * * ### Config * * By default produces unique string with param as a prefix: * * ``` * sq('user') => 'user_876asd8as87a' * ``` * * This behavior can be configured using `prefix` config param. * * Old style sequences: * * ```yaml * Sequence: * prefix: '_' * ``` * * Using id param inside prefix: * * ```yaml * Sequence: * prefix: '{id}.' * ``` */ class Sequence extends CodeceptionModule { public static $hash = []; public static $suiteHash = []; public static $prefix = ''; protected $config = ['prefix' => '{id}_']; public function _initialize() { static::$prefix = $this->config['prefix']; } public function _after(TestInterface $t) { self::$hash = []; } public function _afterSuite() { self::$suiteHash = []; } } if (!function_exists('sq') && !function_exists('sqs')) { require_once __DIR__ . '/../Util/sq.php'; } else { throw new ModuleException('Codeception\Module\Sequence', "function 'sq' and 'sqs' already defined"); } module-sequence/src/Codeception/Module/.htaccess000077700000000177151323602320015716 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>module-sequence/src/.htaccess000077700000000177151323602320012235 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>module-sequence/LICENSE000077700000002116151323602320010650 0ustar00The MIT License (MIT) Copyright (c) 2011 Michael Bodnarchuk and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. module-sequence/.github/workflows/main.yml000077700000001167151323602320014714 0ustar00name: CI on: [push, pull_request] jobs: tests: runs-on: ubuntu-latest strategy: matrix: php: [5.6, 7.0, 7.1, 7.2, 7.3, 7.4, 8.0] steps: - name: Checkout code uses: actions/checkout@v2 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} coverage: none - name: Validate composer.json and composer.lock run: composer validate - name: Install dependencies run: composer install --prefer-dist --no-progress --no-interaction --no-suggest - name: Run test suite run: php vendor/bin/codecept run module-sequence/.github/workflows/.htaccess000077700000000177151323602320015043 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>module-sequence/.github/.htaccess000077700000000177151323602320013006 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>module-sequence/composer.json000077700000001035151323602320012364 0ustar00{ "name":"codeception/module-sequence", "description":"Sequence module for Codeception", "keywords":["codeception"], "homepage":"http://codeception.com/", "type":"library", "license":"MIT", "authors":[ { "name":"Michael Bodnarchuk" } ], "minimum-stability": "RC", "require": { "php": ">=5.6.0 <9.0", "codeception/codeception": "^4.0" }, "autoload":{ "classmap": ["src/"] }, "config": { "classmap-authoritative": true } } module-sequence/.htaccess000077700000000177151323602320011446 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>module-webdriver/src/Codeception/Lib/Interfaces/ScreenshotSaver.php000077700000000550151323602320021467 0ustar00<?php namespace Codeception\Lib\Interfaces; interface ScreenshotSaver { /** * Saves screenshot of current page to a file * * ```php * $this->getModule('{{MODULE_NAME}}')->_saveScreenshot(codecept_output_dir().'screenshot_1.png'); * ``` * @api * @param $filename */ public function _saveScreenshot($filename); } module-webdriver/src/Codeception/Lib/Interfaces/.htaccess000077700000000177151323602320017443 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>module-webdriver/src/Codeception/Lib/Interfaces/SessionSnapshot.php000077700000002774151323602320021526 0ustar00<?php namespace Codeception\Lib\Interfaces; interface SessionSnapshot { /** * Saves current cookies into named snapshot in order to restore them in other tests * This is useful to save session state between tests. * For example, if user needs log in to site for each test this scenario can be executed once * while other tests can just restore saved cookies. * * ``` php * <?php * // inside AcceptanceTester class: * * public function login() * { * // if snapshot exists - skipping login * if ($I->loadSessionSnapshot('login')) return; * * // logging in * $I->amOnPage('/login'); * $I->fillField('name', 'jon'); * $I->fillField('password', '123345'); * $I->click('Login'); * * // saving snapshot * $I->saveSessionSnapshot('login'); * } * ?> * ``` * * @param $name * @return mixed */ public function saveSessionSnapshot($name); /** * Loads cookies from a saved snapshot. * Allows to reuse same session across tests without additional login. * * See [saveSessionSnapshot](#saveSessionSnapshot) * * @param $name * @return mixed */ public function loadSessionSnapshot($name); /** * Deletes session snapshot. * * See [saveSessionSnapshot](#saveSessionSnapshot) * * @param $name * @return mixed */ public function deleteSessionSnapshot($name); } module-webdriver/src/Codeception/Lib/.htaccess000077700000000177151323602320015360 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>module-webdriver/src/Codeception/Exception/ConnectionException.php000077700000000140151323602320021467 0ustar00<?php namespace Codeception\Exception; class ConnectionException extends \RuntimeException { } module-webdriver/src/Codeception/Exception/.htaccess000077700000000177151323602320016610 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>module-webdriver/src/Codeception/.htaccess000077700000000177151323602320014652 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>module-webdriver/src/Codeception/Module/WebDriver.php000077700000347776151323602320016726 0ustar00<?php namespace Codeception\Module; use Codeception\Coverage\Subscriber\LocalServer; use Codeception\Exception\ConnectionException; use Codeception\Exception\ElementNotFound; use Codeception\Exception\MalformedLocatorException; use Codeception\Exception\ModuleConfigException; use Codeception\Exception\ModuleException; use Codeception\Exception\TestRuntimeException; use Codeception\Lib\Interfaces\ConflictsWithModule; use Codeception\Lib\Interfaces\ElementLocator; use Codeception\Lib\Interfaces\MultiSession as MultiSessionInterface; use Codeception\Lib\Interfaces\PageSourceSaver; use Codeception\Lib\Interfaces\Remote as RemoteInterface; use Codeception\Lib\Interfaces\RequiresPackage; use Codeception\Lib\Interfaces\ScreenshotSaver; use Codeception\Lib\Interfaces\SessionSnapshot; use Codeception\Lib\Interfaces\Web as WebInterface; use Codeception\Module as CodeceptionModule; use Codeception\PHPUnit\Constraint\Page as PageConstraint; use Codeception\PHPUnit\Constraint\WebDriver as WebDriverConstraint; use Codeception\PHPUnit\Constraint\WebDriverNot as WebDriverConstraintNot; use Codeception\Test\Descriptor; use Codeception\Test\Interfaces\ScenarioDriven; use Codeception\TestInterface; use Codeception\Util\ActionSequence; use Codeception\Util\Locator; use Codeception\Util\Uri; use Facebook\WebDriver\Cookie; use Facebook\WebDriver\Exception\InvalidElementStateException; use Facebook\WebDriver\Exception\InvalidSelectorException; use Facebook\WebDriver\Exception\NoSuchElementException; use Facebook\WebDriver\Exception\UnknownErrorException; use Facebook\WebDriver\Exception\UnknownServerException; use Facebook\WebDriver\Exception\WebDriverCurlException; use Facebook\WebDriver\Interactions\WebDriverActions; use Facebook\WebDriver\Remote\LocalFileDetector; use Facebook\WebDriver\Remote\RemoteWebDriver; use Facebook\WebDriver\Remote\RemoteWebElement; use Facebook\WebDriver\Remote\UselessFileDetector; use Facebook\WebDriver\Remote\WebDriverCapabilityType; use Facebook\WebDriver\WebDriverBy; use Facebook\WebDriver\WebDriverDimension; use Facebook\WebDriver\WebDriverElement; use Facebook\WebDriver\WebDriverExpectedCondition; use Facebook\WebDriver\WebDriverKeys; use Facebook\WebDriver\WebDriverSelect; /** * Run tests in real browsers using the W3C [WebDriver protocol](https://www.w3.org/TR/webdriver/). * * ## Local Testing * * ### Browsers: Chrome and/or Firefox * * First, you need to install the browser itself: Chrome and/or Firefox. * * To run tests in Chrome/Chromium, you need to install [ChromeDriver](https://sites.google.com/a/chromium.org/chromedriver/getting-started). * * To use Firefox, install [GeckoDriver](https://github.com/mozilla/geckodriver). * If you want to use both, consider setting up a dedicated [Codeception environment](https://codeception.com/docs/07-AdvancedUsage#Environments) for each. * * ### Selenium * * To run Selenium Server you need [Java](https://www.java.com/) as well as Chrome or Firefox browser installed. * * 1. Download [Selenium Standalone Server](http://docs.seleniumhq.org/download/) * 2. Launch the Selenium Server: `java -jar selenium-server-standalone-3.xx.xxx.jar`. To locate Chromedriver binary use `-Dwebdriver.chrome.driver=./chromedriver` option. For Geckodriver use `-Dwebdriver.gecko.driver=./geckodriver`. * 3. Configure this module (in `acceptance.suite.yml`) by setting `url` and `browser`: * * ```yaml * modules: * enabled: * - WebDriver: * url: 'http://localhost/' * browser: chrome # 'chrome' or 'firefox' * ``` * * Launch Selenium Server before executing tests. * * ``` * java -jar "/path/to/selenium-server-standalone-xxx.jar" * ``` * * ### ChromeDriver * * To run tests in Chrome/Chromium you may connect to ChromeDriver directly, without using Selenium Server. * * 1. Install [ChromeDriver](https://sites.google.com/a/chromium.org/chromedriver/getting-started). * 2. Launch ChromeDriver: `chromedriver --url-base=/wd/hub` * 3. Configure this module to use ChromeDriver port: * * ```yaml * modules: * enabled: * - WebDriver: * url: 'http://localhost/' * window_size: false # disabled in ChromeDriver * port: 9515 * browser: chrome * capabilities: * "goog:chromeOptions": # additional chrome options * ``` * * Additional [Chrome options](https://sites.google.com/a/chromium.org/chromedriver/capabilities) can be set in `goog:chromeOptions` capabilities. Note that Selenium 3.8 renamed this capability from `chromeOptions` to `goog:chromeOptions`. * * ### GeckoDriver * * To run tests in Firefox you may connect to GeckoDriver directly, without using Selenium Server. * * 1. Install [GeckoDriver](https://github.com/mozilla/geckodriver). * 2. Launch GeckoDriver: `geckodriver` * 3. Configure this module: * * ```yaml * modules: * enabled: * - WebDriver: * url: 'http://localhost/' * browser: firefox * path: '' * capabilities: * acceptInsecureCerts: true # allow self-signed certificates * ``` * * ### Headless Selenium in Docker * * Docker can ship Selenium Server with all its dependencies and browsers inside a single container. * Running tests inside Docker is as easy as pulling [official selenium image](https://github.com/SeleniumHQ/docker-selenium) and starting a container with Chrome: * * ``` * docker run --net=host selenium/standalone-chrome * ``` * * By using `--net=host` we allow selenium to access local websites. * * ## Cloud Testing * * Cloud Testing services can run your WebDriver tests in the cloud. * In case you want to test a local site or site behind a firewall * you should use a tunnel application provided by a service. * * ### SauceLabs * * 1. Create an account at [SauceLabs.com](http://SauceLabs.com) to get your username and access key * 2. In the module configuration use the format `username`:`access_key`@ondemand.saucelabs.com' for `host` * 3. Configure `platform` under `capabilities` to define the [Operating System](https://docs.saucelabs.com/reference/platforms-configurator/#/) * 4. run a tunnel app if your site can't be accessed from Internet * * ```yaml * modules: * enabled: * - WebDriver: * url: http://mysite.com * host: '<username>:<access key>@ondemand.saucelabs.com' * port: 80 * browser: chrome * capabilities: * platform: 'Windows 10' * ``` * * ### BrowserStack * * 1. Create an account at [BrowserStack](https://www.browserstack.com/) to get your username and access key * 2. In the module configuration use the format `username`:`access_key`@hub.browserstack.com' for `host` * 3. Configure `os` and `os_version` under `capabilities` to define the operating System * 4. If your site is available only locally or via VPN you should use a tunnel app. In this case add `browserstack.local` capability and set it to true. * * ```yaml * modules: * enabled: * - WebDriver: * url: http://mysite.com * host: '<username>:<access key>@hub.browserstack.com' * port: 80 * browser: chrome * capabilities: * os: Windows * os_version: 10 * browserstack.local: true # for local testing * ``` * ### TestingBot * * 1. Create an account at [TestingBot](https://testingbot.com/) to get your key and secret * 2. In the module configuration use the format `key`:`secret`@hub.testingbot.com' for `host` * 3. Configure `platform` under `capabilities` to define the [Operating System](https://testingbot.com/support/getting-started/browsers.html) * 4. Run [TestingBot Tunnel](https://testingbot.com/support/other/tunnel) if your site can't be accessed from Internet * * ```yaml * modules: * enabled: * - WebDriver: * url: http://mysite.com * host: '<key>:<secret>@hub.testingbot.com' * port: 80 * browser: chrome * capabilities: * platform: Windows 10 * ``` * * ## Configuration * * * `url` *required* - Base URL for your app (amOnPage opens URLs relative to this setting). * * `browser` *required* - Browser to launch. * * `host` - Selenium server host (127.0.0.1 by default). * * `port` - Selenium server port (4444 by default). * * `restart` - Set to `false` (default) to use the same browser window for all tests, or set to `true` to create a new window for each test. In any case, when all tests are finished the browser window is closed. * * `start` - Autostart a browser for tests. Can be disabled if browser session is started with `_initializeSession` inside a Helper. * * `window_size` - Initial window size. Set to `maximize` or a dimension in the format `640x480`. * * `clear_cookies` - Set to false to keep cookies, or set to true (default) to delete all cookies between tests. * * `wait` (default: 0 seconds) - Whenever element is required and is not on page, wait for n seconds to find it before fail. * * `capabilities` - Sets Selenium [desired capabilities](https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities). Should be a key-value array. * * `connection_timeout` - timeout for opening a connection to remote selenium server (30 seconds by default). * * `request_timeout` - timeout for a request to return something from remote selenium server (30 seconds by default). * * `pageload_timeout` - amount of time to wait for a page load to complete before throwing an error (default 0 seconds). * * `http_proxy` - sets http proxy server url for testing a remote server. * * `http_proxy_port` - sets http proxy server port * * `ssl_proxy` - sets ssl(https) proxy server url for testing a remote server. * * `ssl_proxy_port` - sets ssl(https) proxy server port * * `debug_log_entries` - how many selenium entries to print with `debugWebDriverLogs` or on fail (0 by default). * * `log_js_errors` - Set to true to include possible JavaScript to HTML report, or set to false (default) to deactivate. * * Example (`acceptance.suite.yml`) * * ```yaml * modules: * enabled: * - WebDriver: * url: 'http://localhost/' * browser: firefox * window_size: 1024x768 * capabilities: * unexpectedAlertBehaviour: 'accept' * firefox_profile: '~/firefox-profiles/codeception-profile.zip.b64' * ``` * * ## Loading Parts from other Modules * * While all Codeception modules are designed to work stand-alone, it's still possible to load *several* modules at once. To use e.g. the [Asserts module](https://codeception.com/docs/modules/Asserts) in your acceptance tests, just load it like this in your `acceptance.suite.yml`: * * ```yaml * modules: * enabled: * - WebDriver * - Asserts * ``` * * However, when loading a framework module (e.g. [Symfony](https://codeception.com/docs/modules/Symfony)) like this, it would lead to a conflict: When you call `$I->amOnPage()`, Codeception wouldn't know if you want to access the page using WebDriver's `amOnPage()`, or Symfony's `amOnPage()`. That's why possibly conflicting modules are separated into "parts". Here's how to load just the "services" part from e.g. Symfony: * ```yaml * modules: * enabled: * - WebDriver * - Symfony: * part: services * ``` * To find out which parts each module has, look at the "Parts" header on the module's page. * * ## Usage * * ### Locating Elements * * Most methods in this module that operate on a DOM element (e.g. `click`) accept a locator as the first argument, * which can be either a string or an array. * * If the locator is an array, it should have a single element, * with the key signifying the locator type (`id`, `name`, `css`, `xpath`, `link`, or `class`) * and the value being the locator itself. * This is called a "strict" locator. * Examples: * * * `['id' => 'foo']` matches `<div id="foo">` * * `['name' => 'foo']` matches `<div name="foo">` * * `['css' => 'input[type=input][value=foo]']` matches `<input type="input" value="foo">` * * `['xpath' => "//input[@type='submit'][contains(@value, 'foo')]"]` matches `<input type="submit" value="foobar">` * * `['link' => 'Click here']` matches `<a href="google.com">Click here</a>` * * `['class' => 'foo']` matches `<div class="foo">` * * Writing good locators can be tricky. * The Mozilla team has written an excellent guide titled [Writing reliable locators for Selenium and WebDriver tests](https://blog.mozilla.org/webqa/2013/09/26/writing-reliable-locators-for-selenium-and-webdriver-tests/). * * If you prefer, you may also pass a string for the locator. This is called a "fuzzy" locator. * In this case, Codeception uses a a variety of heuristics (depending on the exact method called) to determine what element you're referring to. * For example, here's the heuristic used for the `submitForm` method: * * 1. Does the locator look like an ID selector (e.g. "#foo")? If so, try to find a form matching that ID. * 2. If nothing found, check if locator looks like a CSS selector. If so, run it. * 3. If nothing found, check if locator looks like an XPath expression. If so, run it. * 4. Throw an `ElementNotFound` exception. * * Be warned that fuzzy locators can be significantly slower than strict locators. * Especially if you use Selenium WebDriver with `wait` (aka implicit wait) option. * In the example above if you set `wait` to 5 seconds and use XPath string as fuzzy locator, * `submitForm` method will wait for 5 seconds at each step. * That means 5 seconds finding the form by ID, another 5 seconds finding by CSS * until it finally tries to find the form by XPath). * If speed is a concern, it's recommended you stick with explicitly specifying the locator type via the array syntax. * * ## Public Properties * * * `webDriver` - instance of `\Facebook\WebDriver\Remote\RemoteWebDriver`. Can be accessed from Helper classes for complex WebDriver interactions. * * ```php * // inside Helper class * $this->getModule('WebDriver')->webDriver->getKeyboard()->sendKeys('hello, webdriver'); * ``` * */ class WebDriver extends CodeceptionModule implements WebInterface, RemoteInterface, MultiSessionInterface, SessionSnapshot, ScreenshotSaver, PageSourceSaver, ElementLocator, ConflictsWithModule, RequiresPackage { protected $requiredFields = ['browser', 'url']; protected $config = [ 'protocol' => 'http', 'host' => '127.0.0.1', 'port' => '4444', 'path' => '/wd/hub', 'start' => true, 'restart' => false, 'wait' => 0, 'clear_cookies' => true, 'window_size' => false, 'capabilities' => [], 'connection_timeout' => null, 'request_timeout' => null, 'pageload_timeout' => null, 'http_proxy' => null, 'http_proxy_port' => null, 'ssl_proxy' => null, 'ssl_proxy_port' => null, 'debug_log_entries' => 0, 'log_js_errors' => false ]; protected $wdHost; protected $capabilities; protected $connectionTimeoutInMs; protected $requestTimeoutInMs; protected $test; protected $sessions = []; protected $sessionSnapshots = []; protected $httpProxy; protected $httpProxyPort; protected $sslProxy; protected $sslProxyPort; /** * @var RemoteWebDriver */ public $webDriver; /** * @var RemoteWebElement */ protected $baseElement; public function _requires() { return ['Facebook\WebDriver\Remote\RemoteWebDriver' => '"php-webdriver/webdriver": "^1.0.1"']; } /** * @return RemoteWebElement * @throws ModuleException */ protected function getBaseElement() { if (!$this->baseElement) { throw new ModuleException($this, "Page not loaded. Use `\$I->amOnPage` (or hidden API methods `_request` and `_loadPage`) to open it"); } return $this->baseElement; } public function _initialize() { $this->wdHost = sprintf('%s://%s:%s%s', $this->config['protocol'], $this->config['host'], $this->config['port'], $this->config['path']); $this->capabilities = $this->config['capabilities']; $this->capabilities[WebDriverCapabilityType::BROWSER_NAME] = $this->config['browser']; if ($proxy = $this->getProxy()) { $this->capabilities[WebDriverCapabilityType::PROXY] = $proxy; } $this->connectionTimeoutInMs = $this->config['connection_timeout'] * 1000; $this->requestTimeoutInMs = $this->config['request_timeout'] * 1000; $this->loadFirefoxProfile(); } /** * Change capabilities of WebDriver. Should be executed before starting a new browser session. * This method expects a function to be passed which returns array or [WebDriver Desired Capabilities](https://github.com/php-webdriver/php-webdriver/blob/main/lib/Remote/DesiredCapabilities.php) object. * Additional [Chrome options](https://github.com/php-webdriver/php-webdriver/wiki/ChromeOptions) (like adding extensions) can be passed as well. * * ```php * <?php // in helper * public function _before(TestInterface $test) * { * $this->getModule('WebDriver')->_capabilities(function($currentCapabilities) { * // or new \Facebook\WebDriver\Remote\DesiredCapabilities(); * return \Facebook\WebDriver\Remote\DesiredCapabilities::firefox(); * }); * } * ``` * * to make this work load `\Helper\Acceptance` before `WebDriver` in `acceptance.suite.yml`: * * ```yaml * modules: * enabled: * - \Helper\Acceptance * - WebDriver * ``` * * For instance, [**BrowserStack** cloud service](https://www.browserstack.com/automate/capabilities) may require a test name to be set in capabilities. * This is how it can be done via `_capabilities` method from `Helper\Acceptance`: * * ```php * <?php // inside Helper\Acceptance * public function _before(TestInterface $test) * { * $name = $test->getMetadata()->getName(); * $this->getModule('WebDriver')->_capabilities(function($currentCapabilities) use ($name) { * $currentCapabilities['name'] = $name; * return $currentCapabilities; * }); * } * ``` * In this case, please ensure that `\Helper\Acceptance` is loaded before WebDriver so new capabilities could be applied. * * @api * @param \Closure $capabilityFunction */ public function _capabilities(\Closure $capabilityFunction) { $this->capabilities = $capabilityFunction($this->capabilities); } public function _conflicts() { return 'Codeception\Lib\Interfaces\Web'; } public function _before(TestInterface $test) { if (!isset($this->webDriver) && $this->config['start']) { $this->_initializeSession(); } $this->setBaseElement(); $test->getMetadata()->setCurrent( [ 'browser' => $this->webDriver->getCapabilities()->getBrowserName(), 'capabilities' => $this->webDriver->getCapabilities()->toArray(), ] ); } /** * Restarts a web browser. * Can be used with `_reconfigure` to open browser with different configuration * * ```php * <?php * // inside a Helper * $this->getModule('WebDriver')->_restart(); // just restart * $this->getModule('WebDriver')->_restart(['browser' => $browser]); // reconfigure + restart * ``` * * @param array $config * @api */ public function _restart($config = []) { $this->webDriver->quit(); if (!empty($config)) { $this->_reconfigure($config); } $this->_initializeSession(); } protected function onReconfigure() { $this->_initialize(); } protected function loadFirefoxProfile() { if (!array_key_exists('firefox_profile', $this->config['capabilities'])) { return; } $firefox_profile = $this->config['capabilities']['firefox_profile']; if (file_exists($firefox_profile) === false) { throw new ModuleConfigException( __CLASS__, "Firefox profile does not exist under given path " . $firefox_profile ); } // Set firefox profile as capability $this->capabilities['firefox_profile'] = file_get_contents($firefox_profile); } protected function initialWindowSize() { if ($this->config['window_size'] == 'maximize') { $this->maximizeWindow(); return; } $size = explode('x', $this->config['window_size']); if (count($size) == 2) { $this->resizeWindow(intval($size[0]), intval($size[1])); } } public function _after(TestInterface $test) { if ($this->config['restart']) { $this->stopAllSessions(); return; } if ($this->config['clear_cookies'] && isset($this->webDriver)) { try { $this->webDriver->manage()->deleteAllCookies(); } catch (\Exception $e) { // may cause fatal errors when not handled $this->debug("Error, can't clean cookies after a test: " . $e->getMessage()); } } } public function _failed(TestInterface $test, $fail) { $this->debugWebDriverLogs($test); $filename = preg_replace('~[^a-zA-Z0-9\x80-\xff]~', '.', Descriptor::getTestSignatureUnique($test)); $outputDir = codecept_output_dir(); $this->_saveScreenshot($report = $outputDir . mb_strcut($filename, 0, 245, 'utf-8') . '.fail.png'); $test->getMetadata()->addReport('png', $report); $this->_savePageSource($report = $outputDir . mb_strcut($filename, 0, 244, 'utf-8') . '.fail.html'); $test->getMetadata()->addReport('html', $report); $this->debug("Screenshot and page source were saved into '$outputDir' dir"); } /** * Print out latest Selenium Logs in debug mode * * @param \Codeception\TestInterface $test */ public function debugWebDriverLogs(TestInterface $test = null) { if (!isset($this->webDriver)) { $this->debug('WebDriver::debugWebDriverLogs method has been called when webDriver is not set'); return; } // don't show logs if log entries not set if (!$this->config['debug_log_entries']) { return; } try { // Dump out latest Selenium logs $logs = $this->webDriver->manage()->getAvailableLogTypes(); foreach ($logs as $logType) { $logEntries = array_slice( $this->webDriver->manage()->getLog($logType), -$this->config['debug_log_entries'] ); if (empty($logEntries)) { $this->debugSection("Selenium {$logType} Logs", " EMPTY "); continue; } $this->debugSection("Selenium {$logType} Logs", "\n" . $this->formatLogEntries($logEntries)); if ($logType === 'browser' && $this->config['log_js_errors'] && ($test instanceof ScenarioDriven) ) { $this->logJSErrors($test, $logEntries); } } } catch (\Exception $e) { $this->debug('Unable to retrieve Selenium logs : ' . $e->getMessage()); } } /** * Turns an array of log entries into a human-readable string. * Each log entry is an array with the keys "timestamp", "level", and "message". * See https://code.google.com/p/selenium/wiki/JsonWireProtocol#Log_Entry_JSON_Object * * @param array $logEntries * @return string */ protected function formatLogEntries(array $logEntries) { $formattedLogs = ''; foreach ($logEntries as $logEntry) { // Timestamp is in milliseconds, but date() requires seconds. $time = date('H:i:s', $logEntry['timestamp'] / 1000) . // Append the milliseconds to the end of the time string '.' . ($logEntry['timestamp'] % 1000); $formattedLogs .= "{$time} {$logEntry['level']} - {$logEntry['message']}\n"; } return $formattedLogs; } /** * Logs JavaScript errors as comments. * * @param ScenarioDriven $test * @param array $browserLogEntries */ protected function logJSErrors(ScenarioDriven $test, array $browserLogEntries) { foreach ($browserLogEntries as $logEntry) { if (true === isset($logEntry['level']) && true === isset($logEntry['message']) && $this->isJSError($logEntry['level'], $logEntry['message']) ) { // Timestamp is in milliseconds, but date() requires seconds. $time = date('H:i:s', $logEntry['timestamp'] / 1000) . // Append the milliseconds to the end of the time string '.' . ($logEntry['timestamp'] % 1000); $test->getScenario()->comment("{$time} {$logEntry['level']} - {$logEntry['message']}"); } } } /** * Determines if the log entry is an error. * The decision is made depending on browser and log-level. * * @param string $logEntryLevel * @param string $message * @return bool */ protected function isJSError($logEntryLevel, $message) { return ( ($this->isPhantom() && $logEntryLevel != 'INFO') // phantomjs logs errors as "WARNING" || $logEntryLevel === 'SEVERE' // other browsers log errors as "SEVERE" ) && strpos($message, 'ERR_PROXY_CONNECTION_FAILED') === false; // ignore blackhole proxy } public function _afterSuite() { // this is just to make sure webDriver is cleared after suite $this->stopAllSessions(); } protected function stopAllSessions() { foreach ($this->sessions as $session) { $this->_closeSession($session); } $this->webDriver = null; $this->baseElement = null; } public function amOnSubdomain($subdomain) { $url = $this->config['url']; $url = preg_replace('~(https?:\/\/)(.*\.)(.*\.)~', "$1$3", $url); // removing current subdomain $url = preg_replace('~(https?:\/\/)(.*)~', "$1$subdomain.$2", $url); // inserting new $this->_reconfigure(['url' => $url]); } /** * Returns URL of a host. * * @api * @return mixed * @throws ModuleConfigException */ public function _getUrl() { if (!isset($this->config['url'])) { throw new ModuleConfigException( __CLASS__, "Module connection failure. The URL for client can't bre retrieved" ); } return $this->config['url']; } protected function getProxy() { $proxyConfig = []; if ($this->config['http_proxy']) { $proxyConfig['httpProxy'] = $this->config['http_proxy']; if ($this->config['http_proxy_port']) { $proxyConfig['httpProxy'] .= ':' . $this->config['http_proxy_port']; } } if ($this->config['ssl_proxy']) { $proxyConfig['sslProxy'] = $this->config['ssl_proxy']; if ($this->config['ssl_proxy_port']) { $proxyConfig['sslProxy'] .= ':' . $this->config['ssl_proxy_port']; } } if (!empty($proxyConfig)) { $proxyConfig['proxyType'] = 'manual'; return $proxyConfig; } return null; } /** * Uri of currently opened page. * @return string * @api * @throws ModuleException */ public function _getCurrentUri() { $url = $this->webDriver->getCurrentURL(); if ($url == 'about:blank' || strpos($url, 'data:') === 0) { throw new ModuleException($this, 'Current url is blank, no page was opened'); } return Uri::retrieveUri($url); } public function _saveScreenshot($filename) { if (!isset($this->webDriver)) { $this->debug('WebDriver::_saveScreenshot method has been called when webDriver is not set'); return; } try { $this->webDriver->takeScreenshot($filename); } catch (\Exception $e) { $this->debug('Unable to retrieve screenshot from Selenium : ' . $e->getMessage()); return; } } public function _saveElementScreenshot($selector, $filename) { if (!isset($this->webDriver)) { $this->debug('WebDriver::_saveElementScreenshot method has been called when webDriver is not set'); return; } try { $this->matchFirstOrFail($this->webDriver, $selector)->takeElementScreenshot($filename); } catch (\Exception $e) { $this->debug('Unable to retrieve element screenshot from Selenium : ' . $e->getMessage()); return; } } public function _findElements($locator) { return $this->match($this->webDriver, $locator); } /** * Saves HTML source of a page to a file * @param $filename */ public function _savePageSource($filename) { if (!isset($this->webDriver)) { $this->debug('WebDriver::_savePageSource method has been called when webDriver is not set'); return; } try { file_put_contents($filename, $this->webDriver->getPageSource()); } catch (\Exception $e) { $this->debug('Unable to retrieve source page from Selenium : ' . $e->getMessage()); } } /** * Takes a screenshot of the current window and saves it to `tests/_output/debug`. * * ``` php * <?php * $I->amOnPage('/user/edit'); * $I->makeScreenshot('edit_page'); * // saved to: tests/_output/debug/edit_page.png * $I->makeScreenshot(); * // saved to: tests/_output/debug/2017-05-26_14-24-11_4b3403665fea6.png * ``` * * @param $name */ public function makeScreenshot($name = null) { if (empty($name)) { $name = uniqid(date("Y-m-d_H-i-s_")); } $debugDir = codecept_log_dir() . 'debug'; if (!is_dir($debugDir)) { mkdir($debugDir, 0777); } $screenName = $debugDir . DIRECTORY_SEPARATOR . $name . '.png'; $this->_saveScreenshot($screenName); $this->debugSection('Screenshot Saved', "file://$screenName"); } /** * Takes a screenshot of an element of the current window and saves it to `tests/_output/debug`. * * ``` php * <?php * $I->amOnPage('/user/edit'); * $I->makeElementScreenshot('#dialog', 'edit_page'); * // saved to: tests/_output/debug/edit_page.png * $I->makeElementScreenshot('#dialog'); * // saved to: tests/_output/debug/2017-05-26_14-24-11_4b3403665fea6.png * ``` * * @param $name */ public function makeElementScreenshot($selector, $name = null) { if (empty($name)) { $name = uniqid(date("Y-m-d_H-i-s_")); } $debugDir = codecept_log_dir() . 'debug'; if (!is_dir($debugDir)) { mkdir($debugDir, 0777); } $screenName = $debugDir . DIRECTORY_SEPARATOR . $name . '.png'; $this->_saveElementScreenshot($selector, $screenName); $this->debugSection('Screenshot Saved', "file://$screenName"); } public function makeHtmlSnapshot($name = null) { if (empty($name)) { $name = uniqid(date("Y-m-d_H-i-s_")); } $debugDir = codecept_output_dir() . 'debug'; if (!is_dir($debugDir)) { mkdir($debugDir, 0777); } $fileName = $debugDir . DIRECTORY_SEPARATOR . $name . '.html'; $this->_savePageSource($fileName); $this->debugSection('Snapshot Saved', "file://$fileName"); } /** * Resize the current window. * * ``` php * <?php * $I->resizeWindow(800, 600); * * ``` * * @param int $width * @param int $height */ public function resizeWindow($width, $height) { $this->webDriver->manage()->window()->setSize(new WebDriverDimension($width, $height)); } private function debugCookies() { $result = []; $cookies = $this->webDriver->manage()->getCookies(); foreach ($cookies as $cookie) { $result[] = is_array($cookie) ? $cookie : $cookie->toArray(); } $this->debugSection('Cookies', json_encode($result)); } public function seeCookie($cookie, array $params = []) { $cookies = $this->filterCookies($this->webDriver->manage()->getCookies(), $params); $cookies = array_map( function ($c) { return $c['name']; }, $cookies ); $this->debugCookies(); $this->assertContains($cookie, $cookies); } public function dontSeeCookie($cookie, array $params = []) { $cookies = $this->filterCookies($this->webDriver->manage()->getCookies(), $params); $cookies = array_map( function ($c) { return $c['name']; }, $cookies ); $this->debugCookies(); $this->assertNotContains($cookie, $cookies); } public function setCookie($cookie, $value, array $params = [], $showDebug = true) { $params['name'] = $cookie; $params['value'] = $value; if (isset($params['expires'])) { // PhpBrowser compatibility $params['expiry'] = $params['expires']; } // #5401 Supply defaults, otherwise chromedriver 2.46 complains. $defaults = [ 'path' => '/', 'expiry' => time() + 86400, 'secure' => false, 'httpOnly' => false, ]; foreach ($defaults as $key => $default) { if (empty($params[$key])) { $params[$key] = $default; } } $this->webDriver->manage()->addCookie($params); if ($showDebug) { $this->debugCookies(); } } public function resetCookie($cookie, array $params = []) { $this->webDriver->manage()->deleteCookieNamed($cookie); $this->debugCookies(); } public function grabCookie($cookie, array $params = []) { $params['name'] = $cookie; $cookies = $this->filterCookies($this->webDriver->manage()->getCookies(), $params); if (empty($cookies)) { return null; } $cookie = reset($cookies); return $cookie['value']; } /** * Grabs current page source code. * * @throws ModuleException if no page was opened. * * @return string Current page source code. */ public function grabPageSource() { // Make sure that some page was opened. $this->_getCurrentUri(); return $this->webDriver->getPageSource(); } protected function filterCookies($cookies, $params = []) { foreach (['domain', 'path', 'name'] as $filter) { if (!isset($params[$filter])) { continue; } $cookies = array_filter( $cookies, function ($item) use ($filter, $params) { return $item[$filter] == $params[$filter]; } ); } return $cookies; } public function amOnUrl($url) { $host = Uri::retrieveHost($url); $this->_reconfigure(['url' => $host]); $this->debugSection('Host', $host); $this->webDriver->get($url); } public function amOnPage($page) { $url = Uri::appendPath($this->config['url'], $page); $this->debugSection('GET', $url); $this->webDriver->get($url); } public function see($text, $selector = null) { if (!$selector) { return $this->assertPageContains($text); } $this->enableImplicitWait(); $nodes = $this->matchVisible($selector); $this->disableImplicitWait(); $this->assertNodesContain($text, $nodes, $selector); } public function dontSee($text, $selector = null) { if (!$selector) { return $this->assertPageNotContains($text); } $nodes = $this->matchVisible($selector); $this->assertNodesNotContain($text, $nodes, $selector); } public function seeInSource($raw) { $this->assertPageSourceContains($raw); } public function dontSeeInSource($raw) { $this->assertPageSourceNotContains($raw); } /** * Checks that the page source contains the given string. * * ```php * <?php * $I->seeInPageSource('<link rel="apple-touch-icon"'); * ``` * * @param $text */ public function seeInPageSource($text) { $this->assertThat( $this->webDriver->getPageSource(), new PageConstraint($text, $this->_getCurrentUri()), '' ); } /** * Checks that the page source doesn't contain the given string. * * @param $text */ public function dontSeeInPageSource($text) { $this->assertThatItsNot( $this->webDriver->getPageSource(), new PageConstraint($text, $this->_getCurrentUri()), '' ); } public function click($link, $context = null) { $page = $this->webDriver; if ($context) { $page = $this->matchFirstOrFail($this->webDriver, $context); } $el = $this->_findClickable($page, $link); if (!$el) { // check one more time if this was a CSS selector we didn't match try { $els = $this->match($page, $link); } catch (MalformedLocatorException $e) { throw new ElementNotFound("name=$link", "'$link' is invalid CSS and XPath selector and Link or Button"); } $el = reset($els); } if (!$el) { throw new ElementNotFound($link, 'Link or Button or CSS or XPath'); } $el->click(); } /** * Locates a clickable element. * * Use it in Helpers or GroupObject or Extension classes: * * ```php * <?php * $module = $this->getModule('WebDriver'); * $page = $module->webDriver; * * // search a link or button on a page * $el = $module->_findClickable($page, 'Click Me'); * * // search a link or button within an element * $topBar = $module->_findElements('.top-bar')[0]; * $el = $module->_findClickable($topBar, 'Click Me'); * * ``` * @api * @param RemoteWebDriver $page WebDriver instance or an element to search within * @param $link a link text or locator to click * @return WebDriverElement */ public function _findClickable($page, $link) { if (is_array($link) or ($link instanceof WebDriverBy)) { return $this->matchFirstOrFail($page, $link); } // try to match by strict locators, CSS Ids or XPath if (Locator::isPrecise($link)) { return $this->matchFirstOrFail($page, $link); } $locator = static::xpathLiteral(trim($link)); // narrow $xpath = Locator::combine( ".//a[normalize-space(.)=$locator]", ".//button[normalize-space(.)=$locator]", ".//a/img[normalize-space(@alt)=$locator]/ancestor::a", ".//input[./@type = 'submit' or ./@type = 'image' or ./@type = 'button'][normalize-space(@value)=$locator]" ); $els = $page->findElements(WebDriverBy::xpath($xpath)); if (count($els)) { return reset($els); } // wide $xpath = Locator::combine( ".//a[./@href][((contains(normalize-space(string(.)), $locator)) or contains(./@title, $locator) or .//img[contains(./@alt, $locator)])]", ".//input[./@type = 'submit' or ./@type = 'image' or ./@type = 'button'][contains(./@value, $locator)]", ".//input[./@type = 'image'][contains(./@alt, $locator)]", ".//button[contains(normalize-space(string(.)), $locator)]", ".//input[./@type = 'submit' or ./@type = 'image' or ./@type = 'button'][./@name = $locator or ./@title = $locator]", ".//button[./@name = $locator or ./@title = $locator]" ); $els = $page->findElements(WebDriverBy::xpath($xpath)); if (count($els)) { return reset($els); } return null; } /** * @param $selector * @return WebDriverElement[] * @throws \Codeception\Exception\ElementNotFound */ protected function findFields($selector) { if ($selector instanceof WebDriverElement) { return [$selector]; } if (is_array($selector) || ($selector instanceof WebDriverBy)) { $fields = $this->match($this->webDriver, $selector); if (empty($fields)) { throw new ElementNotFound($selector); } return $fields; } $locator = static::xpathLiteral(trim($selector)); // by text or label $xpath = Locator::combine( // @codingStandardsIgnoreStart ".//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')][(((./@name = $locator) or ./@id = //label[contains(normalize-space(string(.)), $locator)]/@for) or ./@placeholder = $locator)]", ".//label[contains(normalize-space(string(.)), $locator)]//.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]" // @codingStandardsIgnoreEnd ); $fields = $this->webDriver->findElements(WebDriverBy::xpath($xpath)); if (!empty($fields)) { return $fields; } // by name $xpath = ".//*[self::input | self::textarea | self::select][@name = $locator]"; $fields = $this->webDriver->findElements(WebDriverBy::xpath($xpath)); if (!empty($fields)) { return $fields; } // try to match by CSS or XPath $fields = $this->match($this->webDriver, $selector, false); if (!empty($fields)) { return $fields; } throw new ElementNotFound($selector, "Field by name, label, CSS or XPath"); } /** * @param $selector * @return WebDriverElement * @throws \Codeception\Exception\ElementNotFound */ protected function findField($selector) { $arr = $this->findFields($selector); return reset($arr); } public function seeLink($text, $url = null) { $this->enableImplicitWait(); $nodes = $this->getBaseElement()->findElements(WebDriverBy::partialLinkText($text)); $this->disableImplicitWait(); $currentUri = $this->_getCurrentUri(); if (empty($nodes)) { $this->fail("No links containing text '$text' were found in page $currentUri"); } if ($url) { $nodes = $this->filterNodesByHref($url, $nodes); } $this->assertNotEmpty($nodes, "No links containing text '$text' and URL '$url' were found in page $currentUri"); } public function dontSeeLink($text, $url = null) { $nodes = $this->getBaseElement()->findElements(WebDriverBy::partialLinkText($text)); $currentUri = $this->_getCurrentUri(); if (!$url) { $this->assertEmpty($nodes, "Link containing text '$text' was found in page $currentUri"); } else { $nodes = $this->filterNodesByHref($url, $nodes); $this->assertEmpty($nodes, "Link containing text '$text' and URL '$url' was found in page $currentUri"); } } /** * @param string $url * @param $nodes * @return array */ private function filterNodesByHref($url, $nodes) { //current uri can be relative, merging it with configured base url gives absolute url $absoluteCurrentUrl = Uri::mergeUrls($this->_getUrl(), $this->_getCurrentUri()); $expectedUrl = Uri::mergeUrls($absoluteCurrentUrl, $url); $nodes = array_filter( $nodes, function (WebDriverElement $e) use ($expectedUrl, $absoluteCurrentUrl) { $elementHref = Uri::mergeUrls($absoluteCurrentUrl, $e->getAttribute('href')); return $elementHref === $expectedUrl; } ); return $nodes; } public function seeInCurrentUrl($uri) { $this->assertStringContainsString($uri, $this->_getCurrentUri()); } public function seeCurrentUrlEquals($uri) { $this->assertEquals($uri, $this->_getCurrentUri()); } public function seeCurrentUrlMatches($uri) { $this->assertRegExp($uri, $this->_getCurrentUri()); } public function dontSeeInCurrentUrl($uri) { $this->assertStringNotContainsString($uri, $this->_getCurrentUri()); } public function dontSeeCurrentUrlEquals($uri) { $this->assertNotEquals($uri, $this->_getCurrentUri()); } public function dontSeeCurrentUrlMatches($uri) { $this->assertNotRegExp($uri, $this->_getCurrentUri()); } public function grabFromCurrentUrl($uri = null) { if (!$uri) { return $this->_getCurrentUri(); } $matches = []; $res = preg_match($uri, $this->_getCurrentUri(), $matches); if (!$res) { $this->fail("Couldn't match $uri in " . $this->_getCurrentUri()); } if (!isset($matches[1])) { $this->fail("Nothing to grab. A regex parameter required. Ex: '/user/(\\d+)'"); } return $matches[1]; } public function seeCheckboxIsChecked($checkbox) { $this->assertTrue($this->findField($checkbox)->isSelected()); } public function dontSeeCheckboxIsChecked($checkbox) { $this->assertFalse($this->findField($checkbox)->isSelected()); } public function seeInField($field, $value) { $els = $this->findFields($field); $this->assert($this->proceedSeeInField($els, $value)); } public function dontSeeInField($field, $value) { $els = $this->findFields($field); $this->assertNot($this->proceedSeeInField($els, $value)); } public function seeInFormFields($formSelector, array $params) { $this->proceedSeeInFormFields($formSelector, $params, false); } public function dontSeeInFormFields($formSelector, array $params) { $this->proceedSeeInFormFields($formSelector, $params, true); } protected function proceedSeeInFormFields($formSelector, array $params, $assertNot) { $form = $this->match($this->getBaseElement(), $formSelector); if (empty($form)) { throw new ElementNotFound($formSelector, "Form via CSS or XPath"); } $form = reset($form); $els = []; foreach ($params as $name => $values) { $this->pushFormField($els, $form, $name, $values); } foreach ($els as $arrayElement) { list($el, $values) = $arrayElement; if (!is_array($values)) { $values = [$values]; } foreach ($values as $value) { $ret = $this->proceedSeeInField($el, $value); if ($assertNot) { $this->assertNot($ret); } else { $this->assert($ret); } } } } /** * Map an array element passed to seeInFormFields to its corresponding WebDriver element, * recursing through array values if the field is not found. * * @param array $els The previously found elements. * @param RemoteWebElement $form The form in which to search for fields. * @param string $name The field's name. * @param mixed $values * @return void */ protected function pushFormField(&$els, $form, $name, $values) { $el = $form->findElements(WebDriverBy::name($name)); if ($el) { $els[] = [$el, $values]; } elseif (is_array($values)) { foreach ($values as $key => $value) { $this->pushFormField($els, $form, "{$name}[$key]", $value); } } else { throw new ElementNotFound($name); } } /** * @param RemoteWebElement[] $elements * @param $value * @return array */ protected function proceedSeeInField(array $elements, $value) { $strField = reset($elements)->getAttribute('name'); if (reset($elements)->getTagName() === 'select') { $el = reset($elements); $elements = $el->findElements(WebDriverBy::xpath('.//option')); if (empty($value) && empty($elements)) { return ['True', true]; } } $currentValues = []; if (is_bool($value)) { $currentValues = [false]; } foreach ($elements as $el) { switch ($el->getTagName()) { case 'input': if ($el->getAttribute('type') === 'radio' || $el->getAttribute('type') === 'checkbox') { if ($el->getAttribute('checked')) { if (is_bool($value)) { $currentValues = [true]; break; } else { $currentValues[] = $el->getAttribute('value'); } } } else { $currentValues[] = $el->getAttribute('value'); } break; case 'option': // no break we need the trim text and the value also if (!$el->isSelected()) { break; } $currentValues[] = $el->getText(); case 'textarea': // we include trimmed and real value of textarea for check $currentValues[] = trim($el->getText()); default: $currentValues[] = $el->getAttribute('value'); // raw value break; } } return [ 'Contains', $value, $currentValues, "Failed testing for '$value' in $strField's value: '" . implode("', '", $currentValues) . "'" ]; } public function selectOption($select, $option) { $el = $this->findField($select); if ($el->getTagName() != 'select') { $els = $this->matchCheckables($select); $radio = null; foreach ($els as $el) { $radio = $this->findCheckable($el, $option, true); if ($radio) { break; } } if (!$radio) { throw new ElementNotFound($select, "Radiobutton with value or name '$option in"); } $radio->click(); return; } $wdSelect = new WebDriverSelect($el); if ($wdSelect->isMultiple()) { $wdSelect->deselectAll(); } if (!is_array($option)) { $option = [$option]; } $matched = false; if (key($option) !== 'value') { foreach ($option as $opt) { try { $wdSelect->selectByVisibleText($opt); $matched = true; } catch (NoSuchElementException $e) { } } } if ($matched) { return; } if (key($option) !== 'text') { foreach ($option as $opt) { try { $wdSelect->selectByValue($opt); $matched = true; } catch (NoSuchElementException $e) { } } } if ($matched) { return; } // partially matching foreach ($option as $opt) { try { $optElement = $el->findElement(WebDriverBy::xpath('.//option [contains (., "' . $opt . '")]')); $matched = true; if (!$optElement->isSelected()) { $optElement->click(); } } catch (NoSuchElementException $e) { // exception treated at the end } } if ($matched) { return; } throw new ElementNotFound(json_encode($option), "Option inside $select matched by name or value"); } /** * Manually starts a new browser session. * * ```php * <?php * $this->getModule('WebDriver')->_initializeSession(); * ``` * * @api */ public function _initializeSession() { try { $this->sessions[] = $this->webDriver; $this->webDriver = RemoteWebDriver::create( $this->wdHost, $this->capabilities, $this->connectionTimeoutInMs, $this->requestTimeoutInMs, $this->httpProxy, $this->httpProxyPort ); if (!is_null($this->config['pageload_timeout'])) { $this->webDriver->manage()->timeouts()->pageLoadTimeout($this->config['pageload_timeout']); } $this->setBaseElement(); $this->initialWindowSize(); } catch (WebDriverCurlException $e) { codecept_debug('Curl error: ' . $e->getMessage()); throw new ConnectionException("Can't connect to WebDriver at {$this->wdHost}. Make sure that ChromeDriver, GeckoDriver or Selenium Server is running."); } } /** * Loads current RemoteWebDriver instance as a session * * @api * @param RemoteWebDriver $session */ public function _loadSession($session) { $this->webDriver = $session; $this->setBaseElement(); } /** * Returns current WebDriver session for saving * * @api * @return RemoteWebDriver */ public function _backupSession() { return $this->webDriver; } /** * Manually closes current WebDriver session. * * ```php * <?php * $this->getModule('WebDriver')->_closeSession(); * * // close a specific session * $webDriver = $this->getModule('WebDriver')->webDriver; * $this->getModule('WebDriver')->_closeSession($webDriver); * ``` * * @api * @param $webDriver (optional) a specific webdriver session instance */ public function _closeSession($webDriver = null) { if (!$webDriver and $this->webDriver) { $webDriver = $this->webDriver; } if (!$webDriver) { return; } try { $webDriver->quit(); unset($webDriver); } catch (UnknownServerException $e) { // Session already closed so nothing to do } catch (UnknownErrorException $e) { // Session already closed so nothing to do } } /** * Unselect an option in the given select box. * * @param $select * @param $option */ public function unselectOption($select, $option) { $el = $this->findField($select); $wdSelect = new WebDriverSelect($el); if (!is_array($option)) { $option = [$option]; } $matched = false; foreach ($option as $opt) { try { $wdSelect->deselectByVisibleText($opt); $matched = true; } catch (NoSuchElementException $e) { // exception treated at the end } try { $wdSelect->deselectByValue($opt); $matched = true; } catch (NoSuchElementException $e) { // exception treated at the end } } if ($matched) { return; } throw new ElementNotFound(json_encode($option), "Option inside $select matched by name or value"); } /** * @param $context * @param $radioOrCheckbox * @param bool $byValue * @return mixed|null */ protected function findCheckable($context, $radioOrCheckbox, $byValue = false) { if ($radioOrCheckbox instanceof WebDriverElement) { return $radioOrCheckbox; } if (is_array($radioOrCheckbox) or ($radioOrCheckbox instanceof WebDriverBy)) { return $this->matchFirstOrFail($this->getBaseElement(), $radioOrCheckbox); } $locator = static::xpathLiteral($radioOrCheckbox); if ($context instanceof WebDriverElement && $context->getTagName() === 'input') { $contextType = $context->getAttribute('type'); if (!in_array($contextType, ['checkbox', 'radio'], true)) { return null; } $nameLiteral = static::xpathLiteral($context->getAttribute('name')); $typeLiteral = static::xpathLiteral($contextType); $inputLocatorFragment = "input[@type = $typeLiteral][@name = $nameLiteral]"; $xpath = Locator::combine( // @codingStandardsIgnoreStart "ancestor::form//{$inputLocatorFragment}[(@id = ancestor::form//label[contains(normalize-space(string(.)), $locator)]/@for) or @placeholder = $locator]", // @codingStandardsIgnoreEnd "ancestor::form//label[contains(normalize-space(string(.)), $locator)]//{$inputLocatorFragment}" ); if ($byValue) { $xpath = Locator::combine($xpath, "ancestor::form//{$inputLocatorFragment}[@value = $locator]"); } } else { $xpath = Locator::combine( // @codingStandardsIgnoreStart "//input[@type = 'checkbox' or @type = 'radio'][(@id = //label[contains(normalize-space(string(.)), $locator)]/@for) or @placeholder = $locator or @name = $locator]", // @codingStandardsIgnoreEnd "//label[contains(normalize-space(string(.)), $locator)]//input[@type = 'radio' or @type = 'checkbox']" ); if ($byValue) { $xpath = Locator::combine($xpath, "//input[@type = 'checkbox' or @type = 'radio'][@value = $locator]"); } } $els = $context->findElements(WebDriverBy::xpath($xpath)); if (count($els)) { return reset($els); } $els = $context->findElements(WebDriverBy::xpath(str_replace('ancestor::form', '', $xpath))); if (count($els)) { return reset($els); } $els = $this->match($context, $radioOrCheckbox); if (count($els)) { return reset($els); } return null; } protected function matchCheckables($selector) { $els = $this->match($this->webDriver, $selector); if (!count($els)) { throw new ElementNotFound($selector, "Element containing radio by CSS or XPath"); } return $els; } public function checkOption($option) { $field = $this->findCheckable($this->webDriver, $option); if (!$field) { throw new ElementNotFound($option, "Checkbox or Radio by Label or CSS or XPath"); } if ($field->isSelected()) { return; } $field->click(); } public function uncheckOption($option) { $field = $this->findCheckable($this->getBaseElement(), $option); if (!$field) { throw new ElementNotFound($option, "Checkbox by Label or CSS or XPath"); } if (!$field->isSelected()) { return; } $field->click(); } public function fillField($field, $value) { $el = $this->findField($field); $el->clear(); $el->sendKeys((string)$value); } /** * Clears given field which isn't empty. * * ``` php * <?php * $I->clearField('#username'); * ``` * * @param $field */ public function clearField($field) { $el = $this->findField($field); $el->clear(); } public function attachFile($field, $filename) { $el = $this->findField($field); // in order to be compatible on different OS $filePath = codecept_data_dir() . $filename; if (!file_exists($filePath)) { throw new \InvalidArgumentException("File does not exist: $filePath"); } if (!is_readable($filePath)) { throw new \InvalidArgumentException("File is not readable: $filePath"); } // in order for remote upload to be enabled $el->setFileDetector(new LocalFileDetector()); // skip file detector for phantomjs if ($this->isPhantom()) { $el->setFileDetector(new UselessFileDetector()); } $el->sendKeys(realpath($filePath)); } /** * Grabs all visible text from the current page. * * @return string */ protected function getVisibleText() { if ($this->getBaseElement() instanceof RemoteWebElement) { return $this->getBaseElement()->getText(); } $els = $this->getBaseElement()->findElements(WebDriverBy::cssSelector('body')); if (isset($els[0])) { return $els[0]->getText(); } return ''; } public function grabTextFrom($cssOrXPathOrRegex) { $els = $this->match($this->getBaseElement(), $cssOrXPathOrRegex, false); if (count($els)) { return $els[0]->getText(); } if (@preg_match($cssOrXPathOrRegex, $this->webDriver->getPageSource(), $matches)) { return $matches[1]; } throw new ElementNotFound($cssOrXPathOrRegex, 'CSS or XPath or Regex'); } public function grabAttributeFrom($cssOrXpath, $attribute) { $el = $this->matchFirstOrFail($this->getBaseElement(), $cssOrXpath); return $el->getAttribute($attribute); } public function grabValueFrom($field) { $el = $this->findField($field); // value of multiple select is the value of the first selected option if ($el->getTagName() == 'select') { $select = new WebDriverSelect($el); return $select->getFirstSelectedOption()->getAttribute('value'); } return $el->getAttribute('value'); } public function grabMultiple($cssOrXpath, $attribute = null) { $els = $this->match($this->getBaseElement(), $cssOrXpath); return array_map( function (WebDriverElement $e) use ($attribute) { if ($attribute) { return $e->getAttribute($attribute); } return $e->getText(); }, $els ); } protected function filterByAttributes($els, array $attributes) { foreach ($attributes as $attr => $value) { $els = array_filter( $els, function (WebDriverElement $el) use ($attr, $value) { return $el->getAttribute($attr) == $value; } ); } return $els; } public function seeElement($selector, $attributes = []) { $this->enableImplicitWait(); $els = $this->matchVisible($selector); $this->disableImplicitWait(); $els = $this->filterByAttributes($els, $attributes); $this->assertNotEmpty($els); } public function dontSeeElement($selector, $attributes = []) { $els = $this->matchVisible($selector); $els = $this->filterByAttributes($els, $attributes); $this->assertEmpty($els); } /** * Checks that the given element exists on the page, even it is invisible. * * ``` php * <?php * $I->seeElementInDOM('//form/input[type=hidden]'); * ?> * ``` * * @param $selector * @param array $attributes */ public function seeElementInDOM($selector, $attributes = []) { $this->enableImplicitWait(); $els = $this->match($this->getBaseElement(), $selector); $els = $this->filterByAttributes($els, $attributes); $this->disableImplicitWait(); $this->assertNotEmpty($els); } /** * Opposite of `seeElementInDOM`. * * @param $selector * @param array $attributes */ public function dontSeeElementInDOM($selector, $attributes = []) { $els = $this->match($this->getBaseElement(), $selector); $els = $this->filterByAttributes($els, $attributes); $this->assertEmpty($els); } public function seeNumberOfElements($selector, $expected) { $counted = count($this->matchVisible($selector)); if (is_array($expected)) { list($floor, $ceil) = $expected; $this->assertTrue( $floor <= $counted && $ceil >= $counted, 'Number of elements counted differs from expected range' ); } else { $this->assertEquals( $expected, $counted, 'Number of elements counted differs from expected number' ); } } public function seeNumberOfElementsInDOM($selector, $expected) { $counted = count($this->match($this->getBaseElement(), $selector)); if (is_array($expected)) { list($floor, $ceil) = $expected; $this->assertTrue( $floor <= $counted && $ceil >= $counted, 'Number of elements counted differs from expected range' ); } else { $this->assertEquals( $expected, $counted, 'Number of elements counted differs from expected number' ); } } public function seeOptionIsSelected($selector, $optionText) { $el = $this->findField($selector); if ($el->getTagName() !== 'select') { $els = $this->matchCheckables($selector); foreach ($els as $k => $el) { $els[$k] = $this->findCheckable($el, $optionText, true); } $this->assertNotEmpty( array_filter( $els, function ($e) { return $e && $e->isSelected(); } ) ); return; } $select = new WebDriverSelect($el); $this->assertNodesContain($optionText, $select->getAllSelectedOptions(), 'option'); } public function dontSeeOptionIsSelected($selector, $optionText) { $el = $this->findField($selector); if ($el->getTagName() !== 'select') { $els = $this->matchCheckables($selector); foreach ($els as $k => $el) { $els[$k] = $this->findCheckable($el, $optionText, true); } $this->assertEmpty( array_filter( $els, function ($e) { return $e && $e->isSelected(); } ) ); return; } $select = new WebDriverSelect($el); $this->assertNodesNotContain($optionText, $select->getAllSelectedOptions(), 'option'); } public function seeInTitle($title) { $this->assertStringContainsString($title, $this->webDriver->getTitle()); } public function dontSeeInTitle($title) { $this->assertStringNotContainsString($title, $this->webDriver->getTitle()); } /** * Accepts the active JavaScript native popup window, as created by `window.alert`|`window.confirm`|`window.prompt`. * Don't confuse popups with modal windows, * as created by [various libraries](http://jster.net/category/windows-modals-popups). */ public function acceptPopup() { if ($this->isPhantom()) { throw new ModuleException($this, 'PhantomJS does not support working with popups'); } $this->webDriver->switchTo()->alert()->accept(); } /** * Dismisses the active JavaScript popup, as created by `window.alert`, `window.confirm`, or `window.prompt`. */ public function cancelPopup() { if ($this->isPhantom()) { throw new ModuleException($this, 'PhantomJS does not support working with popups'); } $this->webDriver->switchTo()->alert()->dismiss(); } /** * Checks that the active JavaScript popup, * as created by `window.alert`|`window.confirm`|`window.prompt`, contains the given string. * * @param $text * * @throws \Codeception\Exception\ModuleException */ public function seeInPopup($text) { if ($this->isPhantom()) { throw new ModuleException($this, 'PhantomJS does not support working with popups'); } $alert = $this->webDriver->switchTo()->alert(); try { $this->assertStringContainsString($text, $alert->getText()); } catch (\PHPUnit\Framework\AssertionFailedError $e) { $alert->dismiss(); throw $e; } } /** * Checks that the active JavaScript popup, * as created by `window.alert`|`window.confirm`|`window.prompt`, does NOT contain the given string. * * @param $text * * @throws \Codeception\Exception\ModuleException */ public function dontSeeInPopup($text) { if ($this->isPhantom()) { throw new ModuleException($this, 'PhantomJS does not support working with popups'); } $alert = $this->webDriver->switchTo()->alert(); try { $this->assertStringNotContainsString($text, $alert->getText()); } catch (\PHPUnit\Framework\AssertionFailedError $e) { $alert->dismiss(); throw $e; } } /** * Enters text into a native JavaScript prompt popup, as created by `window.prompt`. * * @param $keys * * @throws \Codeception\Exception\ModuleException */ public function typeInPopup($keys) { if ($this->isPhantom()) { throw new ModuleException($this, 'PhantomJS does not support working with popups'); } $this->webDriver->switchTo()->alert()->sendKeys($keys); } /** * Reloads the current page. */ public function reloadPage() { $this->webDriver->navigate()->refresh(); } /** * Moves back in history. */ public function moveBack() { $this->webDriver->navigate()->back(); $this->debug($this->_getCurrentUri()); } /** * Moves forward in history. */ public function moveForward() { $this->webDriver->navigate()->forward(); $this->debug($this->_getCurrentUri()); } protected function getSubmissionFormFieldName($name) { if (substr($name, -2) === '[]') { return substr($name, 0, -2); } return $name; } /** * Submits the given form on the page, optionally with the given form * values. Give the form fields values as an array. Note that hidden fields * can't be accessed. * * Skipped fields will be filled by their values from the page. * You don't need to click the 'Submit' button afterwards. * This command itself triggers the request to form's action. * * You can optionally specify what button's value to include * in the request with the last parameter as an alternative to * explicitly setting its value in the second parameter, as * button values are not otherwise included in the request. * * Examples: * * ``` php * <?php * $I->submitForm('#login', [ * 'login' => 'davert', * 'password' => '123456' * ]); * // or * $I->submitForm('#login', [ * 'login' => 'davert', * 'password' => '123456' * ], 'submitButtonName'); * * ``` * * For example, given this sample "Sign Up" form: * * ``` html * <form action="/sign_up"> * Login: * <input type="text" name="user[login]" /><br/> * Password: * <input type="password" name="user[password]" /><br/> * Do you agree to our terms? * <input type="checkbox" name="user[agree]" /><br/> * Select pricing plan: * <select name="plan"> * <option value="1">Free</option> * <option value="2" selected="selected">Paid</option> * </select> * <input type="submit" name="submitButton" value="Submit" /> * </form> * ``` * * You could write the following to submit it: * * ``` php * <?php * $I->submitForm( * '#userForm', * [ * 'user[login]' => 'Davert', * 'user[password]' => '123456', * 'user[agree]' => true * ], * 'submitButton' * ); * ``` * Note that "2" will be the submitted value for the "plan" field, as it is * the selected option. * * Also note that this differs from PhpBrowser, in that * ```'user' => [ 'login' => 'Davert' ]``` is not supported at the moment. * Named array keys *must* be included in the name as above. * * Pair this with seeInFormFields for quick testing magic. * * ``` php * <?php * $form = [ * 'field1' => 'value', * 'field2' => 'another value', * 'checkbox1' => true, * // ... * ]; * $I->submitForm('//form[@id=my-form]', $form, 'submitButton'); * // $I->amOnPage('/path/to/form-page') may be needed * $I->seeInFormFields('//form[@id=my-form]', $form); * ?> * ``` * * Parameter values must be set to arrays for multiple input fields * of the same name, or multi-select combo boxes. For checkboxes, * either the string value can be used, or boolean values which will * be replaced by the checkbox's value in the DOM. * * ``` php * <?php * $I->submitForm('#my-form', [ * 'field1' => 'value', * 'checkbox' => [ * 'value of first checkbox', * 'value of second checkbox', * ], * 'otherCheckboxes' => [ * true, * false, * false, * ], * 'multiselect' => [ * 'first option value', * 'second option value', * ] * ]); * ?> * ``` * * Mixing string and boolean values for a checkbox's value is not supported * and may produce unexpected results. * * Field names ending in "[]" must be passed without the trailing square * bracket characters, and must contain an array for its value. This allows * submitting multiple values with the same name, consider: * * ```php * $I->submitForm('#my-form', [ * 'field[]' => 'value', * 'field[]' => 'another value', // 'field[]' is already a defined key * ]); * ``` * * The solution is to pass an array value: * * ```php * // this way both values are submitted * $I->submitForm('#my-form', [ * 'field' => [ * 'value', * 'another value', * ] * ]); * ``` * * The `$button` parameter can be either a string, an array or an instance * of Facebook\WebDriver\WebDriverBy. When it is a string, the * button will be found by its "name" attribute. If $button is an * array then it will be treated as a strict selector and a WebDriverBy * will be used verbatim. * * For example, given the following HTML: * * ``` html * <input type="submit" name="submitButton" value="Submit" /> * ``` * * `$button` could be any one of the following: * - 'submitButton' * - ['name' => 'submitButton'] * - WebDriverBy::name('submitButton') * * @param $selector * @param $params * @param $button */ public function submitForm($selector, array $params, $button = null) { $form = $this->matchFirstOrFail($this->getBaseElement(), $selector); $fields = $form->findElements( WebDriverBy::cssSelector('input:enabled,textarea:enabled,select:enabled,input[type=hidden]') ); foreach ($fields as $field) { $fieldName = $this->getSubmissionFormFieldName($field->getAttribute('name')); if (!isset($params[$fieldName])) { continue; } $value = $params[$fieldName]; if (is_array($value) && $field->getTagName() !== 'select') { if ($field->getAttribute('type') === 'checkbox' || $field->getAttribute('type') === 'radio') { $found = false; foreach ($value as $index => $val) { if (!is_bool($val) && $val === $field->getAttribute('value')) { array_splice($params[$fieldName], $index, 1); $value = $val; $found = true; break; } } if (!$found && !empty($value) && is_bool(reset($value))) { $value = array_pop($params[$fieldName]); } } else { $value = array_pop($params[$fieldName]); } } if ($field->getAttribute('type') === 'checkbox' || $field->getAttribute('type') === 'radio') { if ($value === true || $value === $field->getAttribute('value')) { $this->checkOption($field); } else { $this->uncheckOption($field); } } elseif ($field->getAttribute('type') === 'button' || $field->getAttribute('type') === 'submit') { continue; } elseif ($field->getTagName() === 'select') { $this->selectOption($field, $value); } else { $this->fillField($field, $value); } } $this->debugSection( 'Uri', $form->getAttribute('action') ? $form->getAttribute('action') : $this->_getCurrentUri() ); $this->debugSection('Method', $form->getAttribute('method') ? $form->getAttribute('method') : 'GET'); $this->debugSection('Parameters', json_encode($params)); $submitted = false; if (!empty($button)) { if (is_array($button)) { $buttonSelector = $this->getStrictLocator($button); } elseif ($button instanceof WebDriverBy) { $buttonSelector = $button; } else { $buttonSelector = WebDriverBy::name($button); } $els = $form->findElements($buttonSelector); if (!empty($els)) { $el = reset($els); $el->click(); $submitted = true; } } if (!$submitted) { $form->submit(); } $this->debugSection('Page', $this->_getCurrentUri()); } /** * Waits up to $timeout seconds for the given element to change. * Element "change" is determined by a callback function which is called repeatedly * until the return value evaluates to true. * * ``` php * <?php * use \Facebook\WebDriver\WebDriverElement * $I->waitForElementChange('#menu', function(WebDriverElement $el) { * return $el->isDisplayed(); * }, 100); * ?> * ``` * * @param $element * @param \Closure $callback * @param int $timeout seconds * @throws \Codeception\Exception\ElementNotFound */ public function waitForElementChange($element, \Closure $callback, $timeout = 30) { $el = $this->matchFirstOrFail($this->getBaseElement(), $element); $checker = function () use ($el, $callback) { return $callback($el); }; $this->webDriver->wait($timeout)->until($checker); } /** * Waits up to $timeout seconds for an element to appear on the page. * If the element doesn't appear, a timeout exception is thrown. * * ``` php * <?php * $I->waitForElement('#agree_button', 30); // secs * $I->click('#agree_button'); * ?> * ``` * * @param $element * @param int $timeout seconds * @throws \Exception */ public function waitForElement($element, $timeout = 10) { $condition = WebDriverExpectedCondition::presenceOfElementLocated($this->getLocator($element)); $this->webDriver->wait($timeout)->until($condition); } /** * Waits up to $timeout seconds for the given element to be visible on the page. * If element doesn't appear, a timeout exception is thrown. * * ``` php * <?php * $I->waitForElementVisible('#agree_button', 30); // secs * $I->click('#agree_button'); * ?> * ``` * * @param $element * @param int $timeout seconds * @throws \Exception */ public function waitForElementVisible($element, $timeout = 10) { $condition = WebDriverExpectedCondition::visibilityOfElementLocated($this->getLocator($element)); $this->webDriver->wait($timeout)->until($condition); } /** * Waits up to $timeout seconds for the given element to become invisible. * If element stays visible, a timeout exception is thrown. * * ``` php * <?php * $I->waitForElementNotVisible('#agree_button', 30); // secs * ?> * ``` * * @param $element * @param int $timeout seconds * @throws \Exception */ public function waitForElementNotVisible($element, $timeout = 10) { $condition = WebDriverExpectedCondition::invisibilityOfElementLocated($this->getLocator($element)); $this->webDriver->wait($timeout)->until($condition); } /** * Waits up to $timeout seconds for the given element to be clickable. * If element doesn't become clickable, a timeout exception is thrown. * * ``` php * <?php * $I->waitForElementClickable('#agree_button', 30); // secs * $I->click('#agree_button'); * ?> * ``` * * @param $element * @param int $timeout seconds * @throws \Exception */ public function waitForElementClickable($element, $timeout = 10) { $condition = WebDriverExpectedCondition::elementToBeClickable($this->getLocator($element)); $this->webDriver->wait($timeout)->until($condition); } /** * Waits up to $timeout seconds for the given string to appear on the page. * * Can also be passed a selector to search in, be as specific as possible when using selectors. * waitForText() will only watch the first instance of the matching selector / text provided. * If the given text doesn't appear, a timeout exception is thrown. * * ``` php * <?php * $I->waitForText('foo', 30); // secs * $I->waitForText('foo', 30, '.title'); // secs * ?> * ``` * * @param string $text * @param int $timeout seconds * @param string $selector optional * @throws \Exception */ public function waitForText($text, $timeout = 10, $selector = null) { $message = sprintf( 'Waited for %d secs but text %s still not found', $timeout, Locator::humanReadableString($text) ); if (!$selector) { $condition = WebDriverExpectedCondition::textToBePresentInElement(WebDriverBy::xpath('//body'), $text); $this->webDriver->wait($timeout)->until($condition, $message); return; } $condition = WebDriverExpectedCondition::textToBePresentInElement($this->getLocator($selector), $text); $this->webDriver->wait($timeout)->until($condition, $message); } /** * Wait for $timeout seconds. * * @param int|float $timeout secs * @throws \Codeception\Exception\TestRuntimeException */ public function wait($timeout) { if ($timeout >= 1000) { throw new TestRuntimeException( " Waiting for more then 1000 seconds: 16.6667 mins\n Please note that wait method accepts number of seconds as parameter." ); } usleep($timeout * 1000000); } /** * Low-level API method. * If Codeception commands are not enough, this allows you to use Selenium WebDriver methods directly: * * ``` php * $I->executeInSelenium(function(\Facebook\WebDriver\Remote\RemoteWebDriver $webdriver) { * $webdriver->get('http://google.com'); * }); * ``` * * This runs in the context of the * [RemoteWebDriver class](https://github.com/php-webdriver/php-webdriver/blob/master/lib/remote/RemoteWebDriver.php). * Try not to use this command on a regular basis. * If Codeception lacks a feature you need, please implement it and submit a patch. * * @param callable $function */ public function executeInSelenium(\Closure $function) { return $function($this->webDriver); } /** * Switch to another window identified by name. * * The window can only be identified by name. If the $name parameter is blank, the parent window will be used. * * Example: * ``` html * <input type="button" value="Open window" onclick="window.open('http://example.com', 'another_window')"> * ``` * * ``` php * <?php * $I->click("Open window"); * # switch to another window * $I->switchToWindow("another_window"); * # switch to parent window * $I->switchToWindow(); * ?> * ``` * * If the window has no name, match it by switching to next active tab using `switchToNextTab` method. * * Or use native Selenium functions to get access to all opened windows: * * ``` php * <?php * $I->executeInSelenium(function (\Facebook\WebDriver\Remote\RemoteWebDriver $webdriver) { * $handles=$webdriver->getWindowHandles(); * $last_window = end($handles); * $webdriver->switchTo()->window($last_window); * }); * ?> * ``` * * @param string|null $name */ public function switchToWindow($name = null) { $this->webDriver->switchTo()->window($name); } /** * Switch to another iframe on the page. * * Example: * ``` html * <iframe name="another_frame" id="fr1" src="http://example.com"> * * ``` * * ``` php * <?php * # switch to iframe by name * $I->switchToIFrame("another_frame"); * # switch to iframe by CSS or XPath * $I->switchToIFrame("#fr1"); * # switch to parent page * $I->switchToIFrame(); * * ``` * * @param string|null $locator (name, CSS or XPath) */ public function switchToIFrame($locator = null) { $this->findAndSwitchToFrame($locator, 'iframe'); } /** * Switch to another frame on the page. * * Example: * ``` html * <frame name="another_frame" id="fr1" src="http://example.com"> * * ``` * * ``` php * <?php * # switch to frame by name * $I->switchToFrame("another_frame"); * # switch to frame by CSS or XPath * $I->switchToFrame("#fr1"); * # switch to parent page * $I->switchToFrame(); * * ``` * * @param string|null $locator (name, CSS or XPath) */ public function switchToFrame($locator = null) { $this->findAndSwitchToFrame($locator, 'frame'); } /** * @param string|null $locator * @param string $tag */ private function findAndSwitchToFrame($locator = null, $tag = 'frame') { if ($locator === null) { $this->webDriver->switchTo()->defaultContent(); return; } $els = null; try { $els = $this->_findElements("{$tag}[name='$locator']"); } catch (\Exception $e) { $this->debug('Failed to find locator by name: ' . $e->getMessage()); } if (!isset($els) || !is_array($els) || !count($els)) { $this->debug(ucfirst($tag) . ' was not found by name, locating ' . $tag . ' by CSS or XPath'); $els = $this->_findElements($locator); } if (!count($els)) { throw new ElementNotFound($locator, ucfirst($tag)); } $this->webDriver->switchTo()->frame($els[0]); } /** * Executes JavaScript and waits up to $timeout seconds for it to return true. * * In this example we will wait up to 60 seconds for all jQuery AJAX requests to finish. * * ``` php * <?php * $I->waitForJS("return $.active == 0;", 60); * ?> * ``` * * @param string $script * @param int $timeout seconds */ public function waitForJS($script, $timeout = 5) { $condition = function ($wd) use ($script) { return $wd->executeScript($script); }; $message = sprintf( 'Waited for %d secs but script %s still doesn\'t evaluate to true', $timeout, Locator::humanReadableString($script) ); $this->webDriver->wait($timeout)->until($condition, $message); } /** * Executes custom JavaScript. * * This example uses jQuery to get a value and assigns that value to a PHP variable: * * ```php * <?php * $myVar = $I->executeJS('return $("#myField").val()'); * * // additional arguments can be passed as array * // Example shows `Hello World` alert: * $I->executeJS("window.alert(arguments[0])", ['Hello world']); * ``` * * @param $script * @param array $arguments * @return mixed */ public function executeJS($script, array $arguments = []) { return $this->webDriver->executeScript($script, $arguments); } /** * Executes asynchronous JavaScript. * A callback should be executed by JavaScript to exit from a script. * Callback is passed as a last element in `arguments` array. * Additional arguments can be passed as array in second parameter. * * ```js * // wait for 1200 milliseconds my running `setTimeout` * * $I->executeAsyncJS('setTimeout(arguments[0], 1200)'); * * $seconds = 1200; // or seconds are passed as argument * $I->executeAsyncJS('setTimeout(arguments[1], arguments[0])', [$seconds]); * ``` * * @param $script * @param array $arguments * @return mixed */ public function executeAsyncJS($script, array $arguments = []) { return $this->webDriver->executeAsyncScript($script, $arguments); } /** * Maximizes the current window. */ public function maximizeWindow() { $this->webDriver->manage()->window()->maximize(); } /** * Performs a simple mouse drag-and-drop operation. * * ``` php * <?php * $I->dragAndDrop('#drag', '#drop'); * ?> * ``` * * @param string $source (CSS ID or XPath) * @param string $target (CSS ID or XPath) */ public function dragAndDrop($source, $target) { $snodes = $this->matchFirstOrFail($this->getBaseElement(), $source); $tnodes = $this->matchFirstOrFail($this->getBaseElement(), $target); $action = new WebDriverActions($this->webDriver); $action->dragAndDrop($snodes, $tnodes)->perform(); } /** * Move mouse over the first element matched by the given locator. * If the first parameter null then the page is used. * If the second and third parameters are given, * then the mouse is moved to an offset of the element's top-left corner. * Otherwise, the mouse is moved to the center of the element. * * ``` php * <?php * $I->moveMouseOver(['css' => '.checkout']); * $I->moveMouseOver(null, 20, 50); * $I->moveMouseOver(['css' => '.checkout'], 20, 50); * ?> * ``` * * @param string $cssOrXPath css or xpath of the web element * @param int $offsetX * @param int $offsetY * * @throws \Codeception\Exception\ElementNotFound */ public function moveMouseOver($cssOrXPath = null, $offsetX = null, $offsetY = null) { $where = null; if (null !== $cssOrXPath) { $el = $this->matchFirstOrFail($this->getBaseElement(), $cssOrXPath); $where = $el->getCoordinates(); } $this->webDriver->getMouse()->mouseMove($where, $offsetX, $offsetY); } /** * Performs click with the left mouse button on an element. * If the first parameter `null` then the offset is relative to the actual mouse position. * If the second and third parameters are given, * then the mouse is moved to an offset of the element's top-left corner. * Otherwise, the mouse is moved to the center of the element. * * ``` php * <?php * $I->clickWithLeftButton(['css' => '.checkout']); * $I->clickWithLeftButton(null, 20, 50); * $I->clickWithLeftButton(['css' => '.checkout'], 20, 50); * ?> * ``` * * @param string $cssOrXPath css or xpath of the web element (body by default). * @param int $offsetX * @param int $offsetY * * @throws \Codeception\Exception\ElementNotFound */ public function clickWithLeftButton($cssOrXPath = null, $offsetX = null, $offsetY = null) { $this->moveMouseOver($cssOrXPath, $offsetX, $offsetY); $this->webDriver->getMouse()->click(); } /** * Performs contextual click with the right mouse button on an element. * If the first parameter `null` then the offset is relative to the actual mouse position. * If the second and third parameters are given, * then the mouse is moved to an offset of the element's top-left corner. * Otherwise, the mouse is moved to the center of the element. * * ``` php * <?php * $I->clickWithRightButton(['css' => '.checkout']); * $I->clickWithRightButton(null, 20, 50); * $I->clickWithRightButton(['css' => '.checkout'], 20, 50); * ?> * ``` * * @param string $cssOrXPath css or xpath of the web element (body by default). * @param int $offsetX * @param int $offsetY * * @throws \Codeception\Exception\ElementNotFound */ public function clickWithRightButton($cssOrXPath = null, $offsetX = null, $offsetY = null) { $this->moveMouseOver($cssOrXPath, $offsetX, $offsetY); $this->webDriver->getMouse()->contextClick(); } /** * Performs a double-click on an element matched by CSS or XPath. * * @param $cssOrXPath * @throws \Codeception\Exception\ElementNotFound */ public function doubleClick($cssOrXPath) { $el = $this->matchFirstOrFail($this->getBaseElement(), $cssOrXPath); $this->webDriver->getMouse()->doubleClick($el->getCoordinates()); } /** * @param $page * @param $selector * @param bool $throwMalformed * @return array */ protected function match($page, $selector, $throwMalformed = true) { if (is_array($selector)) { try { return $page->findElements($this->getStrictLocator($selector)); } catch (InvalidSelectorException $e) { throw new MalformedLocatorException(key($selector) . ' => ' . reset($selector), "Strict locator"); } catch (InvalidElementStateException $e) { if ($this->isPhantom() and $e->getResults()['status'] == 12) { throw new MalformedLocatorException( key($selector) . ' => ' . reset($selector), "Strict locator " . $e->getCode() ); } } } if ($selector instanceof WebDriverBy) { try { return $page->findElements($selector); } catch (InvalidSelectorException $e) { throw new MalformedLocatorException( sprintf( "WebDriverBy::%s('%s')", $selector->getMechanism(), $selector->getValue() ), 'WebDriver' ); } } $isValidLocator = false; $nodes = []; try { if (Locator::isID($selector)) { $isValidLocator = true; $nodes = $page->findElements(WebDriverBy::id(substr($selector, 1))); } if (Locator::isClass($selector)) { $isValidLocator = true; $nodes = $page->findElements(WebDriverBy::className(substr($selector, 1))); } if (empty($nodes) and Locator::isCSS($selector)) { $isValidLocator = true; try { $nodes = $page->findElements(WebDriverBy::cssSelector($selector)); } catch (InvalidElementStateException $e) { $nodes = $page->findElements(WebDriverBy::linkText($selector)); } } if (empty($nodes) and Locator::isXPath($selector)) { $isValidLocator = true; $nodes = $page->findElements(WebDriverBy::xpath($selector)); } } catch (InvalidSelectorException $e) { throw new MalformedLocatorException($selector); } if (!$isValidLocator and $throwMalformed) { throw new MalformedLocatorException($selector); } return $nodes; } /** * @param array $by * @return WebDriverBy */ protected function getStrictLocator(array $by) { $type = key($by); $locator = $by[$type]; switch ($type) { case 'id': return WebDriverBy::id($locator); case 'name': return WebDriverBy::name($locator); case 'css': return WebDriverBy::cssSelector($locator); case 'xpath': return WebDriverBy::xpath($locator); case 'link': return WebDriverBy::linkText($locator); case 'class': return WebDriverBy::className($locator); default: throw new MalformedLocatorException( "$by => $locator", "Strict locator can be either xpath, css, id, link, class, name: " ); } } /** * @param $page * @param $selector * @return WebDriverElement * @throws \Codeception\Exception\ElementNotFound */ protected function matchFirstOrFail($page, $selector) { $this->enableImplicitWait(); $els = $this->match($page, $selector); $this->disableImplicitWait(); if (!count($els)) { throw new ElementNotFound($selector, "CSS or XPath"); } return reset($els); } /** * Presses the given key on the given element. * To specify a character and modifier (e.g. <kbd>Ctrl</kbd>, Alt, Shift, Meta), pass an array for `$char` with * the modifier as the first element and the character as the second. * For special keys, use the constants from [`Facebook\WebDriver\WebDriverKeys`](https://github.com/php-webdriver/php-webdriver/blob/main/lib/WebDriverKeys.php). * * ``` php * <?php * // <input id="page" value="old" /> * $I->pressKey('#page','a'); // => olda * $I->pressKey('#page',array('ctrl','a'),'new'); //=> new * $I->pressKey('#page',array('shift','111'),'1','x'); //=> old!!!1x * $I->pressKey('descendant-or-self::*[@id='page']','u'); //=> oldu * $I->pressKey('#name', array('ctrl', 'a'), \Facebook\WebDriver\WebDriverKeys::DELETE); //=>'' * ?> * ``` * * @param $element * @param $char string|array Can be char or array with modifier. You can provide several chars. * @throws \Codeception\Exception\ElementNotFound */ public function pressKey($element, $char) { $el = $this->matchFirstOrFail($this->getBaseElement(), $element); $args = func_get_args(); array_shift($args); $keys = []; foreach ($args as $key) { $keys[] = $this->convertKeyModifier($key); } $el->sendKeys($keys); } protected function convertKeyModifier($keys) { if (!is_array($keys)) { return $keys; } if (!isset($keys[1])) { return $keys; } list($modifier, $key) = $keys; switch ($modifier) { case 'ctrl': case 'control': return [WebDriverKeys::CONTROL, $key]; case 'alt': return [WebDriverKeys::ALT, $key]; case 'shift': return [WebDriverKeys::SHIFT, $key]; case 'meta': return [WebDriverKeys::META, $key]; } return $keys; } protected function assertNodesContain($text, $nodes, $selector = null) { $this->assertNodeConstraint($nodes, new WebDriverConstraint($text, $this->_getCurrentUri()), $selector); } protected function assertNodesNotContain($text, $nodes, $selector = null) { $this->assertNodeConstraint($nodes, new WebDriverConstraintNot($text, $this->_getCurrentUri()), $selector); } protected function assertNodeConstraint($nodes, WebDriverConstraint $constraint, $selector = null) { $message = $selector; if (is_array($selector)) { $type = key($selector); $locator = $selector[$type]; $message = $type . ':' . $locator; } $this->assertThat($nodes, $constraint, $message); } protected function assertPageContains($needle, $message = '') { $this->assertThat( htmlspecialchars_decode($this->getVisibleText()), new PageConstraint($needle, $this->_getCurrentUri()), $message ); } protected function assertPageNotContains($needle, $message = '') { $this->assertThatItsNot( htmlspecialchars_decode($this->getVisibleText()), new PageConstraint($needle, $this->_getCurrentUri()), $message ); } protected function assertPageSourceContains($needle, $message = '') { $this->assertThat( $this->webDriver->getPageSource(), new PageConstraint($needle, $this->_getCurrentUri()), $message ); } protected function assertPageSourceNotContains($needle, $message = '') { $this->assertThatItsNot( $this->webDriver->getPageSource(), new PageConstraint($needle, $this->_getCurrentUri()), $message ); } /** * Append the given text to the given element. * Can also add a selection to a select box. * * ``` php * <?php * $I->appendField('#mySelectbox', 'SelectValue'); * $I->appendField('#myTextField', 'appended'); * ?> * ``` * * @param string $field * @param string $value * @throws \Codeception\Exception\ElementNotFound */ public function appendField($field, $value) { $el = $this->findField($field); switch ($el->getTagName()) { //Multiple select case "select": $matched = false; $wdSelect = new WebDriverSelect($el); try { $wdSelect->selectByVisibleText($value); $matched = true; } catch (NoSuchElementException $e) { // exception treated at the end } try { $wdSelect->selectByValue($value); $matched = true; } catch (NoSuchElementException $e) { // exception treated at the end } if ($matched) { return; } throw new ElementNotFound(json_encode($value), "Option inside $field matched by name or value"); case "textarea": $el->sendKeys($value); return; case "div": //allows for content editable divs $el->sendKeys(WebDriverKeys::END); $el->sendKeys($value); return; //Text, Checkbox, Radio case "input": $type = $el->getAttribute('type'); if ($type == 'checkbox') { //Find by value or css,id,xpath $field = $this->findCheckable($this->getBaseElement(), $value, true); if (!$field) { throw new ElementNotFound($value, "Checkbox or Radio by Label or CSS or XPath"); } if ($field->isSelected()) { return; } $field->click(); return; } elseif ($type == 'radio') { $this->selectOption($field, $value); return; } $el->sendKeys($value); return; } throw new ElementNotFound($field, "Field by name, label, CSS or XPath"); } /** * @param $selector * @return array */ protected function matchVisible($selector) { $els = $this->match($this->getBaseElement(), $selector); $nodes = array_filter( $els, function (WebDriverElement $el) { return $el->isDisplayed(); } ); return $nodes; } /** * @param $selector * @return WebDriverBy * @throws \InvalidArgumentException */ protected function getLocator($selector) { if ($selector instanceof WebDriverBy) { return $selector; } if (is_array($selector)) { return $this->getStrictLocator($selector); } if (Locator::isID($selector)) { return WebDriverBy::id(substr($selector, 1)); } if (Locator::isCSS($selector)) { return WebDriverBy::cssSelector($selector); } if (Locator::isXPath($selector)) { return WebDriverBy::xpath($selector); } throw new \InvalidArgumentException("Only CSS or XPath allowed"); } public function saveSessionSnapshot($name) { $this->sessionSnapshots[$name] = []; foreach ($this->webDriver->manage()->getCookies() as $cookie) { if (in_array(trim($cookie['name']), [LocalServer::COVERAGE_COOKIE, LocalServer::COVERAGE_COOKIE_ERROR])) { continue; } if ($this->cookieDomainMatchesConfigUrl($cookie)) { $this->sessionSnapshots[$name][] = $cookie; } } $this->debugSection('Snapshot', "Saved \"$name\" session snapshot"); } public function loadSessionSnapshot($name) { if (!isset($this->sessionSnapshots[$name])) { return false; } foreach ($this->webDriver->manage()->getCookies() as $cookie) { if (in_array(trim($cookie['name']), [LocalServer::COVERAGE_COOKIE, LocalServer::COVERAGE_COOKIE_ERROR])) { continue; } $this->webDriver->manage()->deleteCookieNamed($cookie['name']); } foreach ($this->sessionSnapshots[$name] as $cookie) { $this->setCookie($cookie['name'], $cookie['value'], (array)$cookie, false); } $this->debugCookies(); $this->debugSection('Snapshot', "Restored \"$name\" session snapshot"); return true; } public function deleteSessionSnapshot($name) { if (isset($this->sessionSnapshots[$name])) { unset($this->sessionSnapshots[$name]); } $this->debugSection('Snapshot', "Deleted \"$name\" session snapshot"); } /** * Check if the cookie domain matches the config URL. * * Taken from Guzzle\Cookie\SetCookie * * @param array|Cookie $cookie * @return bool */ private function cookieDomainMatchesConfigUrl($cookie) { if (!isset($cookie['domain'])) { return true; } $domain = parse_url($this->config['url'], PHP_URL_HOST); // Remove the leading '.' as per spec in RFC 6265. // http://tools.ietf.org/html/rfc6265#section-5.2.3 $cookieDomain = ltrim($cookie['domain'], '.'); // Domain not set or exact match. if (!$cookieDomain || !strcasecmp($domain, $cookieDomain)) { return true; } // Matching the subdomain according to RFC 6265. // http://tools.ietf.org/html/rfc6265#section-5.1.3 if (filter_var($domain, FILTER_VALIDATE_IP)) { return false; } return (bool) preg_match('/\.' . preg_quote($cookieDomain, '/') . '$/', $domain); } /** * @return bool */ protected function isPhantom() { return strpos($this->config['browser'], 'phantom') === 0; } /** * Move to the middle of the given element matched by the given locator. * Extra shift, calculated from the top-left corner of the element, * can be set by passing $offsetX and $offsetY parameters. * * ``` php * <?php * $I->scrollTo(['css' => '.checkout'], 20, 50); * ?> * ``` * * @param $selector * @param int $offsetX * @param int $offsetY */ public function scrollTo($selector, $offsetX = null, $offsetY = null) { $el = $this->matchFirstOrFail($this->getBaseElement(), $selector); $x = $el->getLocation()->getX() + $offsetX; $y = $el->getLocation()->getY() + $offsetY; $this->webDriver->executeScript("window.scrollTo($x, $y)"); } /** * Opens a new browser tab and switches to it. * * ```php * <?php * $I->openNewTab(); * ``` * The tab is opened with JavaScript's `window.open()`, which means: * * Some adblockers might restrict it. * * The sessionStorage is copied to the new tab (contrary to a tab that was manually opened by the user) */ public function openNewTab() { $this->executeJS("window.open('about:blank','_blank');"); $this->switchToNextTab(); } /** * Closes current browser tab and switches to previous active tab. * * ```php * <?php * $I->closeTab(); * ``` */ public function closeTab() { $prevTab = $this->getRelativeTabHandle(-1); $this->webDriver->close(); $this->webDriver->switchTo()->window($prevTab); } /** * Switches to next browser tab. * An offset can be specified. * * ```php * <?php * // switch to next tab * $I->switchToNextTab(); * // switch to 2nd next tab * $I->switchToNextTab(2); * ``` * @param int $offset 1 */ public function switchToNextTab($offset = 1) { $tab = $this->getRelativeTabHandle($offset); $this->webDriver->switchTo()->window($tab); } /** * Switches to previous browser tab. * An offset can be specified. * * ```php * <?php * // switch to previous tab * $I->switchToPreviousTab(); * // switch to 2nd previous tab * $I->switchToPreviousTab(2); * ``` * @param int $offset 1 */ public function switchToPreviousTab($offset = 1) { $this->switchToNextTab(0 - $offset); } protected function getRelativeTabHandle($offset) { if ($this->isPhantom()) { throw new ModuleException($this, "PhantomJS doesn't support tab actions"); } $handle = $this->webDriver->getWindowHandle(); $handles = $this->webDriver->getWindowHandles(); $idx = array_search($handle, $handles); return $handles[($idx + $offset) % count($handles)]; } /** * Waits for element and runs a sequence of actions inside its context. * Actions can be defined with array, callback, or `Codeception\Util\ActionSequence` instance. * * Actions as array are recommended for simple to combine "waitForElement" with assertions; * `waitForElement($el)` and `see('text', $el)` can be simplified to: * * ```php * <?php * $I->performOn($el, ['see' => 'text']); * ``` * * List of actions can be pragmatically build using `Codeception\Util\ActionSequence`: * * ```php * <?php * $I->performOn('.model', ActionSequence::build() * ->see('Warning') * ->see('Are you sure you want to delete this?') * ->click('Yes') * ); * ``` * * Actions executed from array or ActionSequence will print debug output for actions, and adds an action name to * exception on failure. * * Whenever you need to define more actions a callback can be used. A WebDriver module is passed for argument: * * ```php * <?php * $I->performOn('.rememberMe', function (WebDriver $I) { * $I->see('Remember me next time'); * $I->seeElement('#LoginForm_rememberMe'); * $I->dontSee('Login'); * }); * ``` * * In 3rd argument you can set number a seconds to wait for element to appear * * @param $element * @param $actions * @param int $timeout */ public function performOn($element, $actions, $timeout = 10) { $this->waitForElement($element, $timeout); $this->setBaseElement($element); $this->debugSection('InnerText', $this->getBaseElement()->getText()); if (is_callable($actions)) { $actions($this); $this->setBaseElement(); return; } if (is_array($actions)) { $actions = ActionSequence::build()->fromArray($actions); } if (!$actions instanceof ActionSequence) { throw new \InvalidArgumentException("2nd parameter, actions should be callback, ActionSequence or array"); } $actions->run($this); $this->setBaseElement(); } protected function setBaseElement($element = null) { if ($element === null) { $this->baseElement = $this->webDriver; return; } $this->baseElement = $this->matchFirstOrFail($this->webDriver, $element); } protected function enableImplicitWait() { if (!$this->config['wait']) { return; } $this->webDriver->manage()->timeouts()->implicitlyWait($this->config['wait']); } protected function disableImplicitWait() { if (!$this->config['wait']) { return; } $this->webDriver->manage()->timeouts()->implicitlyWait(0); } /** * From symfony/dom-crawler * * Converts string for XPath expressions. * * Escaped characters are: quotes (") and apostrophe ('). * * Examples: * * echo static::xpathLiteral('foo " bar'); * //prints 'foo " bar' * * echo static::xpathLiteral("foo ' bar"); * //prints "foo ' bar" * * echo static::xpathLiteral('a\'b"c'); * //prints concat('a', "'", 'b"c') * * @return string Converted string */ private static function xpathLiteral($s) { if (false === strpos($s, "'")) { return sprintf("'%s'", $s); } if (false === strpos($s, '"')) { return sprintf('"%s"', $s); } $string = $s; $parts = []; while (true) { if (false !== $pos = strpos($string, "'")) { $parts[] = sprintf("'%s'", substr($string, 0, $pos)); $parts[] = "\"'\""; $string = substr($string, $pos + 1); } else { $parts[] = "'$string'"; break; } } return sprintf('concat(%s)', implode(', ', $parts)); } } module-webdriver/src/Codeception/Module/.htaccess000077700000000177151323602320016077 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>module-webdriver/src/.htaccess000077700000000177151323602320012416 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>module-webdriver/LICENSE000077700000002116151323602320011031 0ustar00The MIT License (MIT) Copyright (c) 2011 Michael Bodnarchuk and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. module-webdriver/composer.json000077700000001546151323602320012554 0ustar00{ "name":"codeception/module-webdriver", "description":"WebDriver module for Codeception", "keywords":["codeception", "browser-testing", "acceptance-testing"], "homepage":"http://codeception.com/", "type":"library", "license":"MIT", "authors":[ { "name": "Michael Bodnarchuk" }, { "name": "Gintautas Miselis" }, { "name": "Zaahid Bateson" } ], "minimum-stability": "RC", "require": { "php": ">=5.6.0 <9.0", "codeception/codeception": "^4.0", "php-webdriver/webdriver": "^1.8.0" }, "suggest": { "codeception/phpbuiltinserver": "Start and stop PHP built-in web server for your tests" }, "autoload":{ "classmap": ["src/"] }, "config": { "classmap-authoritative": true } } module-webdriver/.htaccess000077700000000177151323602320011627 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/codecept000077700000000123151323602320010551 0ustar00#!/usr/bin/env php <?php /** * Codeception CLI */ require __DIR__ . '/app.php'; codeception/src/Codeception/Coverage/PhpCodeCoverageFactory.php000077700000001213151323602320020656 0ustar00<?php namespace Codeception\Coverage; use SebastianBergmann\CodeCoverage\CodeCoverage; use SebastianBergmann\CodeCoverage\Driver\Driver; use SebastianBergmann\CodeCoverage\Filter as CodeCoverageFilter; class PhpCodeCoverageFactory { public static function build() { if (method_exists(Driver::class, 'forLineCoverage')) { //php-code-coverage 9+ $filter = new CodeCoverageFilter(); $driver = Driver::forLineCoverage($filter); return new CodeCoverage($driver, $filter); } else { //php-code-coverage 8 or older return new CodeCoverage(); } } } codeception/src/Codeception/Coverage/Subscriber/RemoteServer.php000077700000005652151323602320021070 0ustar00<?php namespace Codeception\Coverage\Subscriber; use Codeception\Configuration; use Codeception\Event\SuiteEvent; use Codeception\Util\FileSystem; /** * When collecting code coverage on remote server * data is retrieved over HTTP and not merged with the local code coverage results. * * Class RemoteServer * @package Codeception\Coverage\Subscriber */ class RemoteServer extends LocalServer { public function isEnabled() { return $this->module and $this->settings['remote'] and $this->settings['enabled']; } public function afterSuite(SuiteEvent $e) { if (!$this->isEnabled()) { return; } $suite = strtr($e->getSuite()->getName(), ['\\' => '.']); if ($this->options['coverage-xml']) { $this->retrieveAndPrintXml($suite); } if ($this->options['coverage-html']) { $this->retrieveAndPrintHtml($suite); } if ($this->options['coverage-crap4j']) { $this->retrieveAndPrintCrap4j($suite); } if ($this->options['coverage-cobertura']) { $this->retrieveAndPrintCobertura($suite); } if ($this->options['coverage-phpunit']) { $this->retrieveAndPrintPHPUnit($suite); } } protected function retrieveAndPrintHtml($suite) { $tempFile = tempnam(sys_get_temp_dir(), 'C3') . '.tar'; file_put_contents($tempFile, $this->c3Request('html')); $destDir = Configuration::outputDir() . $suite . '.remote.coverage'; if (is_dir($destDir)) { FileSystem::doEmptyDir($destDir); } else { mkdir($destDir, 0777, true); } $phar = new \PharData($tempFile); $phar->extractTo($destDir); unlink($tempFile); } protected function retrieveAndPrintXml($suite) { $destFile = Configuration::outputDir() . $suite . '.remote.coverage.xml'; file_put_contents($destFile, $this->c3Request('clover')); } protected function retrieveAndPrintCrap4j($suite) { $destFile = Configuration::outputDir() . $suite . '.remote.crap4j.xml'; file_put_contents($destFile, $this->c3Request('crap4j')); } protected function retrieveAndPrintCobertura($suite) { $destFile = Configuration::outputDir() . $suite . '.remote.cobertura.xml'; file_put_contents($destFile, $this->c3Request('cobertura')); } protected function retrieveAndPrintPHPUnit($suite) { $tempFile = tempnam(sys_get_temp_dir(), 'C3') . '.tar'; file_put_contents($tempFile, $this->c3Request('phpunit')); $destDir = Configuration::outputDir() . $suite . '.remote.coverage-phpunit'; if (is_dir($destDir)) { FileSystem::doEmptyDir($destDir); } else { mkdir($destDir, 0777, true); } $phar = new \PharData($tempFile); $phar->extractTo($destDir); unlink($tempFile); } } codeception/src/Codeception/Coverage/Subscriber/Local.php000077700000002162151323602320017471 0ustar00<?php namespace Codeception\Coverage\Subscriber; use Codeception\Coverage\SuiteSubscriber; use Codeception\Event\SuiteEvent; use Codeception\Events; use Codeception\Lib\Interfaces\Remote; /** * Collects code coverage from unit and functional tests. * Results from all suites are merged. */ class Local extends SuiteSubscriber { public static $events = [ Events::SUITE_BEFORE => 'beforeSuite', Events::SUITE_AFTER => 'afterSuite', ]; /** * @var Remote */ protected $module; protected function isEnabled() { return $this->module === null and $this->settings['enabled']; } public function beforeSuite(SuiteEvent $e) { $this->applySettings($e->getSettings()); $this->module = $this->getServerConnectionModule($e->getSuite()->getModules()); if (!$this->isEnabled()) { return; } $this->applyFilter($e->getResult()); } public function afterSuite(SuiteEvent $e) { if (!$this->isEnabled()) { return; } $this->mergeToPrint($e->getResult()->getCodeCoverage()); } } codeception/src/Codeception/Coverage/Subscriber/LocalServer.php000077700000021477151323602320020672 0ustar00<?php namespace Codeception\Coverage\Subscriber; use Codeception\Configuration; use Codeception\Coverage\SuiteSubscriber; use Codeception\Event\StepEvent; use Codeception\Event\SuiteEvent; use Codeception\Event\TestEvent; use Codeception\Events; use Codeception\Exception\ModuleException; use Codeception\Exception\RemoteException; /** * When collecting code coverage data from local server HTTP requests are sent to c3.php file. * Coverage Collection is started by sending cookies/headers. * Result is taken from the local file and merged with local code coverage results. * * Class LocalServer * @package Codeception\Coverage\Subscriber */ class LocalServer extends SuiteSubscriber { // headers const COVERAGE_HEADER = 'X-Codeception-CodeCoverage'; const COVERAGE_HEADER_ERROR = 'X-Codeception-CodeCoverage-Error'; const COVERAGE_HEADER_CONFIG = 'X-Codeception-CodeCoverage-Config'; const COVERAGE_HEADER_SUITE = 'X-Codeception-CodeCoverage-Suite'; // cookie names const COVERAGE_COOKIE = 'CODECEPTION_CODECOVERAGE'; const COVERAGE_COOKIE_ERROR = 'CODECEPTION_CODECOVERAGE_ERROR'; protected $suiteName; protected $c3Access = [ 'http' => [ 'method' => "GET", 'header' => '' ] ]; /** * @var \Codeception\Lib\Interfaces\Web */ protected $module; public static $events = [ Events::SUITE_BEFORE => 'beforeSuite', Events::TEST_BEFORE => 'beforeTest', Events::STEP_AFTER => 'afterStep', Events::SUITE_AFTER => 'afterSuite', ]; protected function isEnabled() { return $this->module && !$this->settings['remote'] && $this->settings['enabled']; } public function beforeSuite(SuiteEvent $e) { $this->module = $this->getServerConnectionModule($e->getSuite()->getModules()); $this->applySettings($e->getSettings()); if (!$this->isEnabled()) { return; } $this->suiteName = $e->getSuite()->getBaseName(); if ($this->settings['remote_config']) { $this->addC3AccessHeader(self::COVERAGE_HEADER_CONFIG, $this->settings['remote_config']); $knock = $this->c3Request('clear'); if ($knock === false) { throw new RemoteException( ' CodeCoverage Error. Check the file "c3.php" is included in your application. We tried to access "/c3/report/clear" but this URI was not accessible. You can review actual error messages in c3tmp dir. ' ); } } } public function beforeTest(TestEvent $e) { if (!$this->isEnabled()) { return; } $this->startCoverageCollection($e->getTest()->getName()); } public function afterStep(StepEvent $e) { if (!$this->isEnabled()) { return; } $this->fetchErrors(); } public function afterSuite(SuiteEvent $e) { if (!$this->isEnabled()) { return; } $coverageFile = Configuration::outputDir() . 'c3tmp/codecoverage.serialized'; $retries = 5; while (!file_exists($coverageFile) && --$retries >= 0) { usleep(0.5 * 1000000); // 0.5 sec } if (!file_exists($coverageFile)) { if (file_exists(Configuration::outputDir() . 'c3tmp/error.txt')) { throw new \RuntimeException(file_get_contents(Configuration::outputDir() . 'c3tmp/error.txt')); } return; } $contents = file_get_contents($coverageFile); $coverage = @unserialize($contents); if ($coverage === false) { return; } $this->preProcessCoverage($coverage) ->mergeToPrint($coverage); } /** * Allows Translating Remote Paths To Local (IE: When Using Docker) * * @param \SebastianBergmann\CodeCoverage\CodeCoverage $coverage * @return $this */ protected function preProcessCoverage($coverage) { //Only Process If Work Directory Set if ($this->settings['work_dir'] === null) { return $this; } $workDir = rtrim($this->settings['work_dir'], '/\\') . DIRECTORY_SEPARATOR; $projectDir = Configuration::projectDir(); $data = $coverage->getData(true); //We only want covered files, not all whitelisted ones. codecept_debug("Replacing all instances of {$workDir} with {$projectDir}"); foreach ($data as $path => $datum) { unset($data[$path]); $path = str_replace($workDir, $projectDir, $path); $data[$path] = $datum; } $coverage->setData($data); return $this; } protected function c3Request($action) { $this->addC3AccessHeader(self::COVERAGE_HEADER, 'remote-access'); $context = stream_context_create($this->c3Access); $c3Url = $this->settings['c3_url'] ? $this->settings['c3_url'] : $this->module->_getUrl(); $contents = file_get_contents($c3Url . '/c3/report/' . $action, false, $context); $okHeaders = array_filter( $http_response_header, function ($h) { return preg_match('~^HTTP(.*?)\s200~', $h); } ); if (empty($okHeaders)) { throw new RemoteException("Request was not successful. See response header: " . $http_response_header[0]); } if ($contents === false) { $this->getRemoteError($http_response_header); } return $contents; } protected function startCoverageCollection($testName) { $value = [ 'CodeCoverage' => $testName, 'CodeCoverage_Suite' => $this->suiteName, 'CodeCoverage_Config' => $this->settings['remote_config'] ]; $value = json_encode($value); if ($this->module instanceof \Codeception\Module\WebDriver) { $this->module->amOnPage('/'); } $cookieDomain = isset($this->settings['cookie_domain']) ? $this->settings['cookie_domain'] : null; if (!$cookieDomain) { $c3Url = parse_url($this->settings['c3_url'] ? $this->settings['c3_url'] : $this->module->_getUrl()); // we need to separate coverage cookies by host; we can't separate cookies by port. $cookieDomain = isset($c3Url['host']) ? $c3Url['host'] : 'localhost'; } $this->module->setCookie(self::COVERAGE_COOKIE, $value, ['domain' => $cookieDomain]); // putting in configuration ensures the cookie is used for all sessions of a MultiSession test $cookies = $this->module->_getConfig('cookies'); if (!$cookies || !is_array($cookies)) { $cookies = []; } $found = false; foreach ($cookies as &$cookie) { if (!is_array($cookie) || !array_key_exists('Name', $cookie) || !array_key_exists('Value', $cookie)) { // \Codeception\Lib\InnerBrowser will complain about this continue; } if ($cookie['Name'] === self::COVERAGE_COOKIE) { $found = true; $cookie['Value'] = $value; break; } } unset($cookie); if (!$found) { $cookies[] = [ 'Name' => self::COVERAGE_COOKIE, 'Value' => $value ]; } $this->module->_setConfig(['cookies' => $cookies]); } protected function fetchErrors() { try { $error = $this->module->grabCookie(self::COVERAGE_COOKIE_ERROR); } catch (ModuleException $e) { // when a new session is started we can't get cookies because there is no // current page, but there can be no code coverage error either $error = null; } if (!empty($error)) { $this->module->resetCookie(self::COVERAGE_COOKIE_ERROR); throw new RemoteException($error); } } protected function getRemoteError($headers) { foreach ($headers as $header) { if (strpos($header, self::COVERAGE_HEADER_ERROR) === 0) { throw new RemoteException($header); } } } protected function addC3AccessHeader($header, $value) { $headerString = "$header: $value\r\n"; if (strpos($this->c3Access['http']['header'], $headerString) === false) { $this->c3Access['http']['header'] .= $headerString; } } protected function applySettings($settings) { parent::applySettings($settings); if (isset($settings['coverage']['remote_context_options'])) { $this->c3Access = array_replace_recursive($this->c3Access, $settings['coverage']['remote_context_options']); } } } codeception/src/Codeception/Coverage/Subscriber/.htaccess000077700000000177151323602320017530 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Coverage/Subscriber/Printer.php000077700000013463151323602320020070 0ustar00<?php namespace Codeception\Coverage\Subscriber; use Codeception\Configuration; use Codeception\Coverage\Filter; use Codeception\Coverage\PhpCodeCoverageFactory; use Codeception\Event\PrintResultEvent; use Codeception\Events; use Codeception\Exception\ConfigurationException; use Codeception\Subscriber\Shared\StaticEvents; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class Printer implements EventSubscriberInterface { use StaticEvents; public static $events = [ Events::RESULT_PRINT_AFTER => 'printResult' ]; protected $settings = [ 'enabled' => true, 'low_limit' => '35', 'high_limit' => '70', 'show_uncovered' => false, 'show_only_summary' => false ]; public static $coverage; protected $options; protected $logDir; protected $destination = []; public function __construct($options) { $this->options = $options; $this->logDir = Configuration::outputDir(); $this->settings = array_merge($this->settings, Configuration::config()['coverage']); self::$coverage = PhpCodeCoverageFactory::build(); // Apply filter $filter = new Filter(self::$coverage); $filter ->whiteList(Configuration::config()) ->blackList(Configuration::config()); } protected function absolutePath($path) { if ((strpos($path, '/') === 0) || (strpos($path, ':') === 1)) { // absolute path return $path; } return $this->logDir . $path; } public function printResult(PrintResultEvent $e) { $printer = $e->getPrinter(); if (!$this->settings['enabled']) { $printer->write("\nCodeCoverage is disabled in `codeception.yml` config\n"); return; } if (!$this->options['quiet']) { $this->printConsole($printer); } $printer->write("Remote CodeCoverage reports are not printed to console\n"); $this->printPHP(); $printer->write("\n"); if ($this->options['coverage-html']) { $this->printHtml(); $printer->write("HTML report generated in {$this->options['coverage-html']}\n"); } if ($this->options['coverage-xml']) { $this->printXml(); $printer->write("XML report generated in {$this->options['coverage-xml']}\n"); } if ($this->options['coverage-text']) { $this->printText(); $printer->write("Text report generated in {$this->options['coverage-text']}\n"); } if ($this->options['coverage-crap4j']) { $this->printCrap4j(); $printer->write("Crap4j report generated in {$this->options['coverage-crap4j']}\n"); } if ($this->options['coverage-cobertura']) { $this->printCobertura(); $printer->write("Cobertura report generated in {$this->options['coverage-cobertura']}\n"); } if ($this->options['coverage-phpunit']) { $this->printPHPUnit(); $printer->write("PHPUnit report generated in {$this->options['coverage-phpunit']}\n"); } } protected function printConsole(\PHPUnit\Util\Printer $printer) { $writer = new \SebastianBergmann\CodeCoverage\Report\Text( $this->settings['low_limit'], $this->settings['high_limit'], $this->settings['show_uncovered'], $this->settings['show_only_summary'] ); $printer->write($writer->process(self::$coverage, $this->options['colors'])); } protected function printHtml() { $writer = new \SebastianBergmann\CodeCoverage\Report\Html\Facade( $this->settings['low_limit'], $this->settings['high_limit'], sprintf( ', <a href="http://codeception.com">Codeception</a> and <a href="http://phpunit.de/">PHPUnit %s</a>', \PHPUnit\Runner\Version::id() ) ); $writer->process(self::$coverage, $this->absolutePath($this->options['coverage-html'])); } protected function printXml() { $writer = new \SebastianBergmann\CodeCoverage\Report\Clover(); $writer->process(self::$coverage, $this->absolutePath($this->options['coverage-xml'])); } protected function printPHP() { $writer = new \SebastianBergmann\CodeCoverage\Report\PHP; $writer->process(self::$coverage, $this->absolutePath($this->options['coverage'])); } protected function printText() { $writer = new \SebastianBergmann\CodeCoverage\Report\Text( $this->settings['low_limit'], $this->settings['high_limit'], $this->settings['show_uncovered'], $this->settings['show_only_summary'] ); file_put_contents( $this->absolutePath($this->options['coverage-text']), $writer->process(self::$coverage, false) ); } protected function printCrap4j() { $writer = new \SebastianBergmann\CodeCoverage\Report\Crap4j; $writer->process(self::$coverage, $this->absolutePath($this->options['coverage-crap4j'])); } protected function printCobertura() { if (!class_exists(\SebastianBergmann\CodeCoverage\Report\Cobertura::class)) { throw new ConfigurationException("Cobertura report requires php-code-coverage >= 9.2"); } $writer = new \SebastianBergmann\CodeCoverage\Report\Cobertura; $writer->process(self::$coverage, $this->absolutePath($this->options['coverage-cobertura'])); } protected function printPHPUnit() { $writer = new \SebastianBergmann\CodeCoverage\Report\Xml\Facade(\PHPUnit\Runner\Version::id()); $writer->process(self::$coverage, $this->absolutePath($this->options['coverage-phpunit'])); } } codeception/src/Codeception/Coverage/SuiteSubscriber.php000077700000006424151323602320017456 0ustar00<?php namespace Codeception\Coverage; use Codeception\Configuration; use Codeception\Coverage\Subscriber\Printer; use Codeception\Lib\Interfaces\Remote; use Codeception\Stub; use Codeception\Subscriber\Shared\StaticEvents; use PHPUnit\Framework\CodeCoverageException; use SebastianBergmann\CodeCoverage\CodeCoverage; use SebastianBergmann\CodeCoverage\Filter as CodeCoverageFilter; use Symfony\Component\EventDispatcher\EventSubscriberInterface; abstract class SuiteSubscriber implements EventSubscriberInterface { use StaticEvents; protected $defaultSettings = [ 'enabled' => false, 'remote' => false, 'local' => false, 'xdebug_session' => 'codeception', 'remote_config' => null, 'show_uncovered' => false, 'c3_url' => null, 'work_dir' => null, 'cookie_domain' => null, ]; protected $settings = []; protected $filters = []; protected $modules = []; protected $coverage; protected $logDir; protected $options; public static $events = []; abstract protected function isEnabled(); public function __construct($options = []) { $this->options = $options; $this->logDir = Configuration::outputDir(); } protected function applySettings($settings) { try { $this->coverage = PhpCodeCoverageFactory::build(); } catch (CodeCoverageException $e) { throw new \Exception( 'XDebug is required to collect CodeCoverage. Please install xdebug extension and enable it in php.ini' ); } $this->filters = $settings; $this->settings = $this->defaultSettings; $keys = array_keys($this->defaultSettings); foreach ($keys as $key) { if (isset($settings['coverage'][$key])) { $this->settings[$key] = $settings['coverage'][$key]; } } if (method_exists($this->coverage, 'setProcessUncoveredFilesFromWhitelist')) { //php-code-coverage 8 or older $this->coverage->setProcessUncoveredFilesFromWhitelist($this->settings['show_uncovered']); } else { //php-code-coverage 9+ if ($this->settings['show_uncovered']) { $this->coverage->processUncoveredFiles(); } else { $this->coverage->doNotProcessUncoveredFiles(); } } } /** * @param array $modules * @return \Codeception\Lib\Interfaces\Remote|null */ protected function getServerConnectionModule(array $modules) { foreach ($modules as $module) { if ($module instanceof Remote) { return $module; } } return null; } public function applyFilter(\PHPUnit\Framework\TestResult $result) { $driver = Stub::makeEmpty('SebastianBergmann\CodeCoverage\Driver\Driver'); $result->setCodeCoverage(new CodeCoverage($driver, new CodeCoverageFilter())); Filter::setup($this->coverage) ->whiteList($this->filters) ->blackList($this->filters); $result->setCodeCoverage($this->coverage); } protected function mergeToPrint($coverage) { Printer::$coverage->merge($coverage); } } codeception/src/Codeception/Coverage/.htaccess000077700000000177151323602320015425 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Coverage/Filter.php000077700000014323151323602320015563 0ustar00<?php namespace Codeception\Coverage; use Codeception\Configuration; use Codeception\Exception\ConfigurationException; use Codeception\Exception\ModuleException; use SebastianBergmann\CodeCoverage\CodeCoverage; use Symfony\Component\Finder\Exception\DirectoryNotFoundException; use Symfony\Component\Finder\Finder; class Filter { /** * @var CodeCoverage */ protected $phpCodeCoverage = null; /** * @var Filter */ protected static $c3; /** * @var \SebastianBergmann\CodeCoverage\Filter */ protected $filter = null; public function __construct(CodeCoverage $phpCoverage) { $this->phpCodeCoverage = $phpCoverage; $this->filter = $this->phpCodeCoverage->filter(); } /** * @param CodeCoverage $phpCoverage * @return Filter */ public static function setup(CodeCoverage $phpCoverage) { self::$c3 = new self($phpCoverage); return self::$c3; } /** * @return null|CodeCoverage */ public function getPhpCodeCoverage() { return $this->phpCodeCoverage; } /** * @param $config * @return Filter */ public function whiteList($config) { $filter = $this->filter; if (!isset($config['coverage'])) { return $this; } $coverage = $config['coverage']; if (!isset($coverage['whitelist'])) { $coverage['whitelist'] = []; if (isset($coverage['include'])) { $coverage['whitelist']['include'] = $coverage['include']; } if (isset($coverage['exclude'])) { $coverage['whitelist']['exclude'] = $coverage['exclude']; } } if (isset($coverage['whitelist']['include'])) { if (!is_array($coverage['whitelist']['include'])) { throw new ConfigurationException('Error parsing yaml. Config `whitelist: include:` should be an array'); } foreach ($coverage['whitelist']['include'] as $fileOrDir) { $finder = strpos($fileOrDir, '*') === false ? [Configuration::projectDir() . DIRECTORY_SEPARATOR . $fileOrDir] : $this->matchWildcardPattern($fileOrDir); foreach ($finder as $file) { if (method_exists($filter, 'addFileToWhitelist')) { //php-code-coverage 8 or older $filter->addFileToWhitelist($file); } else { //php-code-coverage 9+ $filter->includeFile($file); } } } } if (isset($coverage['whitelist']['exclude'])) { if (!is_array($coverage['whitelist']['exclude'])) { throw new ConfigurationException('Error parsing yaml. Config `whitelist: exclude:` should be an array'); } foreach ($coverage['whitelist']['exclude'] as $fileOrDir) { try { $finder = strpos($fileOrDir, '*') === false ? [Configuration::projectDir() . DIRECTORY_SEPARATOR . $fileOrDir] : $this->matchWildcardPattern($fileOrDir); foreach ($finder as $file) { if (method_exists($filter, 'removeFileFromWhitelist')) { //php-code-coverage 8 or older $filter->removeFileFromWhitelist($file); } else { //php-code-coverage 9+ $filter->excludeFile($file); } } } catch (DirectoryNotFoundException $e) { continue; } } } return $this; } /** * @param $config * @return Filter */ public function blackList($config) { $filter = $this->filter; if (!isset($config['coverage'])) { return $this; } $coverage = $config['coverage']; if (isset($coverage['blacklist'])) { if (!method_exists($filter, 'addFileToBlacklist')) { throw new ModuleException($this, 'The blacklist functionality has been removed from PHPUnit 5,' . ' please remove blacklist section from configuration.'); } if (isset($coverage['blacklist']['include'])) { foreach ($coverage['blacklist']['include'] as $fileOrDir) { $finder = strpos($fileOrDir, '*') === false ? [Configuration::projectDir() . DIRECTORY_SEPARATOR . $fileOrDir] : $this->matchWildcardPattern($fileOrDir); foreach ($finder as $file) { $filter->addFileToBlacklist($file); } } } if (isset($coverage['blacklist']['exclude'])) { foreach ($coverage['blacklist']['exclude'] as $fileOrDir) { $finder = strpos($fileOrDir, '*') === false ? [Configuration::projectDir() . DIRECTORY_SEPARATOR . $fileOrDir] : $this->matchWildcardPattern($fileOrDir); foreach ($finder as $file) { $filter->removeFileFromBlacklist($file); } } } } return $this; } protected function matchWildcardPattern($pattern) { $finder = Finder::create(); $fileOrDir = str_replace('\\', '/', $pattern); $parts = explode('/', $fileOrDir); $file = array_pop($parts); $finder->name($file); if (count($parts)) { $last_path = array_pop($parts); if ($last_path === '*') { $finder->in(Configuration::projectDir() . implode('/', $parts)); } else { $finder->in(Configuration::projectDir() . implode('/', $parts) . '/' . $last_path); } } $finder->ignoreVCS(true)->files(); return $finder; } /** * @return \SebastianBergmann\CodeCoverage\Filter */ public function getFilter() { return $this->filter; } } codeception/src/Codeception/Step.php000077700000024317151323602320013522 0ustar00<?php namespace Codeception; use Codeception\Lib\ModuleContainer; use Codeception\Step\Argument\FormattedOutput; use Codeception\Step\Meta as MetaStep; use Codeception\Util\Locator; use PHPUnit\Framework\MockObject\MockObject; abstract class Step { const DEFAULT_MAX_LENGTH = 200; const STACK_POSITION = 3; /** * @var string */ protected $action; /** * @var array */ protected $arguments; protected $debugOutput; public $executed = false; protected $line = null; protected $file = null; protected $prefix = 'I'; /** * @var MetaStep */ protected $metaStep = null; protected $failed = false; protected $isTry = false; public function __construct($action, array $arguments = []) { $this->action = $action; $this->arguments = $arguments; } public function saveTrace() { $stack = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT); if (count($stack) <= self::STACK_POSITION) { return; } $traceLine = $stack[self::STACK_POSITION - 1]; if (!isset($traceLine['file'])) { return; } $this->file = $traceLine['file']; $this->line = $traceLine['line']; $this->addMetaStep($traceLine, $stack); } private function isTestFile($file) { return preg_match('~[^\\'.DIRECTORY_SEPARATOR.'](Cest|Cept|Test).php$~', $file); } public function getName() { $class = explode('\\', __CLASS__); return end($class); } public function getAction() { return $this->action; } public function getLine() { if ($this->line && $this->file) { return codecept_relative_path($this->file) . ':' . $this->line; } } public function hasFailed() { return $this->failed; } public function getArguments() { return $this->arguments; } public function getArgumentsAsString($maxLength = self::DEFAULT_MAX_LENGTH) { $arguments = $this->arguments; $argumentCount = count($arguments); $totalLength = $argumentCount - 1; // count separators before adding length of individual arguments foreach ($arguments as $key => $argument) { $stringifiedArgument = $this->stringifyArgument($argument); $arguments[$key] = $stringifiedArgument; $totalLength += mb_strlen($stringifiedArgument, 'utf-8'); } if ($totalLength > $maxLength && $maxLength > 0) { //sort arguments from shortest to longest uasort($arguments, function ($arg1, $arg2) { $length1 = mb_strlen($arg1, 'utf-8'); $length2 = mb_strlen($arg2, 'utf-8'); if ($length1 === $length2) { return 0; } return ($length1 < $length2) ? -1 : 1; }); $allowedLength = floor(($maxLength - $argumentCount + 1) / $argumentCount); $lengthRemaining = $maxLength; $argumentsRemaining = $argumentCount; foreach ($arguments as $key => $argument) { $argumentsRemaining--; if (mb_strlen($argument, 'utf-8') > $allowedLength) { $arguments[$key] = mb_substr($argument, 0, $allowedLength - 4, 'utf-8') . '...' . mb_substr($argument, -1, 1, 'utf-8'); $lengthRemaining -= ($allowedLength + 1); } else { $lengthRemaining -= (mb_strlen($arguments[$key], 'utf-8') + 1); //recalculate allowed length because this argument was short if ($argumentsRemaining > 0) { $allowedLength = floor(($lengthRemaining - $argumentsRemaining + 1) / $argumentsRemaining); } } } //restore original order of arguments ksort($arguments); } return implode(',', $arguments); } protected function stringifyArgument($argument) { if (is_string($argument)) { return '"' . strtr($argument, ["\n" => '\n', "\r" => '\r', "\t" => ' ']) . '"'; } elseif (is_resource($argument)) { $argument = (string)$argument; } elseif (is_array($argument)) { foreach ($argument as $key => $value) { if (is_object($value)) { $argument[$key] = $this->getClassName($value); } } } elseif (is_object($argument)) { if ($argument instanceof FormattedOutput) { $argument = $argument->getOutput(); } elseif (method_exists($argument, '__toString')) { $argument = (string)$argument; } elseif (get_class($argument) == 'Facebook\WebDriver\WebDriverBy') { $argument = Locator::humanReadableString($argument); } else { $argument = $this->getClassName($argument); } } $arg_str = json_encode($argument, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $arg_str = str_replace('\"', '"', $arg_str); return $arg_str; } protected function getClassName($argument) { if ($argument instanceof \Closure) { return 'Closure'; } elseif ($argument instanceof MockObject && isset($argument->__mocked)) { return $this->formatClassName($argument->__mocked); } return $this->formatClassName(get_class($argument)); } protected function formatClassName($classname) { return trim($classname, "\\"); } public function getPhpCode($maxLength) { $result = "\${$this->prefix}->" . $this->getAction() . '('; $maxLength = $maxLength - mb_strlen($result, 'utf-8') - 1; $result .= $this->getHumanizedArguments($maxLength) .')'; return $result; } /** * @return MetaStep */ public function getMetaStep() { return $this->metaStep; } public function __toString() { $humanizedAction = $this->humanize($this->getAction()); return $humanizedAction . ' ' . $this->getHumanizedArguments(); } public function toString($maxLength) { $humanizedAction = $this->humanize($this->getAction()); $maxLength = $maxLength - mb_strlen($humanizedAction, 'utf-8') - 1; return $humanizedAction . ' ' . $this->getHumanizedArguments($maxLength); } public function getHtml($highlightColor = '#732E81') { if (empty($this->arguments)) { return sprintf('%s %s', ucfirst($this->prefix), $this->humanize($this->getAction())); } return sprintf('%s %s <span style="color: %s">%s</span>', ucfirst($this->prefix), htmlspecialchars($this->humanize($this->getAction())), $highlightColor, htmlspecialchars($this->getHumanizedArguments(0))); } public function getHumanizedActionWithoutArguments() { return $this->humanize($this->getAction()); } public function getHumanizedArguments($maxLength = self::DEFAULT_MAX_LENGTH) { return $this->getArgumentsAsString($maxLength); } protected function clean($text) { return str_replace('\/', '', $text); } protected function humanize($text) { $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('~\bdont\b~', 'don\'t', $text); return mb_strtolower($text, 'UTF-8'); } public function run(ModuleContainer $container = null) { $this->executed = true; if (!$container) { return null; } $activeModule = $container->moduleForAction($this->action); if (!is_callable([$activeModule, $this->action])) { throw new \RuntimeException("Action '{$this->action}' can't be called"); } try { $res = call_user_func_array([$activeModule, $this->action], $this->arguments); } catch (\Exception $e) { if ($this->isTry) { throw $e; } $this->failed = true; if ($this->getMetaStep()) { $this->getMetaStep()->setFailed(true); } throw $e; } return $res; } /** * If steps are combined into one method they can be reproduced as meta-step. * We are using stack trace to analyze if steps were called from test, if not - they were called from meta-step. * * @param $step * @param $stack */ protected function addMetaStep($step, $stack) { if (($this->isTestFile($this->file)) || ($step['class'] == 'Codeception\Scenario')) { return; } $i = count($stack) - self::STACK_POSITION - 1; // get into test file and retrieve its actual call while (isset($stack[$i])) { $step = $stack[$i]; $i--; if (!isset($step['file']) or !isset($step['function']) or !isset($step['class'])) { continue; } if (!$this->isTestFile($step['file'])) { continue; } // in case arguments were passed by reference, copy args array to ensure dereference. array_values() does not dereference values $this->metaStep = new Step\Meta($step['function'], array_map(function ($i) { return $i; }, array_values($step['args']))); $this->metaStep->setTraceInfo($step['file'], $step['line']); // pageobjects or other classes should not be included with "I" if (!in_array('Codeception\Actor', class_parents($step['class']))) { if (isset($step['object'])) { $this->metaStep->setPrefix(get_class($step['object']) . ':'); return; } $this->metaStep->setPrefix($step['class'] . ':'); } return; } } /** * @param MetaStep $metaStep */ public function setMetaStep($metaStep) { $this->metaStep = $metaStep; } /** * @return string */ public function getPrefix() { return $this->prefix . ' '; } } codeception/src/Codeception/Test/Loader/Gherkin.php000077700000017177151323602320016331 0ustar00<?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'], '\\') . '\\' . rtrim($this->settings['actor'], '\\') : $this->settings['actor']; if ($actorContext) { $contexts['default'][] = $actorContext; } } if (isset($this->settings['gherkin']['contexts']['path']) && isset($this->settings['gherkin']['contexts']['namespace_prefix'])) { $files = glob($this->settings['gherkin']['contexts']['path'] . '/*/*.php'); // Strip off include path $files = str_replace([$this->settings['gherkin']['contexts']['path'], '.php', '/'], ['', '', '\\'], $files); // Add namespace prefix $namespace = $this->settings['gherkin']['contexts']['namespace_prefix']; $dynamicContexts = array_map(function ($path) use ($namespace) { return $namespace . $path; }, $files); $this->addSteps($dynamicContexts, 'default'); } $this->addSteps($contexts['default']); } protected function addSteps(array $contexts, $group = 'default') { if (!isset($this->steps[$group])) { $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; } } codeception/src/Codeception/Test/Loader/Cept.php000077700000001023151323602320015614 0ustar00<?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; } } codeception/src/Codeception/Test/Loader/Unit.php000077700000005231151323602320015645 0ustar00<?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 (method_exists(\PHPUnit\Framework\TestSuite::class, 'isTestMethod')) { //PHPUnit <8.2 if (!\PHPUnit\Framework\TestSuite::isTestMethod($method)) { return; } $test = \PHPUnit\Framework\TestSuite::createTest($class, $method->name); } elseif (method_exists(\PHPUnit\Util\Test::class, 'isTestMethod')) { //PHPUnit >=8.2 if (!\PHPUnit\Util\Test::isTestMethod($method)) { return; } $test = (new \PHPUnit\Framework\TestBuilder)->build($class, $method->name); } else { throw new \Exception('Unsupported version of PHPUnit, where is isTestMethod method?'); } 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)); } } } codeception/src/Codeception/Test/Loader/LoaderInterface.php000077700000000266151323602320017760 0ustar00<?php namespace Codeception\Test\Loader; interface LoaderInterface { public function loadTests($filename); public function getTests(); public function getPattern(); } codeception/src/Codeception/Test/Loader/Cest.php000077700000007375151323602320015637 0ustar00<?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); foreach ($data as $example) { $examples[] = $example; } } 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); } } } } codeception/src/Codeception/Test/Loader/.htaccess000077700000000177151323602320016017 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Test/Gherkin.php000077700000017036151323602320015115 0ustar00<?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($scenarioNode->getTitle()); $this->getMetadata()->setFeature($featureNode->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->getScenarioTitle(); } 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 .= ' ""'; } $matches = []; foreach ($this->steps as $pattern => $context) { $res = preg_match($pattern, $stepText); if (!$res) { continue; } $matches[$pattern] = $context; } if (count($matches) === 0) { // 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) ); } } private function contextAsString($context) { if (is_array($context) && count($context) === 2) { list($class, $method) = $context; if (is_string($class) && is_string($method)) { return $class . ':' . $method; } } return var_export($context, true); } 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->getFeature() . ': ' . $this->getScenarioTitle(); } public function getFeature() { return $this->getMetadata()->getFeature(); } public function getScenarioTitle() { return $this->getMetadata()->getName(); } /** * @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() ]; } } codeception/src/Codeception/Test/Test.php000077700000010204151323602320014433 0ustar00<?php namespace Codeception\Test; use Codeception\TestInterface; use Codeception\Util\ReflectionHelper; use SebastianBergmann\Timer\Duration; 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; $timer = null; if (class_exists(Duration::class)) { $timer = new Timer(); } $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) { if (null !== $timer) { $timer->start(); } else { 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; } if (null !== $timer) { $time = $timer->stop()->asSeconds(); } else { $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; } } codeception/src/Codeception/Test/Cept.php000077700000004062151323602320014414 0ustar00<?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(); } } codeception/src/Codeception/Test/Unit.php000077700000007646151323602320014453 0ustar00<?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 \Codeception\PHPUnit\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() { } /** * @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; } } codeception/src/Codeception/Test/Loader.php000077700000007536151323602320014740 0ustar00<?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()); } } } codeception/src/Codeception/Test/Cest.php000077700000016222151323602320014420 0ustar00<?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); } } codeception/src/Codeception/Test/Interfaces/Dependent.php000077700000000157151323602320017513 0ustar00<?php namespace Codeception\Test\Interfaces; interface Dependent { public function fetchDependencies(); } codeception/src/Codeception/Test/Interfaces/StrictCoverage.php000077700000000240151323602320020522 0ustar00<?php namespace Codeception\Test\Interfaces; interface StrictCoverage { public function getLinesToBeCovered(); public function getLinesToBeUsed(); } codeception/src/Codeception/Test/Interfaces/ScenarioDriven.php000077700000000504151323602320020514 0ustar00<?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(); } codeception/src/Codeception/Test/Interfaces/Descriptive.php000077700000000272151323602320020064 0ustar00<?php namespace Codeception\Test\Interfaces; interface Descriptive extends \PHPUnit\Framework\SelfDescribing { public function getFileName(); public function getSignature(); } codeception/src/Codeception/Test/Interfaces/Reported.php000077700000000305151323602320017364 0ustar00<?php namespace Codeception\Test\Interfaces; interface Reported { /** * Field values for XML/JSON/TAP reports * * @return array */ public function getReportFields(); } codeception/src/Codeception/Test/Interfaces/Plain.php000077700000000156151323602320016647 0ustar00<?php namespace Codeception\Test\Interfaces; /** * TestCases that do not follow OOP */ interface Plain { } codeception/src/Codeception/Test/Interfaces/.htaccess000077700000000177151323602320016674 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Test/Feature/AssertionCounter.php000077700000001242151323602320020420 0ustar00<?php namespace Codeception\Test\Feature; trait AssertionCounter { protected $numAssertions = 0; public function getNumAssertions() { return $this->numAssertions; } /** * This method is not covered by the backward compatibility promise * for PHPUnit, but is nice to have for extensions. */ public function addToAssertionCount($count) { $this->numAssertions += $count; } protected function assertionCounterStart() { \PHPUnit\Framework\Assert::resetCount(); } protected function assertionCounterEnd() { $this->numAssertions = \PHPUnit\Framework\Assert::getCount(); } } codeception/src/Codeception/Test/Feature/ScenarioLoader.php000077700000002103151323602320020000 0ustar00<?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(); } codeception/src/Codeception/Test/Feature/IgnoreIfMetadataBlocked.php000077700000001775151323602320021553 0ustar00<?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; } } } codeception/src/Codeception/Test/Feature/CodeCoverage.php000077700000002401151323602320017435 0ustar00<?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); } } } } codeception/src/Codeception/Test/Feature/.htaccess000077700000000177151323602320016204 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Test/Feature/ErrorLogger.php000077700000001222151323602320017340 0ustar00<?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); } } } codeception/src/Codeception/Test/Feature/MetadataCollector.php000077700000001031151323602320020474 0ustar00<?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(); } } codeception/src/Codeception/Test/Descriptor.php000077700000010062151323602320015634 0ustar00<?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) { $env = ''; $example = ''; if (method_exists($testCase, 'getScenario') && !empty($testCase->getScenario()->current('env')) ) { $env = ':' . $testCase->getScenario()->current('env'); } if (method_exists($testCase, 'getMetaData') && !empty($testCase->getMetadata()->getCurrent('example')) ) { $example = ':' . substr(sha1(json_encode($testCase->getMetadata()->getCurrent('example'))), 0, 7); } return self::getTestSignature($testCase) . $env . $example; } public static function getTestAsString(\PHPUnit\Framework\SelfDescribing $testCase) { if ($testCase instanceof \PHPUnit\Framework\TestCase) { $text = self::getTestCaseNameAsString($testCase->getName()); return ReflectionHelper::getClassShortName($testCase) . ': ' . $text; } return $testCase->toString(); } /** * @param string $testCaseName * @return string */ public static function getTestCaseNameAsString($testCaseName) { $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 = ucfirst(strtolower($text)); $text = str_replace(['::', 'with data set'], [':', '|'], $text); return $text; } /** * 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; } } codeception/src/Codeception/Test/.htaccess000077700000000177151323602320014611 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Test/Metadata.php000077700000011744151323602320015246 0ustar00<?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); } } codeception/src/Codeception/Snapshot.php000077700000010515151323602320014401 0ustar00<?php namespace Codeception; use Codeception\Exception\ContentNotFound; use Codeception\Util\Debug; use Codeception\Util\Shared\Asserts; use PHPUnit\Framework\AssertionFailedError; abstract class Snapshot { use Asserts; protected $fileName; protected $dataSet; protected $refresh; protected $showDiff = false; protected $saveAsJson = true; protected $extension = 'json'; /** * Should return data from current test run * * @return mixed */ abstract protected function fetchData(); /** * Performs assertion on saved data set against current dataset. * Can be overridden to implement custom assertion * * @param $data */ protected function assertData($data) { $this->assertEquals($this->dataSet, $data, 'Snapshot doesn\'t match real data'); } /** * Loads data set from file. */ protected function load() { if (!file_exists($this->getFileName())) { return; } $fileContents = file_get_contents($this->getFileName()); if ($this->saveAsJson) { $fileContents = json_decode($fileContents); } $this->dataSet = $fileContents; if (!$this->dataSet) { throw new ContentNotFound("Loaded snapshot is empty"); } } /** * Saves data set to file */ protected function save() { $fileContents = $this->dataSet; if ($this->saveAsJson) { $fileContents = json_encode($fileContents); } file_put_contents($this->getFileName(), $fileContents); } /** * If no filename is defined, generates one from class name * * @return string */ protected function getFileName() { if (!$this->fileName) { $this->fileName = preg_replace('/\W/', '.', get_class($this)) . '.' . $this->extension; } return codecept_data_dir() . $this->fileName; } /** * Performs assertion for data sets */ public function assert() { // fetch data $data = $this->fetchData(); if (!$data) { throw new ContentNotFound("Fetched snapshot is empty."); } $this->load(); if (!$this->dataSet) { $this->printDebug('Snapshot is empty. Updating snapshot...'); $this->dataSet = $data; $this->save(); return; } try { $this->assertData($data); $this->printDebug('Data matches snapshot'); } catch (AssertionFailedError $exception) { $this->printDebug('Snapshot assertion failed'); if (!is_bool($this->refresh)) { $confirm = Debug::confirm('Should we update snapshot with fresh data? (Y/n) '); } else { $confirm = $this->refresh; } if ($confirm) { $this->dataSet = $data; $this->save(); $this->printDebug('Snapshot data updated'); return; } if ($this->showDiff) { throw $exception; } $this->fail($exception->getMessage()); } } /** * Force update snapshot data. * * @param bool $refresh */ public function shouldRefreshSnapshot($refresh = true) { $this->refresh = $refresh; } /** * Show detailed diff if snapshot test fails * * @param bool $showDiff */ public function shouldShowDiffOnFail($showDiff = true) { $this->showDiff = $showDiff; } /** * json_encode/json_decode the snapshot data on storing/reading. * * @param bool $saveAsJson */ public function shouldSaveAsJson($saveAsJson = true) { $this->saveAsJson = $saveAsJson; } /** * Set the snapshot file extension. * By default it will be stored as `.json`. * * The file extension will not perform any formatting in the data, * it is only used as the snapshot file extension. * * @param string $fileExtension * @return void */ public function setSnapshotFileExtension($fileExtension = 'json') { $this->extension = $fileExtension; } private function printDebug($message) { Debug::debug(get_class($this) . ': ' . $message); } } codeception/src/Codeception/README.md000077700000000653151323602320013352 0ustar00# Codeception Core The most important classes are defined in root of Codeception. * `Codecept` - starts event dispatcher, loads subscribers, starts SuiteManager * `SuiteManager` - starts modules, starts test runner * `TestLoader` - loads tests from files * `Configuration` - loads YAML configuration * `Events` - defines all Codeception events * `TestCase` - applies Codeception feature to `PHPUnit\Framework\TestCase` class.codeception/src/Codeception/Step/Executor.php000077700000000776151323602320015323 0ustar00<?php namespace Codeception\Step; use Codeception\Step as CodeceptionStep; use Codeception\Lib\ModuleContainer; class Executor extends CodeceptionStep { protected $callable = null; public function __construct(\Closure $callable, $arguments = []) { parent::__construct('execute callable function', []); $this->callable = $callable; } public function run(ModuleContainer $container = null) { $callable = $this->callable; return $callable(); } } codeception/src/Codeception/Step/Action.php000077700000000130151323602320014722 0ustar00<?php namespace Codeception\Step; use Codeception\Step; class Action extends Step { } codeception/src/Codeception/Step/README.md000077700000000300151323602320014252 0ustar00# Steps Steps are used in scenarios. They do not keep any logic, but responsible for formatting. Exception for `ConditionalAssertion` which acts like `Assertion` but acts differently on fail.codeception/src/Codeception/Step/Incomplete.php000077700000000605151323602320015613 0ustar00<?php namespace Codeception\Step; use Codeception\Step as CodeceptionStep; use Codeception\Lib\ModuleContainer; class Incomplete extends CodeceptionStep { public function run(ModuleContainer $container = null) { throw new \PHPUnit\Framework\IncompleteTestError($this->getAction()); } public function __toString() { return $this->getAction(); } } codeception/src/Codeception/Step/Skip.php000077700000000574151323602320014427 0ustar00<?php namespace Codeception\Step; use Codeception\Step as CodeceptionStep; use Codeception\Lib\ModuleContainer; class Skip extends CodeceptionStep { public function run(ModuleContainer $container = null) { throw new \PHPUnit\Framework\SkippedTestError($this->getAction()); } public function __toString() { return $this->getAction(); } } codeception/src/Codeception/Step/Comment.php000077700000001412151323602320015113 0ustar00<?php namespace Codeception\Step; use Codeception\Lib\ModuleContainer; use Codeception\Step as CodeceptionStep; class Comment extends CodeceptionStep { public function __toString() { return (string) $this->getAction(); } public function toString($maxLength) { return mb_strcut($this->__toString(), 0, $maxLength, 'utf-8'); } public function getHtml($highlightColor = '#732E81') { return '<strong>' . $this->getAction() . '</strong>'; } public function getPhpCode($maxLength) { return '// ' . $this->getAction(); } public function run(ModuleContainer $container = null) { // don't do anything, let's rest } public function getPrefix() { return ''; } } codeception/src/Codeception/Step/GeneratedStep.php000077700000000233151323602320016243 0ustar00<?php namespace Codeception\Step; use Codeception\Util\Template; interface GeneratedStep { public static function getTemplate(Template $template); } codeception/src/Codeception/Step/Retry.php000077700000004515151323602320014625 0ustar00<?php namespace Codeception\Step; use Codeception\Lib\ModuleContainer; use Codeception\Util\Template; class Retry extends Assertion implements GeneratedStep { protected static $methodTemplate = <<<EOF /** * [!] Method is generated. * * {{doc}} * * Retry number and interval set by \$I->retry(); * * @see \{{module}}::{{method}}() */ public function {{action}}({{params}}) { \$retryNum = isset(\$this->retryNum) ? \$this->retryNum : 1; \$retryInterval = isset(\$this->retryInterval) ? \$this->retryInterval : 200; return \$this->getScenario()->runStep(new \Codeception\Step\Retry('{{method}}', func_get_args(), \$retryNum, \$retryInterval)); } EOF; private $retryNum; private $retryInterval; public function __construct($action, array $arguments, $retryNum, $retryInterval) { $this->action = $action; $this->arguments = $arguments; $this->retryNum = $retryNum; $this->retryInterval = $retryInterval; } public function run(ModuleContainer $container = null) { $retry = 0; $interval = $this->retryInterval; while (true) { try { $this->isTry = $retry < $this->retryNum; return parent::run($container); } catch (\Exception $e) { $retry++; if (!$this->isTry) { throw $e; } codecept_debug("Retrying #$retry in ${interval}ms"); usleep($interval * 1000); $interval *= 2; } } } public static function getTemplate(Template $template) { $action = $template->getVar('action'); if ((strpos($action, 'have') === 0) || (strpos($action, 'am') === 0)) { return; // dont retry conditions } if (strpos($action, 'wait') === 0) { return; // dont retry waiters } $doc = "* Executes $action and retries on failure."; return (new Template(self::$methodTemplate)) ->place('method', $template->getVar('method')) ->place('module', $template->getVar('module')) ->place('params', $template->getVar('params')) ->place('doc', $doc) ->place('action', 'retry'. ucfirst($action)); } } codeception/src/Codeception/Step/Assertion.php000077700000000171151323602320015461 0ustar00<?php namespace Codeception\Step; use Codeception\Step as CodeceptionStep; class Assertion extends CodeceptionStep { } codeception/src/Codeception/Step/Argument/PasswordArgument.php000077700000000730151323602320020602 0ustar00<?php namespace Codeception\Step\Argument; class PasswordArgument implements FormattedOutput { /** * @var string */ private $password; public function __construct($password) { $this->password = $password; } /** * {@inheritdoc} */ public function getOutput() { return '******'; } /** * {@inheritdoc} */ public function __toString() { return $this->password; } } codeception/src/Codeception/Step/Argument/FormattedOutput.php000077700000000715151323602320020446 0ustar00<?php namespace Codeception\Step\Argument; /** * Implemented in Step arguments where literal values need to be modified in test execution output (e.g. passwords). */ interface FormattedOutput { /** * Returns the argument's value formatted for output. * * @return string */ public function getOutput(); /** * Returns the argument's literal value. * * @return string */ public function __toString(); } codeception/src/Codeception/Step/Argument/.htaccess000077700000000177151323602320016367 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Step/Condition.php000077700000000171151323602320015440 0ustar00<?php namespace Codeception\Step; use Codeception\Step as CodeceptionStep; class Condition extends CodeceptionStep { } codeception/src/Codeception/Step/TryTo.php000077700000002315151323602320014575 0ustar00<?php namespace Codeception\Step; use Codeception\Lib\ModuleContainer; use Codeception\Util\Template; class TryTo extends Assertion implements GeneratedStep { public function run(ModuleContainer $container = null) { $this->isTry = true; try { parent::run($container); } catch (\Exception $e) { codecept_debug("Failed to perform: {$e->getMessage()}, skipping..."); return false; } return true; } public static function getTemplate(Template $template) { $action = $template->getVar('action'); if ((strpos($action, 'have') === 0) || (strpos($action, 'am') === 0)) { return; // dont try on conditions } if (strpos($action, 'wait') === 0) { return; // dont try on waiters } if (strpos($action, 'grab') === 0) { return; // dont on grabbers } $conditionalDoc = "* [!] Test won't be stopped on fail. Error won't be logged \n " . $template->getVar('doc'); return $template ->place('doc', $conditionalDoc) ->place('action', 'tryTo' . ucfirst($action)) ->place('step', 'TryTo'); } } codeception/src/Codeception/Step/.htaccess000077700000000177151323602320014605 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Step/ConditionalAssertion.php000077700000003206151323602320017647 0ustar00<?php namespace Codeception\Step; use Codeception\Exception\ConditionalAssertionFailed; use Codeception\Lib\ModuleContainer; use Codeception\Util\Template; class ConditionalAssertion extends Assertion implements GeneratedStep { public function run(ModuleContainer $container = null) { try { parent::run($container); } catch (\PHPUnit\Framework\AssertionFailedError $e) { throw new ConditionalAssertionFailed($e->getMessage(), $e->getCode(), $e); } } public function getAction() { $action = 'can' . ucfirst($this->action); $action = preg_replace('/^canDont/', 'cant', $action); return $action; } public function getHumanizedAction() { return $this->humanize($this->action . ' ' . $this->getHumanizedArguments()); } public static function getTemplate(Template $template) { $action = $template->getVar('action'); if ((0 !== strpos($action, 'see')) && (0 !== strpos($action, 'dontSee'))) { return ''; } $conditionalDoc = "* [!] Conditional Assertion: Test won't be stopped on fail\n " . $template->getVar('doc'); $prefix = 'can'; if (strpos($action, 'dontSee') === 0) { $prefix = 'cant'; $action = str_replace('dont', '', $action); } return $template ->place('doc', $conditionalDoc) ->place('action', $prefix . ucfirst($action)) ->place('step', 'ConditionalAssertion'); } public function match($name) { return 0 === strpos($name, 'see') || 0 === strpos($name, 'dontSee'); } } codeception/src/Codeception/Step/Meta.php000077700000002032151323602320014376 0ustar00<?php namespace Codeception\Step; use Codeception\Lib\ModuleContainer; use Codeception\Step as CodeceptionStep; class Meta extends CodeceptionStep { public function run(ModuleContainer $container = null) { } public function setTraceInfo($file, $line) { $this->file = $file; $this->line = $line; } public function setPrefix($actor) { $this->prefix = $actor; } public function getArgumentsAsString($maxLength = 200) { $argumentBackup = $this->arguments; $lastArgAsString = ''; $lastArg = end($this->arguments); if (is_string($lastArg) && strpos($lastArg, "\n") !== false) { $lastArgAsString = "\r\n " . str_replace("\n", "\n ", $lastArg); array_pop($this->arguments); } $result = parent::getArgumentsAsString($maxLength) . $lastArgAsString; $this->arguments = $argumentBackup; return $result; } public function setFailed($failed) { $this->failed = $failed; } } codeception/src/Codeception/Actor.php000077700000002200151323602320013642 0ustar00<?php namespace Codeception; use Codeception\Lib\Actor\Shared\Comment; use Codeception\Step\Executor; use Codeception\Lib\Actor\Shared\Pause; abstract class Actor { use Comment; use Pause; /** * @var \Codeception\Scenario */ protected $scenario; public function __construct(Scenario $scenario) { $this->scenario = $scenario; } /** * @return \Codeception\Scenario */ protected function getScenario() { return $this->scenario; } public function wantToTest($text) { $this->wantTo('test ' . $text); } public function wantTo($text) { $this->scenario->setFeature($text); } public function __call($method, $arguments) { $class = get_class($this); throw new \RuntimeException("Call to undefined method $class::$method"); } /** * Lazy-execution given anonymous function * @param $callable \Closure * @return $this */ public function execute($callable) { $this->scenario->addStep(new Executor($callable, [])); $callable(); return $this; } } codeception/src/Codeception/CustomCommandInterface.php000077700000000306151323602320017171 0ustar00<?php namespace Codeception; interface CustomCommandInterface { /** * returns the name of the command * * @return string */ public static function getCommandName(); } codeception/src/Codeception/Module.php000077700000021147151323602320014032 0ustar00<?php namespace Codeception; use Codeception\Exception\ModuleException; use Codeception\Lib\Interfaces\RequiresPackage; use Codeception\Lib\ModuleContainer; use Codeception\Util\Shared\Asserts; /** * Basic class for Modules and Helpers. * You must extend from it while implementing own helpers. * * Public methods of this class start with `_` prefix in order to ignore them in actor classes. * Module contains **HOOKS** which allow to handle test execution routine. * */ abstract class Module { use Asserts; /** * @var ModuleContainer */ protected $moduleContainer; /** * By setting it to false module wan't inherit methods of parent class. * * @var bool */ public static $includeInheritedActions = true; /** * Allows to explicitly set what methods have this class. * * @var array */ public static $onlyActions = []; /** * Allows to explicitly exclude actions from module. * * @var array */ public static $excludeActions = []; /** * Allows to rename actions * * @var array */ public static $aliases = []; protected $storage = []; protected $config = []; protected $backupConfig = []; protected $requiredFields = []; /** * Module constructor. * * Requires module container (to provide access between modules of suite) and config. * * @param ModuleContainer $moduleContainer * @param array|null $config */ public function __construct(ModuleContainer $moduleContainer, $config = null) { $this->moduleContainer = $moduleContainer; $this->backupConfig = $this->config; if (is_array($config)) { $this->_setConfig($config); } } /** * Allows to define initial module config. * Can be used in `_beforeSuite` hook of Helpers or Extensions * * ```php * <?php * public function _beforeSuite($settings = []) { * $this->getModule('otherModule')->_setConfig($this->myOtherConfig); * } * ``` * * @param $config * @throws Exception\ModuleConfigException * @throws ModuleException */ public function _setConfig($config) { $this->config = $this->backupConfig = array_merge($this->config, $config); $this->validateConfig(); } /** * Allows to redefine config for a specific test. * Config is restored at the end of a test. * * ```php * <?php * // cleanup DB only for specific group of tests * public function _before(Test $test) { * if (in_array('cleanup', $test->getMetadata()->getGroups()) { * $this->getModule('Db')->_reconfigure(['cleanup' => true]); * } * } * ``` * * @param $config * @throws Exception\ModuleConfigException * @throws ModuleException */ public function _reconfigure($config) { $this->config = array_merge($this->backupConfig, $config); $this->onReconfigure(); $this->validateConfig(); } /** * HOOK to be executed when config changes with `_reconfigure`. */ protected function onReconfigure() { // update client on reconfigurations } /** * Reverts config changed by `_reconfigure` */ public function _resetConfig() { $this->config = $this->backupConfig; } /** * Validates current config for required fields and required packages. * * @throws Exception\ModuleConfigException * @throws ModuleException */ protected function validateConfig() { $fields = array_keys($this->config); if (array_intersect($this->requiredFields, $fields) != $this->requiredFields) { throw new Exception\ModuleConfigException( get_class($this), "\nOptions: " . implode(', ', $this->requiredFields) . " are required\n" . "Please, update the configuration and set all the required fields\n\n" ); } if ($this instanceof RequiresPackage) { $errorMessage = ''; foreach ($this->_requires() as $className => $package) { if (class_exists($className)) { continue; } $errorMessage .= "Class $className can't be loaded, please add $package to composer.json\n"; } if ($errorMessage) { throw new ModuleException($this, $errorMessage); } } } /** * Returns a module name for a Module, a class name for Helper * * @return string */ public function _getName() { $moduleName = '\\' . get_class($this); if (strpos($moduleName, ModuleContainer::MODULE_NAMESPACE) === 0) { return substr($moduleName, strlen(ModuleContainer::MODULE_NAMESPACE)); } return $moduleName; } /** * Checks if a module has required fields * * @return bool */ public function _hasRequiredFields() { return !empty($this->requiredFields); } /** * **HOOK** triggered after module is created and configuration is loaded */ public function _initialize() { } /** * **HOOK** executed before suite * * @param array $settings */ public function _beforeSuite($settings = []) { } /** * **HOOK** executed after suite */ public function _afterSuite() { } /** * **HOOK** executed before step * * @param Step $step */ public function _beforeStep(Step $step) { } /** * **HOOK** executed after step * * @param Step $step */ public function _afterStep(Step $step) { } /** * **HOOK** executed before test * * @param TestInterface $test */ public function _before(TestInterface $test) { } /** * **HOOK** executed after test * * @param TestInterface $test */ public function _after(TestInterface $test) { } /** * **HOOK** executed when test fails but before `_after` * * @param TestInterface $test * @param \Exception $fail */ public function _failed(TestInterface $test, $fail) { } /** * Print debug message to the screen. * * @param $message */ protected function debug($message) { codecept_debug($message); } /** * Print debug message with a title * * @param $title * @param $message */ protected function debugSection($title, $message) { if (is_array($message) or is_object($message)) { $message = stripslashes(json_encode($message)); } $this->debug("[$title] $message"); } /** * Short text message to an amount of chars * * @param $message * @param $chars * @return string */ protected function shortenMessage($message, $chars = 150) { return mb_substr($message, 0, $chars, 'utf-8'); } /** * Checks that module is enabled. * * @param $name * @return bool */ protected function hasModule($name) { return $this->moduleContainer->hasModule($name); } /** * Get all enabled modules * * @return array */ protected function getModules() { return $this->moduleContainer->all(); } /** * Get another module by its name: * * ```php * <?php * $this->getModule('WebDriver')->_findElements('.items'); * ``` * * @param $name * @return Module * @throws ModuleException */ protected function getModule($name) { if (!$this->hasModule($name)) { $this->moduleContainer->throwMissingModuleExceptionWithSuggestion(__CLASS__, $name); } return $this->moduleContainer->getModule($name); } /** * Get config values or specific config item. * * @param mixed $key * @return mixed the config item's value or null if it doesn't exist */ public function _getConfig($key = null) { if (!$key) { return $this->config; } if (isset($this->config[$key])) { return $this->config[$key]; } return null; } protected function scalarizeArray($array) { foreach ($array as $k => $v) { if (!is_null($v) && !is_scalar($v)) { $array[$k] = (is_array($v) || $v instanceof \ArrayAccess) ? $this->scalarizeArray($v) : (string)$v; } } return $array; } } codeception/src/Codeception/Lib/Parser.php000077700000014131151323602320014542 0ustar00<?php namespace Codeception\Lib; use Codeception\Configuration; use Codeception\Exception\TestParseException; use Codeception\Scenario; use Codeception\Step; use Codeception\Test\Metadata; class Parser { /** * @var Scenario */ protected $scenario; /** * @var Metadata */ protected $metadata; protected $code; public function __construct(Scenario $scenario, Metadata $metadata) { $this->scenario = $scenario; $this->metadata = $metadata; } public function prepareToRun($code) { $this->parseFeature($code); $this->parseScenarioOptions($code); } public function parseFeature($code) { $matches = []; $code = $this->stripComments($code); $res = preg_match("~\\\$I->wantTo\\(\s*?['\"](.*?)['\"]\s*?\\);~", $code, $matches); if ($res) { $this->scenario->setFeature($matches[1]); return; } $res = preg_match("~\\\$I->wantToTest\\(['\"](.*?)['\"]\\);~", $code, $matches); if ($res) { $this->scenario->setFeature("test " . $matches[1]); return; } } public function parseScenarioOptions($code) { $this->metadata->setParamsFromAnnotations($this->matchComments($code)); } public function parseSteps($code) { // parse per line $friends = []; $lines = explode("\n", $code); $isFriend = false; foreach ($lines as $line) { // friends if (preg_match("~\\\$I->haveFriend\((.*?)\);~", $line, $matches)) { $friends[] = trim($matches[1], '\'"'); } // friend's section start if (preg_match("~\\\$(.*?)->does\(~", $line, $matches)) { $friend = $matches[1]; if (!in_array($friend, $friends)) { continue; } $isFriend = true; $this->addCommentStep("\n----- $friend does -----"); continue; } // actions if (preg_match("~\\\$I->(.*)\((.*?)\);~", $line, $matches)) { $this->addStep($matches); } // friend's section ends if ($isFriend && strpos($line, '}') !== false) { $this->addCommentStep("-------- back to me\n"); $isFriend = false; } } } protected function addStep($matches) { list($m, $action, $params) = $matches; if (in_array($action, ['wantTo', 'wantToTest'])) { return; } $this->scenario->addStep(new Step\Action($action, explode(',', $params))); } protected function addCommentStep($comment) { $this->scenario->addStep(new \Codeception\Step\Comment($comment, [])); } public static function validate($file) { $config = Configuration::config(); if (empty($config['settings']['lint'])) { // lint disabled in config return; } if (!function_exists('exec')) { //exec function is disabled #3324 return; } exec("php -l " . escapeshellarg($file) . " 2>&1", $output, $code); if ($code !== 0) { throw new TestParseException($file, implode("\n", $output)); } } public static function load($file) { if (PHP_MAJOR_VERSION < 7) { self::validate($file); } try { self::includeFile($file); } catch (\ParseError $e) { throw new TestParseException($file, $e->getMessage(), $e->getLine()); } catch (\Exception $e) { // file is valid otherwise } } public static function getClassesFromFile($file) { $sourceCode = file_get_contents($file); $classes = []; $tokens = token_get_all($sourceCode); $tokenCount = count($tokens); $namespace = ''; for ($i = 0; $i < $tokenCount; $i++) { if ($tokens[$i][0] === T_NAMESPACE) { $namespace = ''; for ($j = $i + 1; $j < $tokenCount; $j++) { if ($tokens[$j] === '{' || $tokens[$j] === ';') { break; } if ($tokens[$j][0] === T_STRING || (PHP_MAJOR_VERSION >= 8 && $tokens[$j][0] === T_NAME_QUALIFIED)) { $namespace .= $tokens[$j][1] . '\\'; } } } if ($tokens[$i][0] === T_CLASS) { if (!isset($tokens[$i - 2])) { $classes[] = $namespace . $tokens[$i + 2][1]; continue; } if ($tokens[$i - 2][0] === T_NEW) { continue; } if ($tokens[$i - 1][0] === T_WHITESPACE and $tokens[$i - 2][0] === T_DOUBLE_COLON) { continue; } if ($tokens[$i - 1][0] === T_DOUBLE_COLON) { continue; } $classes[] = $namespace . $tokens[$i + 2][1]; } } return $classes; } /* * Include in different scope to prevent included file from affecting $file variable */ private static function includeFile($file) { include_once $file; } /** * @param $code * @return mixed */ protected function stripComments($code) { $code = preg_replace('~\/\/.*?$~m', '', $code); // remove inline comments $code = preg_replace('~\/*\*.*?\*\/~ms', '', $code); return $code; // remove block comment } protected function matchComments($code) { $matches = []; $comments = ''; $hasLineComment = preg_match_all('~\/\/(.*?)$~m', $code, $matches); if ($hasLineComment) { foreach ($matches[1] as $line) { $comments .= $line."\n"; } } $hasBlockComment = preg_match('~\/*\*(.*?)\*\/~ms', $code, $matches); if ($hasBlockComment) { $comments .= $matches[1]."\n"; } return $comments; } } codeception/src/Codeception/Lib/Di.php000077700000012101151323602320013635 0ustar00<?php namespace Codeception\Lib; use Codeception\Exception\InjectionException; use Codeception\Util\ReflectionHelper; class Di { const DEFAULT_INJECT_METHOD_NAME = '_inject'; protected $container = []; /** * @var Di */ protected $fallback; public function __construct($fallback = null) { $this->fallback = $fallback; } public function get($className) { // normalize namespace $className = ltrim($className, '\\'); return isset($this->container[$className]) ? $this->container[$className] : null; } public function set($class) { $this->container[get_class($class)] = $class; } /** * @param string $className * @param array $constructorArgs * @param string $injectMethodName Method which will be invoked after object creation; * Resolved dependencies will be passed to it as arguments * @throws InjectionException * @return null|object */ public function instantiate( $className, $constructorArgs = null, $injectMethodName = self::DEFAULT_INJECT_METHOD_NAME ) { // normalize namespace $className = ltrim($className, '\\'); // get class from container if (isset($this->container[$className])) { if ($this->container[$className] instanceof $className) { return $this->container[$className]; } throw new InjectionException("Failed to resolve cyclic dependencies for class '$className'"); } // get class from parent container if ($this->fallback) { if ($class = $this->fallback->get($className)) { return $class; } } $this->container[$className] = false; // flag that object is being instantiated $reflectedClass = new \ReflectionClass($className); if (!$reflectedClass->isInstantiable()) { return null; } $reflectedConstructor = $reflectedClass->getConstructor(); if (is_null($reflectedConstructor)) { $object = new $className; } else { try { if (!$constructorArgs) { $constructorArgs = $this->prepareArgs($reflectedConstructor); } } catch (\Exception $e) { throw new InjectionException("Failed to create instance of '$className'. " . $e->getMessage()); } $object = $reflectedClass->newInstanceArgs($constructorArgs); } if ($injectMethodName) { $this->injectDependencies($object, $injectMethodName); } $this->container[$className] = $object; return $object; } /** * @param $object * @param string $injectMethodName Method which will be invoked with resolved dependencies as its arguments * @throws InjectionException */ public function injectDependencies($object, $injectMethodName = self::DEFAULT_INJECT_METHOD_NAME, $defaults = []) { if (!is_object($object)) { return; } $reflectedObject = new \ReflectionObject($object); if (!$reflectedObject->hasMethod($injectMethodName)) { return; } $reflectedMethod = $reflectedObject->getMethod($injectMethodName); try { $args = $this->prepareArgs($reflectedMethod, $defaults); } catch (\Exception $e) { $msg = $e->getMessage(); if ($e->getPrevious()) { // injection failed because PHP code is invalid. See #3869 $msg .= '; '. $e->getPrevious(); } throw new InjectionException( "Failed to inject dependencies in instance of '{$reflectedObject->name}'. $msg" ); } if (!$reflectedMethod->isPublic()) { $reflectedMethod->setAccessible(true); } $reflectedMethod->invokeArgs($object, $args); } /** * @param \ReflectionMethod $method * @param $defaults * @throws InjectionException * @return array */ protected function prepareArgs(\ReflectionMethod $method, $defaults = []) { $args = []; $parameters = $method->getParameters(); foreach ($parameters as $k => $parameter) { $dependency = ReflectionHelper::getClassFromParameter($parameter); if (is_null($dependency)) { if (!$parameter->isOptional()) { if (!isset($defaults[$k])) { throw new InjectionException("Parameter '$parameter->name' must have default value."); } $args[] = $defaults[$k]; continue; } $args[] = $parameter->getDefaultValue(); } else { $arg = $this->instantiate($dependency); if (is_null($arg)) { throw new InjectionException("Failed to resolve dependency '$dependency'."); } $args[] = $arg; } } return $args; } } codeception/src/Codeception/Lib/ParamsLoader.php000077700000012446151323602320015667 0ustar00<?php namespace Codeception\Lib; use Codeception\Exception\ConfigurationException; use Symfony\Component\Yaml\Yaml; class ParamsLoader { protected $paramStorage; protected $paramsFile; public function load($paramStorage) { $this->paramsFile = null; $this->paramStorage = $paramStorage; if (is_array($paramStorage)) { return $this->loadArray(); } if ($paramStorage === 'env' || $paramStorage === 'environment') { return $this->loadEnvironmentVars(); } $this->paramsFile = codecept_absolute_path($paramStorage); if (!file_exists($this->paramsFile)) { throw new ConfigurationException("Params file {$this->paramsFile} not found"); } try { if (preg_match('~\.ya?ml$~', $paramStorage)) { return $this->loadYamlFile(); } if (preg_match('~\.ini$~', $paramStorage)) { return $this->loadIniFile(); } if (preg_match('~\.php$~', $paramStorage)) { return $this->loadPhpFile(); } if (preg_match('~(\.env(\.|$))~', $paramStorage)) { return $this->loadDotEnvFile(); } if (preg_match('~\.xml$~', $paramStorage)) { return $this->loadXmlFile(); } } catch (\Exception $e) { throw new ConfigurationException("Failed loading params from $paramStorage\n" . $e->getMessage()); } throw new ConfigurationException("Params can't be loaded from `$paramStorage`."); } public function loadArray() { return $this->paramStorage; } protected function loadIniFile() { return parse_ini_file($this->paramsFile); } protected function loadPhpFile() { return require $this->paramsFile; } protected function loadYamlFile() { $params = Yaml::parse(file_get_contents($this->paramsFile)); if (isset($params['parameters'])) { // Symfony style $params = $params['parameters']; } return $params; } protected function loadXmlFile() { $paramsToArray = function (\SimpleXMLElement $params) use (&$paramsToArray) { $a = []; foreach ($params as $param) { $key = isset($param['key']) ? (string) $param['key'] : $param->getName(); $type = isset($param['type']) ? (string) $param['type'] : 'string'; $value = (string) $param; switch ($type) { case 'bool': case 'boolean': case 'int': case 'integer': case 'float': case 'double': $a[$key] = settype($value, $type); break; case 'constant': $a[$key] = constant($value); break; case 'collection': $a[$key] = $paramsToArray($param); break; default: $a[$key] = (string) $param; } } return $a; }; return $paramsToArray(simplexml_load_file($this->paramsFile)); } protected function loadDotEnvFile() { if (class_exists('Dotenv\Dotenv')) { if (class_exists('Dotenv\Repository\RepositoryBuilder') && method_exists('Dotenv\Repository\RepositoryBuilder', 'createWithNoAdapters')) { //dotenv v5 $repository = \Dotenv\Repository\RepositoryBuilder::createWithNoAdapters() ->addAdapter(\Dotenv\Repository\Adapter\EnvConstAdapter::class) ->addAdapter(\Dotenv\Repository\Adapter\ServerConstAdapter::class) ->make(); $dotEnv = \Dotenv\Dotenv::create($repository, codecept_root_dir(), $this->paramStorage); } elseif (class_exists('Dotenv\Repository\RepositoryBuilder')) { //dotenv v4 $repository = \Dotenv\Repository\RepositoryBuilder::create() ->withReaders([new \Dotenv\Repository\Adapter\ServerConstAdapter()]) ->immutable() ->make(); $dotEnv = \Dotenv\Dotenv::create($repository, codecept_root_dir(), $this->paramStorage); } elseif (method_exists('Dotenv\Dotenv', 'create')) { //dotenv v3 $dotEnv = \Dotenv\Dotenv::create(codecept_root_dir(), $this->paramStorage); } else { //dotenv v2 $dotEnv = new \Dotenv\Dotenv(codecept_root_dir(), $this->paramStorage); } $dotEnv->load(); return $_SERVER; } elseif (class_exists('Symfony\Component\Dotenv\Dotenv')) { $dotEnv = new \Symfony\Component\Dotenv\Dotenv(); $dotEnv->load(codecept_root_dir($this->paramStorage)); return $_SERVER; } throw new ConfigurationException( "`vlucas/phpdotenv` library is required to parse .env files.\n" . "Please install it via composer: composer require vlucas/phpdotenv" ); } protected function loadEnvironmentVars() { return $_SERVER; } } codeception/src/Codeception/Lib/README.md000077700000000127151323602320014054 0ustar00# Internal Libraries Various classes that Codeception core and modules are relying on.codeception/src/Codeception/Lib/Connector/Shared/PhpSuperGlobalsConverter.php000077700000007202151323602320023411 0ustar00<?php namespace Codeception\Lib\Connector\Shared; /** * Converts BrowserKit\Request's request parameters and files into PHP-compatible structure * * @see https://bugs.php.net/bug.php?id=25589 * @see https://bugs.php.net/bug.php?id=25589 * * @package Codeception\Lib\Connector */ trait PhpSuperGlobalsConverter { /** * Rearrange files array to be compatible with PHP $_FILES superglobal structure * @see https://bugs.php.net/bug.php?id=25589 * * @param array $requestFiles * * @return array */ protected function remapFiles(array $requestFiles) { $files = $this->rearrangeFiles($requestFiles); return $this->replaceSpaces($files); } /** * Escape high-level variable name with dots, underscores and other "special" chars * to be compatible with PHP "bug" * @see https://bugs.php.net/bug.php?id=40000 * * @param array $parameters * * @return array */ protected function remapRequestParameters(array $parameters) { return $this->replaceSpaces($parameters); } private function rearrangeFiles($requestFiles) { $files = []; foreach ($requestFiles as $name => $info) { if (!is_array($info)) { continue; } /** * If we have a form with fields like * ``` * <input type="file" name="foo" /> * <input type="file" name="foo[bar]" /> * ``` * then only array variable will be used while simple variable will be ignored in php $_FILES * (eg $_FILES = [ * foo => [ * tmp_name => [ * 'bar' => 'asdf' * ], * //... * ] * ] * ) * (notice there is no entry for file "foo", only for file "foo[bar]" * this will check if current element contains inner arrays within it's keys * so we can ignore element itself and only process inner files */ $hasInnerArrays = count(array_filter($info, 'is_array')); if ($hasInnerArrays || !isset($info['tmp_name'])) { $inner = $this->remapFiles($info); foreach ($inner as $innerName => $innerInfo) { /** * Convert from ['a' => ['tmp_name' => '/tmp/test.txt'] ] * to ['tmp_name' => ['a' => '/tmp/test.txt'] ] */ $innerInfo = array_map( function ($v) use ($innerName) { return [$innerName => $v]; }, $innerInfo ); if (empty($files[$name])) { $files[$name] = []; } $files[$name] = array_replace_recursive($files[$name], $innerInfo); } } else { $files[$name] = $info; } } return $files; } /** * Replace spaces and dots and other chars in high-level query parameters for * compatibility with PHP bug (or not a bug) * @see https://bugs.php.net/bug.php?id=40000 * * @param array $parameters Array of request parameters to be converted * * @return array */ private function replaceSpaces($parameters) { $qs = http_build_query($parameters, '', '&'); parse_str($qs, $output); return $output; } } codeception/src/Codeception/Lib/Connector/Shared/.htaccess000077700000000177151323602320017540 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Lib/Connector/.htaccess000077700000000177151323602320016332 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Lib/GroupManager.php000077700000015650151323602320015704 0ustar00<?php namespace Codeception\Lib; use Codeception\Configuration; use Codeception\Exception\ConfigurationException; use Codeception\Test\Interfaces\Reported; use Codeception\Test\Descriptor; use Codeception\TestInterface; use Codeception\Test\Gherkin; use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\SplFileInfo; /** * Loads information for groups from external sources (config, filesystem) */ class GroupManager { protected $configuredGroups; protected $testsInGroups = []; public function __construct(array $groups) { $this->configuredGroups = $groups; $this->loadGroupsByPattern(); $this->loadConfiguredGroupSettings(); } /** * proceeds group names with asterisk: * * ``` * "tests/_log/g_*" => [ * "tests/_log/group_1", * "tests/_log/group_2", * "tests/_log/group_3", * ] * ``` */ protected function loadGroupsByPattern() { foreach ($this->configuredGroups as $group => $pattern) { if (strpos($group, '*') === false) { continue; } $files = Finder::create()->files() ->name(basename($pattern)) ->sortByName() ->in(Configuration::projectDir().dirname($pattern)); $i = 1; foreach ($files as $file) { /** @var SplFileInfo $file * */ $this->configuredGroups[str_replace('*', $i, $group)] = dirname($pattern).DIRECTORY_SEPARATOR.$file->getRelativePathname(); $i++; } unset($this->configuredGroups[$group]); } } protected function loadConfiguredGroupSettings() { foreach ($this->configuredGroups as $group => $tests) { $this->testsInGroups[$group] = []; if (is_array($tests)) { foreach ($tests as $test) { $file = str_replace(['/', '\\'], [DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR], $test); $this->testsInGroups[$group][] = $this->normalizeFilePath($file, $group); } } elseif (is_file(Configuration::projectDir() . $tests)) { $handle = @fopen(Configuration::projectDir() . $tests, "r"); if ($handle) { while (($test = fgets($handle, 4096)) !== false) { // if the current line is blank then we need to move to the next line // otherwise the current codeception directory becomes part of the group // which causes every single test to run if (trim($test) === '') { continue; } $file = str_replace(['/', '\\'], [DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR], trim($test)); $this->testsInGroups[$group][] = $this->normalizeFilePath($file, $group); } fclose($handle); } } } } /** * @param string $file * @param string $group * @return false|string * @throws ConfigurationException */ private function normalizeFilePath($file, $group) { $pathParts = explode(':', $file); if (codecept_is_path_absolute($file)) { if ($file[0] === '/' && count($pathParts) > 1) { //take segment before first : $this->checkIfFileExists($pathParts[0], $group); return sprintf('%s:%s', realpath($pathParts[0]), $pathParts[1]); } else if (count($pathParts) > 2) { //on Windows take segment before second : $fullPath = $pathParts[0] . ':' . $pathParts[1]; $this->checkIfFileExists($fullPath, $group); return sprintf('%s:%s', realpath($fullPath), $pathParts[2]); } $this->checkIfFileExists($file); return realpath($file); } elseif (strpos($file, ':') === false) { $dirtyPath = Configuration::projectDir() . $file; $this->checkIfFileExists($dirtyPath, $group); return realpath($dirtyPath); } $dirtyPath = Configuration::projectDir() . $pathParts[0]; $this->checkIfFileExists($dirtyPath, $group); return sprintf('%s:%s', realpath($dirtyPath), $pathParts[1]); } /** * @param string $path * @param string $group * @throws ConfigurationException */ private function checkIfFileExists($path, $group) { if (!file_exists($path)) { throw new ConfigurationException('GroupManager: File or directory ' . $path . ' set in ' . $group. ' group does not exist'); } } public function groupsForTest(\PHPUnit\Framework\Test $test) { $groups = []; $filename = Descriptor::getTestFileName($test); if ($test instanceof TestInterface) { $groups = $test->getMetadata()->getGroups(); } if ($test instanceof Reported) { $info = $test->getReportFields(); if (isset($info['class'])) { $groups = array_merge($groups, \PHPUnit\Util\Test::getGroups($info['class'], $info['name'])); } $filename = str_replace(['\\\\', '//', '/./'], ['\\', '/', '/'], $info['file']); } if ($test instanceof \PHPUnit\Framework\TestCase) { $groups = array_merge($groups, \PHPUnit\Util\Test::getGroups(get_class($test), $test->getName(false))); } if ($test instanceof \PHPUnit\Framework\TestSuite\DataProvider) { $firstTest = $test->testAt(0); if ($firstTest != false && $firstTest instanceof TestInterface) { $groups = array_merge($groups, $firstTest->getMetadata()->getGroups()); $filename = Descriptor::getTestFileName($firstTest); } } foreach ($this->testsInGroups as $group => $tests) { foreach ($tests as $testPattern) { if ($filename == $testPattern) { $groups[] = $group; } if (strpos($filename . ':' . $test->getName(false), $testPattern) === 0) { $groups[] = $group; } if ($test instanceof Gherkin && mb_strtolower($filename . ':' . $test->getMetadata()->getFeature()) === mb_strtolower($testPattern)) { $groups[] = $group; } if ($test instanceof \PHPUnit\Framework\TestSuite\DataProvider) { $firstTest = $test->testAt(0); if ($firstTest != false && $firstTest instanceof TestInterface) { if (strpos($filename . ':' . $firstTest->getName(false), $testPattern) === 0) { $groups[] = $group; } } } } } return array_unique($groups); } } codeception/src/Codeception/Lib/Friend.php000077700000004211151323602320014513 0ustar00<?php namespace Codeception\Lib; use Codeception\Actor; use Codeception\Exception\TestRuntimeException; class Friend { protected $name; protected $actor; protected $data = []; protected $multiSessionModules = []; public function __construct($name, Actor $actor, $modules = []) { $this->name = $name; $this->actor = $actor; $this->multiSessionModules = array_filter($modules, function ($m) { return $m instanceof Interfaces\MultiSession; }); if (empty($this->multiSessionModules)) { throw new TestRuntimeException("No multisession modules used. Can't instantiate friend"); } } public function does($closure) { $currentUserData = []; foreach ($this->multiSessionModules as $module) { $name = $module->_getName(); $currentUserData[$name] = $module->_backupSession(); if (empty($this->data[$name])) { $module->_initializeSession(); $this->data[$name] = $module->_backupSession(); continue; } $module->_loadSession($this->data[$name]); }; $this->actor->comment(strtoupper("{$this->name} does ---")); $ret = $closure($this->actor); $this->actor->comment(strtoupper("--- {$this->name} finished")); foreach ($this->multiSessionModules as $module) { $name = $module->_getName(); $this->data[$name] = $module->_backupSession(); $module->_loadSession($currentUserData[$name]); }; return $ret; } public function isGoingTo($argumentation) { $this->actor->amGoingTo($argumentation); } public function expects($prediction) { $this->actor->expect($prediction); } public function expectsTo($prediction) { $this->actor->expectTo($prediction); } public function leave() { foreach ($this->multiSessionModules as $module) { if (isset($this->data[$module->_getName()])) { $module->_closeSession($this->data[$module->_getName()]); } } } } codeception/src/Codeception/Lib/Interfaces/MultiSession.php000077700000000434151323602320020030 0ustar00<?php namespace Codeception\Lib\Interfaces; interface MultiSession { public function _initializeSession(); public function _loadSession($session); public function _backupSession(); public function _closeSession($session = null); public function _getName(); } codeception/src/Codeception/Lib/Interfaces/ORM.php000077700000000077151323602320016032 0ustar00<?php namespace Codeception\Lib\Interfaces; interface ORM { } codeception/src/Codeception/Lib/Interfaces/Web.php000077700000066741151323602320016124 0ustar00<?php namespace Codeception\Lib\Interfaces; interface Web { /** * Opens the page for the given relative URI. * * ``` php * <?php * // opens front page * $I->amOnPage('/'); * // opens /register page * $I->amOnPage('/register'); * ``` * * @param string $page */ public function amOnPage($page); /** * Checks that the current page contains the given string (case insensitive). * * You can specify a specific HTML element (via CSS or XPath) as the second * parameter to only search within that element. * * ``` php * <?php * $I->see('Logout'); // I can suppose user is logged in * $I->see('Sign Up', 'h1'); // I can suppose it's a signup page * $I->see('Sign Up', '//body/h1'); // with XPath * $I->see('Sign Up', ['css' => 'body h1']); // with strict CSS locator * ``` * * Note that the search is done after stripping all HTML tags from the body, * so `$I->see('strong')` will return true for strings like: * * - `<p>I am Stronger than thou</p>` * - `<script>document.createElement('strong');</script>` * * But will *not* be true for strings like: * * - `<strong>Home</strong>` * - `<div class="strong">Home</strong>` * - `<!-- strong -->` * * For checking the raw source code, use `seeInSource()`. * * @param string $text * @param array|string $selector optional */ public function see($text, $selector = null); /** * Checks that the current page doesn't contain the text specified (case insensitive). * Give a locator as the second parameter to match a specific region. * * ```php * <?php * $I->dontSee('Login'); // I can suppose user is already logged in * $I->dontSee('Sign Up','h1'); // I can suppose it's not a signup page * $I->dontSee('Sign Up','//body/h1'); // with XPath * $I->dontSee('Sign Up', ['css' => 'body h1']); // with strict CSS locator * ``` * * Note that the search is done after stripping all HTML tags from the body, * so `$I->dontSee('strong')` will fail on strings like: * * - `<p>I am Stronger than thou</p>` * - `<script>document.createElement('strong');</script>` * * But will ignore strings like: * * - `<strong>Home</strong>` * - `<div class="strong">Home</strong>` * - `<!-- strong -->` * * For checking the raw source code, use `seeInSource()`. * * @param string $text * @param array|string $selector optional */ public function dontSee($text, $selector = null); /** * Checks that the current page contains the given string in its * raw source code. * * ``` php * <?php * $I->seeInSource('<h1>Green eggs & ham</h1>'); * ``` * * @param $raw */ public function seeInSource($raw); /** * Checks that the current page contains the given string in its * raw source code. * * ```php * <?php * $I->dontSeeInSource('<h1>Green eggs & ham</h1>'); * ``` * * @param $raw */ public function dontSeeInSource($raw); /** * Submits the given form on the page, with the given form * values. Pass the form field's values as an array in the second * parameter. * * Although this function can be used as a short-hand version of * `fillField()`, `selectOption()`, `click()` etc. it has some important * differences: * * * Only field *names* may be used, not CSS/XPath selectors nor field labels * * If a field is sent to this function that does *not* exist on the page, * it will silently be added to the HTTP request. This is helpful for testing * some types of forms, but be aware that you will *not* get an exception * like you would if you called `fillField()` or `selectOption()` with * a missing field. * * Fields that are not provided will be filled by their values from the page, * or from any previous calls to `fillField()`, `selectOption()` etc. * You don't need to click the 'Submit' button afterwards. * This command itself triggers the request to form's action. * * You can optionally specify which button's value to include * in the request with the last parameter (as an alternative to * explicitly setting its value in the second parameter), as * button values are not otherwise included in the request. * * Examples: * * ``` php * <?php * $I->submitForm('#login', [ * 'login' => 'davert', * 'password' => '123456' * ]); * // or * $I->submitForm('#login', [ * 'login' => 'davert', * 'password' => '123456' * ], 'submitButtonName'); * * ``` * * For example, given this sample "Sign Up" form: * * ``` html * <form action="/sign_up"> * Login: * <input type="text" name="user[login]" /><br/> * Password: * <input type="password" name="user[password]" /><br/> * Do you agree to our terms? * <input type="checkbox" name="user[agree]" /><br/> * Select pricing plan: * <select name="plan"> * <option value="1">Free</option> * <option value="2" selected="selected">Paid</option> * </select> * <input type="submit" name="submitButton" value="Submit" /> * </form> * ``` * * You could write the following to submit it: * * ``` php * <?php * $I->submitForm( * '#userForm', * [ * 'user' => [ * 'login' => 'Davert', * 'password' => '123456', * 'agree' => true * ] * ], * 'submitButton' * ); * ``` * Note that "2" will be the submitted value for the "plan" field, as it is * the selected option. * * You can also emulate a JavaScript submission by not specifying any * buttons in the third parameter to submitForm. * * ```php * <?php * $I->submitForm( * '#userForm', * [ * 'user' => [ * 'login' => 'Davert', * 'password' => '123456', * 'agree' => true * ] * ] * ); * ``` * * This function works well when paired with `seeInFormFields()` * for quickly testing CRUD interfaces and form validation logic. * * ``` php * <?php * $form = [ * 'field1' => 'value', * 'field2' => 'another value', * 'checkbox1' => true, * // ... * ]; * $I->submitForm('#my-form', $form, 'submitButton'); * // $I->amOnPage('/path/to/form-page') may be needed * $I->seeInFormFields('#my-form', $form); * ``` * * Parameter values can be set to arrays for multiple input fields * of the same name, or multi-select combo boxes. For checkboxes, * you can use either the string value or boolean `true`/`false` which will * be replaced by the checkbox's value in the DOM. * * ``` php * <?php * $I->submitForm('#my-form', [ * 'field1' => 'value', * 'checkbox' => [ * 'value of first checkbox', * 'value of second checkbox', * ], * 'otherCheckboxes' => [ * true, * false, * false * ], * 'multiselect' => [ * 'first option value', * 'second option value' * ] * ]); * ``` * * Mixing string and boolean values for a checkbox's value is not supported * and may produce unexpected results. * * Field names ending in `[]` must be passed without the trailing square * bracket characters, and must contain an array for its value. This allows * submitting multiple values with the same name, consider: * * ```php * <?php * // This will NOT work correctly * $I->submitForm('#my-form', [ * 'field[]' => 'value', * 'field[]' => 'another value', // 'field[]' is already a defined key * ]); * ``` * * The solution is to pass an array value: * * ```php * <?php * // This way both values are submitted * $I->submitForm('#my-form', [ * 'field' => [ * 'value', * 'another value', * ] * ]); * ``` * * @param $selector * @param $params * @param $button */ public function submitForm($selector, array $params, $button = null); /** * Perform a click on a link or a button, given by a locator. * If a fuzzy locator is given, the page will be searched for a button, link, or image matching the locator string. * For buttons, the "value" attribute, "name" attribute, and inner text are searched. * For links, the link text is searched. * For images, the "alt" attribute and inner text of any parent links are searched. * * The second parameter is a context (CSS or XPath locator) to narrow the search. * * Note that if the locator matches a button of type `submit`, the form will be submitted. * * ``` php * <?php * // simple link * $I->click('Logout'); * // button of form * $I->click('Submit'); * // CSS button * $I->click('#form input[type=submit]'); * // XPath * $I->click('//form/*[@type="submit"]'); * // link in context * $I->click('Logout', '#nav'); * // using strict locator * $I->click(['link' => 'Login']); * ?> * ``` * * @param $link * @param $context */ public function click($link, $context = null); /** * Checks that there's a link with the specified text. * Give a full URL as the second parameter to match links with that exact URL. * * ``` php * <?php * $I->seeLink('Logout'); // matches <a href="#">Logout</a> * $I->seeLink('Logout','/logout'); // matches <a href="/logout">Logout</a> * ?> * ``` * * @param string $text * @param string $url optional */ public function seeLink($text, $url = null); /** * Checks that the page doesn't contain a link with the given string. * If the second parameter is given, only links with a matching "href" attribute will be checked. * * ``` php * <?php * $I->dontSeeLink('Logout'); // I suppose user is not logged in * $I->dontSeeLink('Checkout now', '/store/cart.php'); * ?> * ``` * * @param string $text * @param string $url optional */ public function dontSeeLink($text, $url = null); /** * Checks that current URI contains the given string. * * ``` php * <?php * // to match: /home/dashboard * $I->seeInCurrentUrl('home'); * // to match: /users/1 * $I->seeInCurrentUrl('/users/'); * ?> * ``` * * @param string $uri */ public function seeInCurrentUrl($uri); /** * Checks that the current URL is equal to the given string. * Unlike `seeInCurrentUrl`, this only matches the full URL. * * ``` php * <?php * // to match root url * $I->seeCurrentUrlEquals('/'); * ?> * ``` * * @param string $uri */ public function seeCurrentUrlEquals($uri); /** * Checks that the current URL matches the given regular expression. * * ``` php * <?php * // to match root url * $I->seeCurrentUrlMatches('~^/users/(\d+)~'); * ?> * ``` * * @param string $uri */ public function seeCurrentUrlMatches($uri); /** * Checks that the current URI doesn't contain the given string. * * ``` php * <?php * $I->dontSeeInCurrentUrl('/users/'); * ?> * ``` * * @param string $uri */ public function dontSeeInCurrentUrl($uri); /** * Checks that the current URL doesn't equal the given string. * Unlike `dontSeeInCurrentUrl`, this only matches the full URL. * * ``` php * <?php * // current url is not root * $I->dontSeeCurrentUrlEquals('/'); * ?> * ``` * * @param string $uri */ public function dontSeeCurrentUrlEquals($uri); /** * Checks that current url doesn't match the given regular expression. * * ``` php * <?php * // to match root url * $I->dontSeeCurrentUrlMatches('~^/users/(\d+)~'); * ?> * ``` * * @param string $uri */ public function dontSeeCurrentUrlMatches($uri); /** * Executes the given regular expression against the current URI and returns the first capturing group. * If no parameters are provided, the full URI is returned. * * ``` php * <?php * $user_id = $I->grabFromCurrentUrl('~^/user/(\d+)/~'); * $uri = $I->grabFromCurrentUrl(); * ?> * ``` * * @param string $uri optional * * @return mixed */ public function grabFromCurrentUrl($uri = null); /** * Checks that the specified checkbox is checked. * * ``` php * <?php * $I->seeCheckboxIsChecked('#agree'); // I suppose user agreed to terms * $I->seeCheckboxIsChecked('#signup_form input[type=checkbox]'); // I suppose user agreed to terms, If there is only one checkbox in form. * $I->seeCheckboxIsChecked('//form/input[@type=checkbox and @name=agree]'); * ?> * ``` * * @param $checkbox */ public function seeCheckboxIsChecked($checkbox); /** * Check that the specified checkbox is unchecked. * * ``` php * <?php * $I->dontSeeCheckboxIsChecked('#agree'); // I suppose user didn't agree to terms * $I->seeCheckboxIsChecked('#signup_form input[type=checkbox]'); // I suppose user didn't check the first checkbox in form. * ?> * ``` * * @param $checkbox */ public function dontSeeCheckboxIsChecked($checkbox); /** * Checks that the given input field or textarea *equals* (i.e. not just contains) the given value. * Fields are matched by label text, the "name" attribute, CSS, or XPath. * * ``` php * <?php * $I->seeInField('Body','Type your comment here'); * $I->seeInField('form textarea[name=body]','Type your comment here'); * $I->seeInField('form input[type=hidden]','hidden_value'); * $I->seeInField('#searchform input','Search'); * $I->seeInField('//form/*[@name=search]','Search'); * $I->seeInField(['name' => 'search'], 'Search'); * ?> * ``` * * @param $field * @param $value */ public function seeInField($field, $value); /** * Checks that an input field or textarea doesn't contain the given value. * For fuzzy locators, the field is matched by label text, CSS and XPath. * * ``` php * <?php * $I->dontSeeInField('Body','Type your comment here'); * $I->dontSeeInField('form textarea[name=body]','Type your comment here'); * $I->dontSeeInField('form input[type=hidden]','hidden_value'); * $I->dontSeeInField('#searchform input','Search'); * $I->dontSeeInField('//form/*[@name=search]','Search'); * $I->dontSeeInField(['name' => 'search'], 'Search'); * ?> * ``` * * @param $field * @param $value */ public function dontSeeInField($field, $value); /** * Checks if the array of form parameters (name => value) are set on the form matched with the * passed selector. * * ``` php * <?php * $I->seeInFormFields('form[name=myform]', [ * 'input1' => 'value', * 'input2' => 'other value', * ]); * ?> * ``` * * For multi-select elements, or to check values of multiple elements with the same name, an * array may be passed: * * ``` php * <?php * $I->seeInFormFields('.form-class', [ * 'multiselect' => [ * 'value1', * 'value2', * ], * 'checkbox[]' => [ * 'a checked value', * 'another checked value', * ], * ]); * ?> * ``` * * Additionally, checkbox values can be checked with a boolean. * * ``` php * <?php * $I->seeInFormFields('#form-id', [ * 'checkbox1' => true, // passes if checked * 'checkbox2' => false, // passes if unchecked * ]); * ?> * ``` * * Pair this with submitForm for quick testing magic. * * ``` php * <?php * $form = [ * 'field1' => 'value', * 'field2' => 'another value', * 'checkbox1' => true, * // ... * ]; * $I->submitForm('//form[@id=my-form]', $form, 'submitButton'); * // $I->amOnPage('/path/to/form-page') may be needed * $I->seeInFormFields('//form[@id=my-form]', $form); * ?> * ``` * * @param $formSelector * @param $params */ public function seeInFormFields($formSelector, array $params); /** * Checks if the array of form parameters (name => value) are not set on the form matched with * the passed selector. * * ``` php * <?php * $I->dontSeeInFormFields('form[name=myform]', [ * 'input1' => 'non-existent value', * 'input2' => 'other non-existent value', * ]); * ?> * ``` * * To check that an element hasn't been assigned any one of many values, an array can be passed * as the value: * * ``` php * <?php * $I->dontSeeInFormFields('.form-class', [ * 'fieldName' => [ * 'This value shouldn\'t be set', * 'And this value shouldn\'t be set', * ], * ]); * ?> * ``` * * Additionally, checkbox values can be checked with a boolean. * * ``` php * <?php * $I->dontSeeInFormFields('#form-id', [ * 'checkbox1' => true, // fails if checked * 'checkbox2' => false, // fails if unchecked * ]); * ?> * ``` * * @param $formSelector * @param $params */ public function dontSeeInFormFields($formSelector, array $params); /** * Selects an option in a select tag or in radio button group. * * ``` php * <?php * $I->selectOption('form select[name=account]', 'Premium'); * $I->selectOption('form input[name=payment]', 'Monthly'); * $I->selectOption('//form/select[@name=account]', 'Monthly'); * ?> * ``` * * Provide an array for the second argument to select multiple options: * * ``` php * <?php * $I->selectOption('Which OS do you use?', array('Windows','Linux')); * ?> * ``` * * Or provide an associative array for the second argument to specifically define which selection method should be used: * * ``` php * <?php * $I->selectOption('Which OS do you use?', array('text' => 'Windows')); // Only search by text 'Windows' * $I->selectOption('Which OS do you use?', array('value' => 'windows')); // Only search by value 'windows' * ?> * ``` * * @param $select * @param $option */ public function selectOption($select, $option); /** * Ticks a checkbox. For radio buttons, use the `selectOption` method instead. * * ``` php * <?php * $I->checkOption('#agree'); * ?> * ``` * * @param $option */ public function checkOption($option); /** * Unticks a checkbox. * * ``` php * <?php * $I->uncheckOption('#notify'); * ?> * ``` * * @param $option */ public function uncheckOption($option); /** * Fills a text field or textarea with the given string. * * ``` php * <?php * $I->fillField("//input[@type='text']", "Hello World!"); * $I->fillField(['name' => 'email'], 'jon@mail.com'); * ?> * ``` * * @param $field * @param $value */ public function fillField($field, $value); /** * Attaches a file relative to the Codeception `_data` directory to the given file upload field. * * ``` php * <?php * // file is stored in 'tests/_data/prices.xls' * $I->attachFile('input[@type="file"]', 'prices.xls'); * ?> * ``` * * @param $field * @param $filename */ public function attachFile($field, $filename); /** * Finds and returns the text contents of the given element. * If a fuzzy locator is used, the element is found using CSS, XPath, * and by matching the full page source by regular expression. * * ``` php * <?php * $heading = $I->grabTextFrom('h1'); * $heading = $I->grabTextFrom('descendant-or-self::h1'); * $value = $I->grabTextFrom('~<input value=(.*?)]~sgi'); // match with a regex * ?> * ``` * * @param $cssOrXPathOrRegex * * @return mixed */ public function grabTextFrom($cssOrXPathOrRegex); /** * Finds the value for the given form field. * If a fuzzy locator is used, the field is found by field name, CSS, and XPath. * * ``` php * <?php * $name = $I->grabValueFrom('Name'); * $name = $I->grabValueFrom('input[name=username]'); * $name = $I->grabValueFrom('descendant-or-self::form/descendant::input[@name = 'username']'); * $name = $I->grabValueFrom(['name' => 'username']); * ?> * ``` * * @param $field * * @return mixed */ public function grabValueFrom($field); /** * Grabs the value of the given attribute value from the given element. * Fails if element is not found. * * ``` php * <?php * $I->grabAttributeFrom('#tooltip', 'title'); * ?> * ``` * * * @param $cssOrXpath * @param $attribute * * @return mixed */ public function grabAttributeFrom($cssOrXpath, $attribute); /** * Grabs either the text content, or attribute values, of nodes * matched by $cssOrXpath and returns them as an array. * * ```html * <a href="#first">First</a> * <a href="#second">Second</a> * <a href="#third">Third</a> * ``` * * ```php * <?php * // would return ['First', 'Second', 'Third'] * $aLinkText = $I->grabMultiple('a'); * * // would return ['#first', '#second', '#third'] * $aLinks = $I->grabMultiple('a', 'href'); * ?> * ``` * * @param $cssOrXpath * @param $attribute * @return string[] */ public function grabMultiple($cssOrXpath, $attribute = null); /** * Checks that the given element exists on the page and is visible. * You can also specify expected attributes of this element. * * ``` php * <?php * $I->seeElement('.error'); * $I->seeElement('//form/input[1]'); * $I->seeElement('input', ['name' => 'login']); * $I->seeElement('input', ['value' => '123456']); * * // strict locator in first arg, attributes in second * $I->seeElement(['css' => 'form input'], ['name' => 'login']); * ?> * ``` * * @param $selector * @param array $attributes * @return */ public function seeElement($selector, $attributes = []); /** * Checks that the given element is invisible or not present on the page. * You can also specify expected attributes of this element. * * ``` php * <?php * $I->dontSeeElement('.error'); * $I->dontSeeElement('//form/input[1]'); * $I->dontSeeElement('input', ['name' => 'login']); * $I->dontSeeElement('input', ['value' => '123456']); * ?> * ``` * * @param $selector * @param array $attributes */ public function dontSeeElement($selector, $attributes = []); /** * Checks that there are a certain number of elements matched by the given locator on the page. * * ``` php * <?php * $I->seeNumberOfElements('tr', 10); * $I->seeNumberOfElements('tr', [0,10]); // between 0 and 10 elements * ?> * ``` * @param $selector * @param mixed $expected int or int[] */ public function seeNumberOfElements($selector, $expected); /** * Checks that the given option is selected. * * ``` php * <?php * $I->seeOptionIsSelected('#form input[name=payment]', 'Visa'); * ?> * ``` * * @param $selector * @param $optionText * * @return mixed */ public function seeOptionIsSelected($selector, $optionText); /** * Checks that the given option is not selected. * * ``` php * <?php * $I->dontSeeOptionIsSelected('#form input[name=payment]', 'Visa'); * ?> * ``` * * @param $selector * @param $optionText * * @return mixed */ public function dontSeeOptionIsSelected($selector, $optionText); /** * Checks that the page title contains the given string. * * ``` php * <?php * $I->seeInTitle('Blog - Post #1'); * ?> * ``` * * @param $title * * @return mixed */ public function seeInTitle($title); /** * Checks that the page title does not contain the given string. * * @param $title * * @return mixed */ public function dontSeeInTitle($title); /** * Checks that a cookie with the given name is set. * You can set additional cookie params like `domain`, `path` as array passed in last argument. * * ``` php * <?php * $I->seeCookie('PHPSESSID'); * ?> * ``` * * @param $cookie * @param array $params * @return mixed */ public function seeCookie($cookie, array $params = []); /** * Checks that there isn't a cookie with the given name. * You can set additional cookie params like `domain`, `path` as array passed in last argument. * * @param $cookie * * @param array $params * @return mixed */ public function dontSeeCookie($cookie, array $params = []); /** * Sets a cookie with the given name and value. * You can set additional cookie params like `domain`, `path`, `expires`, `secure` in array passed as last argument. * * ``` php * <?php * $I->setCookie('PHPSESSID', 'el4ukv0kqbvoirg7nkp4dncpk3'); * ?> * ``` * * @param $name * @param $val * @param array $params * * @return mixed */ public function setCookie($name, $val, array $params = []); /** * Unsets cookie with the given name. * You can set additional cookie params like `domain`, `path` in array passed as last argument. * * @param $cookie * * @param array $params * @return mixed */ public function resetCookie($cookie, array $params = []); /** * Grabs a cookie value. * You can set additional cookie params like `domain`, `path` in array passed as last argument. * If the cookie is set by an ajax request (XMLHttpRequest), there might be some delay caused by the browser, so try `$I->wait(0.1)`. * * @param $cookie * * @param array $params * @return mixed */ public function grabCookie($cookie, array $params = []); /** * Grabs current page source code. * * @return string Current page source code. */ public function grabPageSource(); } codeception/src/Codeception/Lib/Interfaces/RequiresPackage.php000077700000000320151323602320020437 0ustar00<?php namespace Codeception\Lib\Interfaces; interface RequiresPackage { /** * Returns list of classes and corresponding packages required for this module */ public function _requires(); } codeception/src/Codeception/Lib/Interfaces/Remote.php000077700000001720151323602320016624 0ustar00<?php namespace Codeception\Lib\Interfaces; interface Remote { /** * Changes the subdomain for the 'url' configuration parameter. * Does not open a page; use `amOnPage` for that. * * ``` php * <?php * // If config is: 'http://mysite.com' * // or config is: 'http://www.mysite.com' * // or config is: 'http://company.mysite.com' * * $I->amOnSubdomain('user'); * $I->amOnPage('/'); * // moves to http://user.mysite.com/ * ?> * ``` * * @param $subdomain * * @return mixed */ public function amOnSubdomain($subdomain); /** * Open web page at the given absolute URL and sets its hostname as the base host. * * ``` php * <?php * $I->amOnUrl('http://codeception.com'); * $I->amOnPage('/quickstart'); // moves to http://codeception.com/quickstart * ?> * ``` */ public function amOnUrl($url); public function _getUrl(); } codeception/src/Codeception/Lib/Interfaces/API.php000077700000000142151323602320015777 0ustar00<?php namespace Codeception\Lib\Interfaces; /** * Modules for API testing */ interface API { } codeception/src/Codeception/Lib/Interfaces/DependsOnModule.php000077700000000537151323602320020423 0ustar00<?php namespace Codeception\Lib\Interfaces; interface DependsOnModule { /** * Specifies class or module which is required for current one. * * THis method should return array with key as class name and value as error message * [className => errorMessage * ] * @return mixed */ public function _depends(); } codeception/src/Codeception/Lib/Interfaces/ActiveRecord.php000077700000000501151323602320017737 0ustar00<?php namespace Codeception\Lib\Interfaces; interface ActiveRecord extends ORM { public function haveRecord($model, $attributes = []); public function seeRecord($model, $attributes = []); public function dontSeeRecord($model, $attributes = []); public function grabRecord($model, $attributes = []); } codeception/src/Codeception/Lib/Interfaces/DoctrineProvider.php000077700000000165151323602320020655 0ustar00<?php namespace Codeception\Lib\Interfaces; interface DoctrineProvider { public function _getEntityManager(); } codeception/src/Codeception/Lib/Interfaces/ElementLocator.php000077700000001753151323602320020314 0ustar00<?php namespace Codeception\Lib\Interfaces; interface ElementLocator { /** * Locates element using available Codeception locator types: * * * XPath * * CSS * * Strict Locator * * Use it in Helpers or GroupObject or Extension classes: * * ```php * <?php * $els = $this->getModule('{{MODULE_NAME}}')->_findElements('.items'); * $els = $this->getModule('{{MODULE_NAME}}')->_findElements(['name' => 'username']); * * $editLinks = $this->getModule('{{MODULE_NAME}}')->_findElements(['link' => 'Edit']); * // now you can iterate over $editLinks and check that all them have valid hrefs * ``` * * WebDriver module returns `Facebook\WebDriver\Remote\RemoteWebElement` instances * PhpBrowser and Framework modules return `Symfony\Component\DomCrawler\Crawler` instances * * @api * @param $locator * @return array of interactive elements */ public function _findElements($locator); } codeception/src/Codeception/Lib/Interfaces/DataMapper.php000077700000000546151323602320017414 0ustar00<?php namespace Codeception\Lib\Interfaces; interface DataMapper extends ORM, DoctrineProvider { public function haveInRepository($entity, array $data); public function seeInRepository($entity, $params = []); public function dontSeeInRepository($entity, $params = []); public function grabFromRepository($entity, $field, $params = []); } codeception/src/Codeception/Lib/Interfaces/PartedModule.php000077700000001016151323602320017754 0ustar00<?php namespace Codeception\Lib\Interfaces; /** * Interface PartedModule * * Module implementing this interface can be loaded partly. * Parts can be defined by marking methods with `@part` annotations. * Part of modules can be loaded by specifying part (or several parts) in config: * * ``` * modules: * enabled: [MyModule] * config: * MyModule: * part: usefulActions * ``` * * * @package Codeception\Lib\Interfaces */ interface PartedModule { public function _parts(); } codeception/src/Codeception/Lib/Interfaces/.htaccess000077700000000177151323602320016463 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Lib/Interfaces/PageSourceSaver.php000077700000001537151323602320020435 0ustar00<?php namespace Codeception\Lib\Interfaces; interface PageSourceSaver { /** * Saves page source of to a file * * ```php * $this->getModule('{{MODULE_NAME}}')->_savePageSource(codecept_output_dir().'page.html'); * ``` * @api * @param $filename */ public function _savePageSource($filename); /** * Use this method within an [interactive pause](https://codeception.com/docs/02-GettingStarted#Interactive-Pause) to save the HTML source code of the current page. * * ```php * <?php * $I->makeHtmlSnapshot('edit_page'); * // saved to: tests/_output/debug/edit_page.html * $I->makeHtmlSnapshot(); * // saved to: tests/_output/debug/2017-05-26_14-24-11_4b3403665fea6.html * ``` * * @param null $name */ public function makeHtmlSnapshot($name = null); } codeception/src/Codeception/Lib/Interfaces/ConflictsWithModule.php000077700000000351151323602320021316 0ustar00<?php namespace Codeception\Lib\Interfaces; interface ConflictsWithModule { /** * Returns class name or interface of module which can conflict with current. * @return string */ public function _conflicts(); } codeception/src/Codeception/Lib/Generator/Group.php000077700000003102151323602320016324 0ustar00<?php namespace Codeception\Lib\Generator; use Codeception\Util\Shared\Namespaces; use Codeception\Util\Template; class Group { use Namespaces; use Shared\Classname; protected $template = <<<EOF <?php namespace {{namespace}}; use \Codeception\Event\TestEvent; /** * Group class is Codeception Extension which is allowed to handle to all internal events. * This class itself can be used to listen events for test execution of one particular group. * It may be especially useful to create fixtures data, prepare server, etc. * * INSTALLATION: * * To use this group extension, include it to "extensions" option of global Codeception config. */ class {{class}} extends \Codeception\Platform\Group { public static \$group = '{{groupName}}'; public function _before(TestEvent \$e) { } public function _after(TestEvent \$e) { } } EOF; protected $name; protected $namespace; protected $settings; public function __construct($settings, $name) { $this->settings = $settings; $this->name = $name; $this->namespace = $this->getNamespaceString($this->settings['namespace'] . '\\Group\\' . $name); } public function produce() { $ns = $this->getNamespaceString($this->settings['namespace'] . '\\' . $this->name); return (new Template($this->template)) ->place('class', ucfirst($this->name)) ->place('name', $this->name) ->place('namespace', $this->namespace) ->place('groupName', strtolower($this->name)) ->produce(); } } codeception/src/Codeception/Lib/Generator/Snapshot.php000077700000003402151323602320017032 0ustar00<?php namespace Codeception\Lib\Generator; use Codeception\Util\Shared\Namespaces; use Codeception\Util\Template; class Snapshot { use Namespaces; protected $template = <<<EOF <?php namespace {{namespace}}; class {{name}} extends \\Codeception\\Snapshot { {{actions}} protected function fetchData() { // TODO: return a value which will be used for snapshot } } EOF; protected $actionsTemplate = <<<EOF /** * @var \\{{actorClass}}; */ protected \${{actor}}; public function __construct(\\{{actorClass}} \$I) { \$this->{{actor}} = \$I; } EOF; protected $namespace; protected $name; protected $settings; public function __construct($settings, $name) { $this->settings = $settings; $this->name = $this->getShortClassName($name); $this->namespace = $this->getNamespaceString($this->settings['namespace'] . '\\Snapshot\\' . $name); } public function produce() { return (new Template($this->template)) ->place('namespace', $this->namespace) ->place('actions', $this->produceActions()) ->place('name', $this->name) ->produce(); } protected function produceActions() { if (!isset($this->settings['actor'])) { return ''; // no actor in suite } $actor = lcfirst($this->settings['actor']); $actorClass = $this->settings['actor']; if (!empty($this->settings['namespace'])) { $actorClass = rtrim($this->settings['namespace'], '\\') . '\\' . $actorClass; } return (new Template($this->actionsTemplate)) ->place('actorClass', $actorClass) ->place('actor', $actor) ->produce(); } } codeception/src/Codeception/Lib/Generator/Test.php000077700000003112151323602320016150 0ustar00<?php namespace Codeception\Lib\Generator; use Codeception\Configuration; use Codeception\Util\Shared\Namespaces; use Codeception\Util\Template; class Test { use Namespaces; use Shared\Classname; protected $template = <<<EOF <?php {{namespace}} class {{name}}Test extends \Codeception\Test\Unit { {{tester}} protected function _before() { } protected function _after() { } // tests public function testSomeFeature() { } } EOF; protected $testerTemplate = <<<EOF /** * @var \{{actorClass}} */ protected \${{actor}}; EOF; protected $settings; protected $name; public function __construct($settings, $name) { $this->settings = $settings; $this->name = $this->removeSuffix($name, 'Test'); } public function produce() { $actor = $this->settings['actor']; if ($this->settings['namespace']) { $actor = $this->settings['namespace'] . '\\' . $actor; } $ns = $this->getNamespaceHeader($this->settings['namespace'] . '\\' . $this->name); $tester = ''; if ($this->settings['actor']) { $tester = (new Template($this->testerTemplate)) ->place('actorClass', $actor) ->place('actor', lcfirst(Configuration::config()['actor_suffix'])) ->produce(); } return (new Template($this->template)) ->place('namespace', $ns) ->place('name', $this->getShortClassName($this->name)) ->place('tester', $tester) ->produce(); } } codeception/src/Codeception/Lib/Generator/Cept.php000077700000001741151323602320016132 0ustar00<?php namespace Codeception\Lib\Generator; use Codeception\Exception\ConfigurationException; use Codeception\Util\Template; class Cept { protected $template = <<<EOF <?php {{use}} \$I = new {{actor}}(\$scenario); \$I->wantTo('perform actions and see result'); EOF; protected $settings; public function __construct($settings) { $this->settings = $settings; } public function produce() { $actor = $this->settings['actor']; if (!$actor) { throw new ConfigurationException("Cept can't be created for suite without an actor. Add `actor: SomeTester` to suite config"); } $use = ''; if (! empty($this->settings['namespace'])) { $namespace = rtrim($this->settings['namespace'], '\\'); $use = "use {$namespace}\\$actor;"; } return (new Template($this->template)) ->place('actor', $actor) ->place('use', $use) ->produce(); } } codeception/src/Codeception/Lib/Generator/Actor.php000077700000006501151323602320016306 0ustar00<?php namespace Codeception\Lib\Generator; use Codeception\Configuration; use Codeception\Lib\Di; use Codeception\Lib\ModuleContainer; use Codeception\Util\ReflectionHelper; use Codeception\Util\Template; class Actor { protected $template = <<<EOF <?php {{hasNamespace}} /** * Inherited Methods {{inheritedMethods}} * * @SuppressWarnings(PHPMD) */ class {{actor}} extends \Codeception\Actor { use _generated\{{actor}}Actions; /** * Define custom actions here */ } EOF; protected $inheritedMethodTemplate = ' * @method {{return}} {{method}}({{params}})'; protected $settings; protected $modules; protected $actions; public function __construct($settings) { $this->settings = $settings; $this->di = new Di(); $this->moduleContainer = new ModuleContainer($this->di, $settings); $modules = Configuration::modules($this->settings); foreach ($modules as $moduleName) { $this->moduleContainer->create($moduleName); } $this->modules = $this->moduleContainer->all(); $this->actions = $this->moduleContainer->getActions(); } public function produce() { $namespace = rtrim($this->settings['namespace'], '\\'); if (!isset($this->settings['actor']) && isset($this->settings['class_name'])) { $this->settings['actor'] = $this->settings['class_name']; } return (new Template($this->template)) ->place('hasNamespace', $namespace ? "namespace $namespace;" : '') ->place('actor', $this->settings['actor']) ->place('inheritedMethods', $this->prependAbstractActorDocBlocks()) ->produce(); } protected function prependAbstractActorDocBlocks() { $inherited = []; $class = new \ReflectionClass('\Codeception\\Actor'); $methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC); foreach ($methods as $method) { if ($method->name == '__call') { continue; } // skipping magic if ($method->name == '__construct') { continue; } // skipping magic $returnType = 'void'; if ($method->name == 'haveFriend') { $returnType = '\Codeception\Lib\Friend'; } $params = $this->getParamsString($method); $inherited[] = (new Template($this->inheritedMethodTemplate)) ->place('method', $method->name) ->place('params', $params) ->place('return', $returnType) ->produce(); } return implode("\n", $inherited); } /** * @param \ReflectionMethod $refMethod * @return array */ protected function getParamsString(\ReflectionMethod $refMethod) { $params = []; foreach ($refMethod->getParameters() as $param) { if ($param->isOptional()) { $params[] = '$' . $param->name . ' = ' . ReflectionHelper::getDefaultValue($param); } else { $params[] = '$' . $param->name; }; } return implode(', ', $params); } public function getActorName() { return $this->settings['actor']; } public function getModules() { return array_keys($this->modules); } } codeception/src/Codeception/Lib/Generator/Actions.php000077700000015723151323602320016644 0ustar00<?php namespace Codeception\Lib\Generator; use Codeception\Codecept; use Codeception\Configuration; use Codeception\Lib\Di; use Codeception\Lib\ModuleContainer; use Codeception\Util\ReflectionHelper; use Codeception\Util\Template; class Actions { protected $template = <<<EOF <?php //[STAMP] {{hash}} namespace {{namespace}}_generated; // This class was automatically generated by build task // You should not change it manually as it will be overwritten on next build // @codingStandardsIgnoreFile trait {{name}}Actions { /** * @return \Codeception\Scenario */ abstract protected function getScenario(); {{methods}} } EOF; protected $methodTemplate = <<<EOF /** * [!] Method is generated. Documentation taken from corresponding module. * {{doc}} * @see \{{module}}::{{method}}() */ public function {{action}}({{params}}){{return_type}} { {{return}}\$this->getScenario()->runStep(new \Codeception\Step\{{step}}('{{method}}', func_get_args())); } EOF; protected $name; protected $settings; protected $modules = []; protected $actions; protected $numMethods = 0; /** * @var array GeneratedStep[] */ protected $generatedSteps = []; public function __construct($settings) { $this->name = $settings['actor']; $this->settings = $settings; $this->di = new Di(); $modules = Configuration::modules($this->settings); $this->moduleContainer = new ModuleContainer($this->di, $settings); foreach ($modules as $moduleName) { $this->moduleContainer->create($moduleName); } $this->modules = $this->moduleContainer->all(); $this->actions = $this->moduleContainer->getActions(); $this->generatedSteps = (array) $settings['step_decorators']; } public function produce() { $namespace = rtrim($this->settings['namespace'], '\\'); $methods = []; $code = []; foreach ($this->actions as $action => $moduleName) { if (in_array($action, $methods)) { continue; } $class = new \ReflectionClass($this->modules[$moduleName]); $method = $class->getMethod($action); $code[] = $this->addMethod($method); $methods[] = $action; $this->numMethods++; } return (new Template($this->template)) ->place('namespace', $namespace ? $namespace . '\\' : '') ->place('hash', self::genHash($this->modules, $this->settings)) ->place('name', $this->name) ->place('methods', implode("\n\n ", $code)) ->produce(); } protected function addMethod(\ReflectionMethod $refMethod) { $class = $refMethod->getDeclaringClass(); $params = $this->getParamsString($refMethod); $module = $class->getName(); $body = ''; $doc = $this->addDoc($class, $refMethod); $doc = str_replace('/**', '', $doc); $doc = trim(str_replace('*/', '', $doc)); if (!$doc) { $doc = "*"; } $returnType = $this->createReturnTypeHint($refMethod); $methodTemplate = (new Template($this->methodTemplate)) ->place('module', $module) ->place('method', $refMethod->name) ->place('return_type', $returnType) ->place('return', $returnType === ': void' ? '' : 'return ') ->place('params', $params); if (0 === strpos($refMethod->name, 'see')) { $type = 'Assertion'; } elseif (0 === strpos($refMethod->name, 'am')) { $type = 'Condition'; } else { $type = 'Action'; } $body .= $methodTemplate ->place('doc', $doc) ->place('action', $refMethod->name) ->place('step', $type) ->produce(); // add auto generated steps foreach (array_unique($this->generatedSteps) as $generator) { if (!is_callable([$generator, 'getTemplate'])) { throw new \Exception("Wrong configuration for generated steps. $generator doesn't implement \Codeception\Step\GeneratedStep interface"); } $template = call_user_func([$generator, 'getTemplate'], clone $methodTemplate); if ($template) { $body .= $template->produce(); } } return $body; } /** * @param \ReflectionMethod $refMethod * @return array */ protected function getParamsString(\ReflectionMethod $refMethod) { $params = []; foreach ($refMethod->getParameters() as $param) { if ($param->isOptional()) { $params[] = '$' . $param->name . ' = ' . ReflectionHelper::getDefaultValue($param); } else { $params[] = '$' . $param->name; }; } return implode(', ', $params); } /** * @param \ReflectionClass $class * @param \ReflectionMethod $refMethod * @return string */ protected function addDoc(\ReflectionClass $class, \ReflectionMethod $refMethod) { $doc = $refMethod->getDocComment(); if (!$doc) { $interfaces = $class->getInterfaces(); foreach ($interfaces as $interface) { $i = new \ReflectionClass($interface->name); if ($i->hasMethod($refMethod->name)) { $doc = $i->getMethod($refMethod->name)->getDocComment(); break; } } } if (!$doc and $class->getParentClass()) { $parent = new \ReflectionClass($class->getParentClass()->name); if ($parent->hasMethod($refMethod->name)) { $doc = $parent->getMethod($refMethod->name)->getDocComment(); return $doc; } return $doc; } return $doc; } public static function genHash($modules, $settings) { $actions = []; foreach ($modules as $moduleName => $module) { $actions[$moduleName] = get_class_methods(get_class($module)); } return md5(Codecept::VERSION . serialize($actions) . serialize($settings['modules']) . implode(',', (array) $settings['step_decorators'])); } public function getNumMethods() { return $this->numMethods; } private function createReturnTypeHint(\ReflectionMethod $refMethod) { if (PHP_VERSION_ID < 70000) { return ''; } $returnType = $refMethod->getReturnType(); if ($returnType === null) { return ''; } if (PHP_VERSION_ID < 70100) { $returnTypeString = (string)$returnType; } else { $returnTypeString = $returnType->getName(); } return sprintf( ': %s%s%s', $returnType->allowsNull() ? '?' : '', $returnType->isBuiltin() ? '' : '\\', $returnTypeString ); } } codeception/src/Codeception/Lib/Generator/Cest.php000077700000003007151323602320016132 0ustar00<?php namespace Codeception\Lib\Generator; use Codeception\Exception\ConfigurationException; use Codeception\Util\Shared\Namespaces; use Codeception\Util\Template; class Cest { use Shared\Classname; use Namespaces; protected $template = <<<EOF <?php {{namespace}} class {{name}}Cest { public function _before({{actor}} \$I) { } // tests public function tryToTest({{actor}} \$I) { } } EOF; protected $settings; protected $name; public function __construct($className, $settings) { $this->name = $this->removeSuffix($className, 'Cest'); $this->settings = $settings; } public function produce() { $actor = $this->settings['actor']; if (!$actor) { throw new ConfigurationException("Cept can't be created for suite without an actor. Add `actor: SomeTester` to suite config"); } if (array_key_exists('suite_namespace', $this->settings)) { $namespace = rtrim($this->settings['suite_namespace'], '\\'); } else { $namespace = rtrim($this->settings['namespace'], '\\'); } $ns = $this->getNamespaceHeader($namespace.'\\'.$this->name); if ($namespace) { $ns .= "use ".$this->settings['namespace'].'\\'.$actor.";"; } return (new Template($this->template)) ->place('name', $this->getShortClassName($this->name)) ->place('namespace', $ns) ->place('actor', $actor) ->produce(); } } codeception/src/Codeception/Lib/Generator/Helper.php000077700000001723151323602320016456 0ustar00<?php namespace Codeception\Lib\Generator; use Codeception\Util\Shared\Namespaces; use Codeception\Util\Template; class Helper { use Namespaces; protected $template = <<<EOF <?php {{namespace}} // here you can define custom actions // all public methods declared in helper class will be available in \$I class {{name}} extends \\Codeception\\Module { } EOF; protected $namespace; protected $name; public function __construct($name, $namespace = '') { $this->namespace = $namespace; $this->name = $name; } public function produce() { return (new Template($this->template)) ->place('namespace', $this->getNamespaceHeader($this->namespace . '\\Helper\\' . $this->name)) ->place('name', $this->getShortClassName($this->name)) ->produce(); } public function getHelperName() { return rtrim('\\' . $this->namespace, '\\') . '\\Helper\\' . $this->name; } } codeception/src/Codeception/Lib/Generator/PageObject.php000077700000004375151323602320017250 0ustar00<?php namespace Codeception\Lib\Generator; use Codeception\Util\Shared\Namespaces; use Codeception\Util\Template; class PageObject { use Namespaces; use Shared\Classname; protected $template = <<<EOF <?php namespace {{namespace}}; class {{class}} { // include url of current page public static \$URL = ''; /** * Declare UI map for this page here. CSS or XPath allowed. * public static \$usernameField = '#username'; * public static \$formSubmitButton = "#mainForm input[type=submit]"; */ /** * Basic route example for your current URL * You can append any additional parameter to URL * and use it in tests like: Page\\Edit::route('/123-post'); */ public static function route(\$param) { return static::\$URL.\$param; } {{actions}} } EOF; protected $actionsTemplate = <<<EOF /** * @var \\{{actorClass}}; */ protected \${{actor}}; public function __construct(\\{{actorClass}} \$I) { \$this->{{actor}} = \$I; } EOF; protected $actions = ''; protected $settings; protected $name; protected $namespace; public function __construct($settings, $name) { $this->settings = $settings; $this->name = $this->getShortClassName($name); $this->namespace = $this->getNamespaceString($this->settings['namespace'] . '\\Page\\' . $name); } public function produce() { return (new Template($this->template)) ->place('namespace', $this->namespace) ->place('actions', $this->produceActions()) ->place('class', $this->name) ->produce(); } protected function produceActions() { if (!isset($this->settings['actor'])) { return ''; // global pageobject } $actor = lcfirst($this->settings['actor']); $actorClass = $this->settings['actor']; if (!empty($this->settings['namespace'])) { $actorClass = rtrim($this->settings['namespace'], '\\') . '\\' . $actorClass; } return (new Template($this->actionsTemplate)) ->place('actorClass', $actorClass) ->place('actor', $actor) ->place('pageObject', $this->name) ->produce(); } } codeception/src/Codeception/Lib/Generator/Feature.php000077700000000741151323602320016631 0ustar00<?php namespace Codeception\Lib\Generator; use Codeception\Util\Template; class Feature { protected $template = <<<EOF Feature: {{name}} In order to ... As a ... I need to ... Scenario: try {{name}} EOF; protected $name; public function __construct($name) { $this->name = $name; } public function produce() { return (new Template($this->template)) ->place('name', $this->name) ->produce(); } } codeception/src/Codeception/Lib/Generator/Shared/Classname.php000077700000000406151323602320020350 0ustar00<?php namespace Codeception\Lib\Generator\Shared; trait Classname { protected function removeSuffix($classname, $suffix) { $classname = preg_replace('~\.php$~', '', $classname); return preg_replace("~$suffix$~", '', $classname); } } codeception/src/Codeception/Lib/Generator/Shared/.htaccess000077700000000177151323602320017534 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Lib/Generator/.htaccess000077700000000177151323602320016326 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Lib/Generator/StepObject.php000077700000003244151323602320017301 0ustar00<?php namespace Codeception\Lib\Generator; use Codeception\Exception\ConfigurationException; use Codeception\Util\Shared\Namespaces; use Codeception\Util\Template; class StepObject { use Namespaces; use Shared\Classname; protected $template = <<<EOF <?php namespace {{namespace}}; class {{name}} extends {{actorClass}} { {{actions}} } EOF; protected $actionTemplate = <<<EOF public function {{action}}() { \$I = \$this; } EOF; protected $settings; protected $name; protected $actions = ''; public function __construct($settings, $name) { $this->settings = $settings; $this->name = $this->getShortClassName($name); $this->namespace = $this->getNamespaceString($this->settings['namespace'] . '\\Step\\' . $name); } public function produce() { $actor = $this->settings['actor']; if (!$actor) { throw new ConfigurationException("Steps can't be created for suite without an actor"); } $ns = $this->getNamespaceString($this->settings['namespace'] . '\\' . $actor . '\\' . $this->name); $ns = ltrim($ns, '\\'); $extended = '\\' . ltrim('\\' . $this->settings['namespace'] . '\\' . $actor, '\\'); return (new Template($this->template)) ->place('namespace', $this->namespace) ->place('name', $this->name) ->place('actorClass', $extended) ->place('actions', $this->actions) ->produce(); } public function createAction($action) { $this->actions .= (new Template($this->actionTemplate)) ->place('action', $action) ->produce(); } } codeception/src/Codeception/Lib/Generator/GherkinSnippets.php000077700000011310151323602320020345 0ustar00<?php namespace Codeception\Lib\Generator; use Behat\Gherkin\Node\StepNode; use Codeception\Test\Loader\Gherkin; use Codeception\Util\Template; use Symfony\Component\Finder\Finder; class GherkinSnippets { protected $template = <<<EOF /** * @{{type}} {{text}} */ public function {{methodName}}({{params}}) { throw new \PHPUnit\Framework\IncompleteTestError("Step `{{text}}` is not defined"); } EOF; protected $snippets = []; protected $processed = []; protected $features = []; public function __construct($settings, $test = null) { $loader = new Gherkin($settings); $pattern = $loader->getPattern(); $path = $settings['path']; if (!empty($test)) { $path = $settings['path'].'/'.$test; if (preg_match($pattern, $test)) { $path = dirname($path); $pattern = basename($test); } } $finder = Finder::create() ->files() ->sortByName() ->in($path) ->followLinks() ->name($pattern); foreach ($finder as $file) { $pathname = str_replace("//", "/", $file->getPathname()); $loader->loadTests($pathname); } $availableSteps = $loader->getSteps(); $allSteps = []; foreach ($availableSteps as $stepGroup) { $allSteps = array_merge($allSteps, $stepGroup); } foreach ($loader->getTests() as $test) { /** @var $test \Codeception\Test\Gherkin **/ $steps = $test->getScenarioNode()->getSteps(); if ($test->getFeatureNode()->hasBackground()) { $steps = array_merge($steps, $test->getFeatureNode()->getBackground()->getSteps()); } foreach ($steps as $step) { $matched = false; $text = $step->getText(); if (self::stepHasPyStringArgument($step)) { // pretend it is inline argument $text .= ' ""'; } foreach (array_keys($allSteps) as $pattern) { if (preg_match($pattern, $text)) { $matched = true; break; } } if (!$matched) { $this->addSnippet($step); $file = str_ireplace($settings['path'], '', $test->getFeatureNode()->getFile()); if (!in_array($file, $this->features)) { $this->features[] = $file; } } } } } public function addSnippet(StepNode $step) { $args = []; $pattern = $step->getText(); // match numbers (not in quotes) if (preg_match_all('~([\d\.])(?=([^"]*"[^"]*")*[^"]*$)~', $pattern, $matches)) { foreach ($matches[1] as $num => $param) { $num++; $args[] = '$num' . $num; $pattern = str_replace($param, ":num$num", $pattern); } } // match quoted string if (preg_match_all('~"(.*?)"~', $pattern, $matches)) { foreach ($matches[1] as $num => $param) { $num++; $args[] = '$arg' . $num; $pattern = str_replace('"'.$param.'"', ":arg$num", $pattern); } } // Has multiline argument at the end of step? if (self::stepHasPyStringArgument($step)) { $num = count($args) + 1; $pattern .= " :arg$num"; $args[] = '$arg' . $num; } if (in_array($pattern, $this->processed)) { return; } $methodName = preg_replace('~(\s+?|\'|\"|\W)~', '', ucwords(preg_replace('~"(.*?)"|\d+~', '', $step->getText()))); if (empty($methodName)) { $methodName = 'step_' . substr(sha1($pattern), 0, 9); } $this->snippets[] = (new Template($this->template)) ->place('type', $step->getKeywordType()) ->place('text', $pattern) ->place('methodName', lcfirst($methodName)) ->place('params', implode(', ', $args)) ->produce(); $this->processed[] = $pattern; } public function getSnippets() { return $this->snippets; } public function getFeatures() { return $this->features; } public static function stepHasPyStringArgument(StepNode $step) { if ($step->hasArguments()) { $stepArgs = $step->getArguments(); if ($stepArgs[count($stepArgs) - 1]->getNodeType() == "PyString") { return true; } } return false; } } codeception/src/Codeception/Lib/Notification.php000077700000001366151323602320015742 0ustar00<?php namespace Codeception\Lib; class Notification { protected static $messages = []; public static function warning($message, $location) { self::$messages[] = 'WARNING: ' . self::formatMessage($message, $location); } public static function deprecate($message, $location = '') { self::$messages[] = 'DEPRECATION: ' . self::formatMessage($message, $location); } private static function formatMessage($message, $location = '') { if ($location) { return "<bold>$message</bold> <info>$location</info>"; } return $message; } public static function all() { $messages = self::$messages; self::$messages = []; return $messages; } } codeception/src/Codeception/Lib/Console/MessageFactory.php000077700000002245151323602320017627 0ustar00<?php namespace Codeception\Lib\Console; use SebastianBergmann\Comparator\ComparisonFailure; /** * MessageFactory **/ class MessageFactory { /** * @var DiffFactory */ protected $diffFactory; /** * @var Output */ private $output; /** * @var Colorizer */ protected $colorizer; /** * MessageFactory constructor. * @param Output $output */ public function __construct(Output $output) { $this->output = $output; $this->diffFactory = new DiffFactory(); $this->colorizer = new Colorizer(); } /** * @param string $text * @return Message */ public function message($text = '') { return new Message($text, $this->output); } /** * @param ComparisonFailure $failure * @return string */ public function prepareComparisonFailureMessage(ComparisonFailure $failure) { $diff = $this->diffFactory->createDiff($failure); if (!$diff) { return ''; } $diff = $this->colorizer->colorize($diff); return "\n<comment>- Expected</comment> | <info>+ Actual</info>\n$diff"; } } codeception/src/Codeception/Lib/Console/Colorizer.php000077700000001521151323602320016657 0ustar00<?php namespace Codeception\Lib\Console; use Symfony\Component\Console\Formatter\OutputFormatter; class Colorizer { /** * @param string $string * @return string */ public function colorize($string = '') { $fp = fopen('php://memory', 'r+'); fwrite($fp, $string); rewind($fp); $colorizedMessage = ''; while ($line = fgets($fp)) { $char = $line[0]; $line = OutputFormatter::escape(trim($line)); switch ($char) { case '+': $line = "<info>$line</info>"; break; case '-': $line = "<comment>$line</comment>"; break; } $colorizedMessage .= $line . "\n"; } return trim($colorizedMessage); } } codeception/src/Codeception/Lib/Console/Message.php000077700000005446151323602320016305 0ustar00<?php namespace Codeception\Lib\Console; use Symfony\Component\Console\Output\OutputInterface; class Message { protected $output; protected $message; public function __construct($message, Output $output = null) { $this->message = $message; $this->output = $output; } public function with($param) { $args = array_merge([$this->message], func_get_args()); $this->message = call_user_func_array('sprintf', $args); return $this; } public function style($name) { $this->message = sprintf('<%s>%s</%s>', $name, $this->message, $name); return $this; } public function width($length, $char = ' ') { $message_length = $this->getLength(); if ($message_length < $length) { $this->message .= str_repeat($char, $length - $message_length); } return $this; } public function cut($length) { $this->message = mb_substr($this->message, 0, $length, 'utf-8'); return $this; } public function write($verbose = OutputInterface::VERBOSITY_NORMAL) { if ($verbose > $this->output->getVerbosity()) { return; } $this->output->write($this->message); } public function writeln($verbose = OutputInterface::VERBOSITY_NORMAL) { if ($verbose > $this->output->getVerbosity()) { return; } $this->output->writeln($this->message); } public function prepend($string) { if ($string instanceof Message) { $string = $string->getMessage(); } $this->message = $string . $this->message; return $this; } public function append($string) { if ($string instanceof Message) { $string = $string->getMessage(); } $this->message .= $string; return $this; } public function apply($func) { $this->message = call_user_func($func, $this->message); return $this; } public function center($char) { $this->message = $char . $this->message . $char; return $this; } /** * @return mixed */ public function getMessage() { return $this->message; } public function block($style) { $this->message = $this->output->formatHelper->formatBlock($this->message, $style, true); return $this; } public function getLength($includeTags = false) { return mb_strwidth($includeTags ? $this->message : strip_tags($this->message), 'utf-8'); } public static function ucfirst($text) { return mb_strtoupper(mb_substr($text, 0, 1, 'utf-8'), 'utf-8') . mb_substr($text, 1, null, 'utf-8'); } public function __toString() { return $this->message; } } codeception/src/Codeception/Lib/Console/DiffFactory.php000077700000001502151323602320017106 0ustar00<?php namespace Codeception\Lib\Console; use SebastianBergmann\Comparator\ComparisonFailure; use SebastianBergmann\Diff\Differ; /** * DiffFactory **/ class DiffFactory { /** * @param ComparisonFailure $failure * @return string|null */ public function createDiff(ComparisonFailure $failure) { $diff = $this->getDiff($failure->getExpectedAsString(), $failure->getActualAsString()); if (!$diff) { return null; } return $diff; } /** * @param string $expected * @param string $actual * @return string */ private function getDiff($expected = '', $actual = '') { if (!$actual && !$expected) { return ''; } $differ = new Differ(''); return $differ->diff($expected, $actual); } } codeception/src/Codeception/Lib/Console/ReplHistory.php000077700000002312151323602320017172 0ustar00<?php namespace Codeception\Lib\Console; class ReplHistory { protected $outputFile; protected $stashedCommands = []; /** * @var ReplHistory */ protected static $instance; private function __construct() { $this->outputFile = codecept_output_dir('stashed-commands'); if (file_exists($this->outputFile)) { unlink($this->outputFile); } } /** * @return ReplHistory */ public static function getInstance() { if (static::$instance == null) { static::$instance = new static(); } return static::$instance; } public function add($command) { $this->stashedCommands[] = $command; } public function getAll() { return $this->stashedCommands; } public function clear() { $this->stashedCommands = []; } public function save() { if (empty($this->stashedCommands)) { return; } file_put_contents($this->outputFile, implode("\n", $this->stashedCommands) . "\n", FILE_APPEND); codecept_debug("Stashed commands have been saved to {$this->outputFile}"); $this->clear(); } } codeception/src/Codeception/Lib/Console/Output.php000077700000006222151323602320016212 0ustar00<?php namespace Codeception\Lib\Console; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Helper\FormatterHelper; use Symfony\Component\Console\Output\ConsoleOutput; class Output extends ConsoleOutput { protected $config = [ 'colors' => true, 'verbosity' => self::VERBOSITY_NORMAL, 'interactive' => true ]; /** * @var \Symfony\Component\Console\Helper\FormatterHelper */ public $formatHelper; public $waitForDebugOutput = true; protected $isInteractive = false; public function __construct($config) { $this->config = array_merge($this->config, $config); // enable interactive output mode for CLI $this->isInteractive = $this->config['interactive'] && isset($_SERVER['TERM']) && php_sapi_name() == 'cli' && $_SERVER['TERM'] != 'linux'; $formatter = new OutputFormatter($this->config['colors']); $formatter->setStyle('default', new OutputFormatterStyle()); $formatter->setStyle('bold', new OutputFormatterStyle(null, null, ['bold'])); $formatter->setStyle('focus', new OutputFormatterStyle('magenta', null, ['bold'])); $formatter->setStyle('ok', new OutputFormatterStyle('green', null, ['bold'])); $formatter->setStyle('error', new OutputFormatterStyle('white', 'red', ['bold'])); $formatter->setStyle('fail', new OutputFormatterStyle('red', null, ['bold'])); $formatter->setStyle('pending', new OutputFormatterStyle('yellow', null, ['bold'])); $formatter->setStyle('debug', new OutputFormatterStyle('cyan')); $formatter->setStyle('comment', new OutputFormatterStyle('yellow')); $formatter->setStyle('info', new OutputFormatterStyle('green')); $this->formatHelper = new FormatterHelper(); parent::__construct($this->config['verbosity'], $this->config['colors'], $formatter); } public function isInteractive() { return $this->isInteractive; } protected function clean($message) { // clear json serialization $message = str_replace('\/', '/', $message); return $message; } public function debug($message) { $message = print_r($message, true); $message = str_replace("\n", "\n ", $message); $message = $this->clean($message); $message = OutputFormatter::escape($message); if ($this->waitForDebugOutput) { $this->writeln(''); $this->waitForDebugOutput = false; } $this->writeln("<debug> $message</debug>"); } public function message($message) { $message = call_user_func_array('sprintf', func_get_args()); return new Message($message, $this); } public function exception(\Exception $e) { $class = get_class($e); $this->writeln(""); $this->writeln("(![ $class ]!)"); $this->writeln($e->getMessage()); $this->writeln(""); } public function notification($message) { $this->writeln("<comment>$message</comment>"); } } codeception/src/Codeception/Lib/Console/.htaccess000077700000000177151323602320016002 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Lib/.htaccess000077700000000177151323602320014400 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Lib/Actor/Shared/Friend.php000077700000001311151323602320016767 0ustar00<?php namespace Codeception\Lib\Actor\Shared; use Codeception\Lib\Friend as LibFriend; use Codeception\Scenario; trait Friend { protected $friends = []; /** * @return Scenario */ abstract protected function getScenario(); /** * @param $name * @param $actorClass * @return \Codeception\Lib\Friend */ public function haveFriend($name, $actorClass = null) { if (!isset($this->friends[$name])) { $actor = $actorClass === null ? $this : new $actorClass($this->getScenario()); $this->friends[$name] = new LibFriend($name, $actor, $this->getScenario()->current('modules')); } return $this->friends[$name]; } } codeception/src/Codeception/Lib/Actor/Shared/Comment.php000077700000001757151323602320017200 0ustar00<?php namespace Codeception\Lib\Actor\Shared; trait Comment { /** * @return \Codeception\Scenario */ abstract protected function getScenario(); public function expectTo($prediction) { return $this->comment('I expect to ' . $prediction); } public function expect($prediction) { return $this->comment('I expect ' . $prediction); } public function amGoingTo($argumentation) { return $this->comment('I am going to ' . $argumentation); } public function am($role) { $role = trim($role); if (stripos('aeiou', $role[0]) !== false) { return $this->comment('As an ' . $role); } return $this->comment('As a ' . $role); } public function lookForwardTo($achieveValue) { return $this->comment('So that I ' . $achieveValue); } public function comment($description) { $this->getScenario()->comment($description); return $this; } } codeception/src/Codeception/Lib/Actor/Shared/Pause.php000077700000011767151323602320016655 0ustar00<?php namespace Codeception\Lib\Actor\Shared; use Codeception\Lib\Console\ReplHistory; use Symfony\Component\Console\Output\ConsoleOutput; trait Pause { public function pause() { if (!\Codeception\Util\Debug::isEnabled()) { return; } if (!class_exists('Hoa\Console\Readline\Readline')) { throw new \Exception('Hoa Console is not installed. Please add `hoa/console` to composer.json'); } $autoStash = false; $I = $this; $output = new ConsoleOutput(); $readline = new \Hoa\Console\Readline\Readline(); $readline->setAutocompleter( new \Hoa\Console\Readline\Autocompleter\Word(get_class_methods($this)) ); $stashFn = function (\Hoa\Console\Readline\Readline $self, $isManual = true) { $lastCommand = $self->previousHistory(); \Hoa\Console\Cursor::clear('↔'); if (strlen($lastCommand) > 0) { ReplHistory::getInstance()->add("\$I->{$lastCommand};"); codecept_debug("Command stashed: \$I->{$lastCommand};"); } else { codecept_debug("Nothing to stash."); } if ($isManual) { \Hoa\Console\Console::getOutput()->writeAll($self->getPrefix() . $self->getLine()); } $self->nextHistory(); return \Hoa\Console\Readline\Readline::STATE_CONTINUE; }; $clearStashFn = function (\Hoa\Console\Readline\Readline $self) { ReplHistory::getInstance()->clear(); \Hoa\Console\Cursor::clear('↔'); codecept_debug("Stash cleared."); \Hoa\Console\Console::getOutput()->writeAll($self->getPrefix() . $self->getLine()); return \Hoa\Console\Readline\Readline::STATE_CONTINUE; }; $viewStashedFn = function (\Hoa\Console\Readline\Readline $self) use ($output) { \Hoa\Console\Cursor::clear('↔'); $stashedCommands = ReplHistory::getInstance()->getAll(); if (!empty($stashedCommands)) { $output->writeln("\n<comment>Stashed commands:</comment>"); codecept_debug(implode("\n", $stashedCommands) . "\n"); } else { codecept_debug("No commands stashed."); } \Hoa\Console\Console::getOutput()->writeAll($self->getPrefix() . $self->getLine()); return \Hoa\Console\Readline\Readline::STATE_CONTINUE; }; $toggleAutoStashFn = function (\Hoa\Console\Readline\Readline $self) use (&$autoStash) { \Hoa\Console\Cursor::clear('↔'); $autoStash = !$autoStash; codecept_debug("Autostash " . ($autoStash ? 'enabled' : 'disabled') . '.'); \Hoa\Console\Console::getOutput()->writeAll($self->getPrefix() . $self->getLine()); return \Hoa\Console\Readline\Readline::STATE_CONTINUE; }; $tput = \Hoa\Console\Console::getTput(); $readline->addMapping($tput->get('key_f5'), $stashFn); $readline->addMapping($tput->get('key_f6'), $toggleAutoStashFn); $readline->addMapping($tput->get('key_f8'), $viewStashedFn); $readline->addMapping($tput->get('key_f10'), $clearStashFn); $output->writeln(" <comment>Execution PAUSED, starting interactive shell...</comment>"); $output->writeln(" Type in commands to try them:"); $output->writeln(" - <info>ENTER</info> to continue"); $output->writeln(" - <info>TAB</info> to auto-complete"); $output->writeln(" - <info>F5</info> to stash a command"); $output->writeln(" - <info>F6</info> to toggle auto-stashing of successful commands"); $output->writeln(" - <info>F8</info> to view stashed commands"); $output->writeln(" - <info>F10</info> to clear stashed commands"); $result = ''; do { $command = $readline->readLine('$I->'); // “> ” is the prefix of the line. if ($command == 'exit' || $command == '') { ReplHistory::getInstance()->save(); return; } try { $value = eval("return \$I->$command;"); if ($value) { $result = $value; if (!is_object($result)) { codecept_debug($result); } codecept_debug('>> Result saved to $result variable, you can use it in next commands'); } if ($autoStash) { call_user_func($stashFn, $readline, false); } } catch (\PHPUnit\Framework\AssertionFailedError $fail) { $output->writeln("<error>fail</error> " . $fail->getMessage()); } catch (\Exception $e) { $output->writeln("<error>error</error> " . $e->getMessage()); } catch (\Throwable $e) { $output->writeln("<error>syntax error</error> " . $e->getMessage()); } } while (true); } } codeception/src/Codeception/Lib/Actor/Shared/Retry.php000077700000000770151323602320016675 0ustar00<?php namespace Codeception\Lib\Actor\Shared; trait Retry { protected $retryNum = 1; protected $retryInterval = 100; /** * Configure number of retries and initial interval. * Interval will be doubled on each unsuccessful execution. * * Use with \$I->retryXXX() methods; * * @param $num * @param int $interval */ public function retry($num, $interval = 200) { $this->retryNum = $num; $this->retryInterval = $interval; } } codeception/src/Codeception/Lib/Actor/Shared/.htaccess000077700000000177151323602320016656 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Lib/Actor/.htaccess000077700000000177151323602320015450 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Lib/ModuleContainer.php000077700000041744151323602320016410 0ustar00<?php namespace Codeception\Lib; use Codeception\Configuration; use Codeception\Exception\ConfigurationException; use Codeception\Exception\ModuleConflictException; use Codeception\Exception\ModuleException; use Codeception\Exception\ModuleRequireException; use Codeception\Lib\Interfaces\ConflictsWithModule; use Codeception\Lib\Interfaces\DependsOnModule; use Codeception\Lib\Interfaces\PartedModule; use Codeception\Util\Annotation; /** * Class ModuleContainer * @package Codeception\Lib */ class ModuleContainer { /** * @var string */ const MODULE_NAMESPACE = '\\Codeception\\Module\\'; /** * @var integer */ const MAXIMUM_LEVENSHTEIN_DISTANCE = 5; public static $packages = [ 'AMQP' => 'codeception/module-amqp', 'Apc' => 'codeception/module-apc', 'Asserts' => 'codeception/module-asserts', 'Cli' => 'codeception/module-cli', 'DataFactory' => 'codeception/module-datafactory', 'Db' => 'codeception/module-db', 'Doctrine2' => "codeception/module-doctrine2", 'Filesystem' => 'codeception/module-filesystem', 'FTP' => 'codeception/module-ftp', 'Laravel5' => 'codeception/module-laravel5', 'Lumen' => 'codeception/module-lumen', 'Memcache' => 'codeception/module-memcache', 'MongoDb' => 'codeception/module-mongodb', 'Phalcon' => 'codeception/module-phalcon', 'PhpBrowser' => 'codeception/module-phpbrowser', 'Queue' => 'codeception/module-queue', 'Redis' => 'codeception/module-redis', 'REST' => 'codeception/module-rest', 'Sequence' => 'codeception/module-sequence', 'SOAP' => 'codeception/module-soap', 'Symfony' => 'codeception/module-symfony', 'WebDriver' => "codeception/module-webdriver", 'Yii2' => "codeception/module-yii2", 'ZendExpressive' => 'codeception/module-zendexpressive', 'ZF2' => 'codeception/module-zf2', ]; /** * @var array */ private $config; /** * @var Di */ private $di; /** * @var array */ private $modules = []; /** * @var array */ private $active = []; /** * @var array */ private $actions = []; /** * Constructor. * * @param Di $di * @param array $config */ public function __construct(Di $di, $config) { $this->di = $di; $this->di->set($this); $this->config = $config; } /** * Create a module. * * @param string $moduleName * @param bool $active * @return \Codeception\Module * @throws \Codeception\Exception\ConfigurationException * @throws \Codeception\Exception\ModuleException * @throws \Codeception\Exception\ModuleRequireException * @throws \Codeception\Exception\InjectionException */ public function create($moduleName, $active = true) { $this->active[$moduleName] = $active; $moduleClass = $this->getModuleClass($moduleName); if (!class_exists($moduleClass)) { if (isset(self::$packages[$moduleName])) { $package = self::$packages[$moduleName]; throw new ConfigurationException("Module $moduleName is not installed.\nUse Composer to install corresponding package:\n\ncomposer require $package --dev"); } throw new ConfigurationException("Module $moduleName could not be found and loaded"); } $config = $this->getModuleConfig($moduleName); if (empty($config) && !$active) { // For modules that are a dependency of other modules we want to skip the validation of the config. // This config validation is performed in \Codeception\Module::__construct(). // Explicitly setting $config to null skips this validation. $config = null; } $this->modules[$moduleName] = $module = $this->di->instantiate($moduleClass, [$this, $config], false); if ($this->moduleHasDependencies($module)) { $this->injectModuleDependencies($moduleName, $module); } // If module is not active its actions should not be included in the actor class $actions = $active ? $this->getActionsForModule($module, $config) : []; foreach ($actions as $action) { $this->actions[$action] = $moduleName; }; return $module; } /** * Does a module have dependencies? * * @param \Codeception\Module $module * @return bool */ private function moduleHasDependencies($module) { if (!$module instanceof DependsOnModule) { return false; } return (bool) $module->_depends(); } /** * Get the actions of a module. * * @param \Codeception\Module $module * @param array $config * @return array */ private function getActionsForModule($module, $config) { $reflectionClass = new \ReflectionClass($module); // Only public methods can be actions $methods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC); // Should this module be loaded partially? $configuredParts = null; if ($module instanceof PartedModule && isset($config['part'])) { $configuredParts = is_array($config['part']) ? $config['part'] : [$config['part']]; } $actions = []; foreach ($methods as $method) { if ($this->includeMethodAsAction($module, $method, $configuredParts)) { $actions[] = $method->name; } } return $actions; } /** * Should a method be included as an action? * * @param \Codeception\Module $module * @param \ReflectionMethod $method * @param array|null $configuredParts * @return bool */ private function includeMethodAsAction($module, $method, $configuredParts = null) { // Filter out excluded actions if ($module::$excludeActions && in_array($method->name, $module::$excludeActions)) { return false; } // Keep only the $onlyActions if they are specified if ($module::$onlyActions && !in_array($method->name, $module::$onlyActions)) { return false; } // Do not include inherited actions if the static $includeInheritedActions property is set to false. // However, if an inherited action is also specified in the static $onlyActions property // it should be included as an action. if (!$module::$includeInheritedActions && !in_array($method->name, $module::$onlyActions) && $method->getDeclaringClass()->getName() != get_class($module) ) { return false; } // Do not include hidden methods, methods with a name starting with an underscore if (strpos($method->name, '_') === 0) { return false; }; // If a part is configured for the module, only include actions from that part if ($configuredParts) { $moduleParts = Annotation::forMethod($module, $method->name)->fetchAll('part'); if (!array_uintersect($moduleParts, $configuredParts, 'strcasecmp')) { return false; } } return true; } /** * Is the module a helper? * * @param string $moduleName * @return bool */ private function isHelper($moduleName) { return strpos($moduleName, '\\') !== false; } /** * Get the fully qualified class name for a module. * * @param string $moduleName * @return string */ private function getModuleClass($moduleName) { if ($this->isHelper($moduleName)) { return $moduleName; } return self::MODULE_NAMESPACE . $moduleName; } /** * Is a module instantiated in this ModuleContainer? * * @param string $moduleName * @return bool */ public function hasModule($moduleName) { return isset($this->modules[$moduleName]); } /** * Get a module from this ModuleContainer. * * @param string $moduleName * @return \Codeception\Module * @throws \Codeception\Exception\ModuleException */ public function getModule($moduleName) { if (!$this->hasModule($moduleName)) { $this->throwMissingModuleExceptionWithSuggestion(__CLASS__, $moduleName); } return $this->modules[$moduleName]; } public function throwMissingModuleExceptionWithSuggestion($className, $moduleName) { $suggestedModuleNameInfo = $this->getModuleSuggestion($moduleName); throw new ModuleException($className, "Module $moduleName couldn't be connected" . $suggestedModuleNameInfo); } protected function getModuleSuggestion($missingModuleName) { $shortestLevenshteinDistance = null; $suggestedModuleName = null; foreach ($this->modules as $moduleName => $module) { $levenshteinDistance = levenshtein($missingModuleName, $moduleName); if ($shortestLevenshteinDistance === null || $levenshteinDistance <= $shortestLevenshteinDistance) { $shortestLevenshteinDistance = $levenshteinDistance; $suggestedModuleName = $moduleName; } } if ($suggestedModuleName !== null && $shortestLevenshteinDistance <= self::MAXIMUM_LEVENSHTEIN_DISTANCE) { return " (did you mean '$suggestedModuleName'?)"; } return ''; } /** * Get the module for an action. * * @param string $action * @return \Codeception\Module|null This method returns null if there is no module for $action */ public function moduleForAction($action) { if (!isset($this->actions[$action])) { return null; } return $this->modules[$this->actions[$action]]; } /** * Get all actions. * * @return array An array with actions as keys and module names as values. */ public function getActions() { return $this->actions; } /** * Get all modules. * * @return array An array with module names as keys and modules as values. */ public function all() { return $this->modules; } /** * Mock a module in this ModuleContainer. * * @param string $moduleName * @param object $mock */ public function mock($moduleName, $mock) { $this->modules[$moduleName] = $mock; } /** * Inject the dependencies of a module. * * @param string $moduleName * @param \Codeception\Lib\Interfaces\DependsOnModule $module * @throws \Codeception\Exception\ModuleException * @throws \Codeception\Exception\ModuleRequireException */ private function injectModuleDependencies($moduleName, DependsOnModule $module) { $this->checkForMissingDependencies($moduleName, $module); if (!method_exists($module, '_inject')) { throw new ModuleException($module, 'Module requires method _inject to be defined to accept dependencies'); } $dependencies = array_map(function ($dependency) { return $this->create($dependency, false); }, $this->getConfiguredDependencies($moduleName)); call_user_func_array([$module, '_inject'], $dependencies); } /** * Check for missing dependencies. * * @param string $moduleName * @param \Codeception\Lib\Interfaces\DependsOnModule $module * @throws \Codeception\Exception\ModuleException * @throws \Codeception\Exception\ModuleRequireException */ private function checkForMissingDependencies($moduleName, DependsOnModule $module) { $dependencies = $this->getModuleDependencies($module); $configuredDependenciesCount = count($this->getConfiguredDependencies($moduleName)); if ($configuredDependenciesCount < count($dependencies)) { $missingDependency = array_keys($dependencies)[$configuredDependenciesCount]; $message = sprintf( "\nThis module depends on %s\n\n\n%s", $missingDependency, $this->getErrorMessageForDependency($module, $missingDependency) ); throw new ModuleRequireException($moduleName, $message); } } /** * Get the dependencies of a module. * * @param \Codeception\Lib\Interfaces\DependsOnModule $module * @return array * @throws \Codeception\Exception\ModuleException */ private function getModuleDependencies(DependsOnModule $module) { $depends = $module->_depends(); if (!$depends) { return []; } if (!is_array($depends)) { $message = sprintf("Method _depends of module '%s' must return an array", get_class($module)); throw new ModuleException($module, $message); } return $depends; } /** * Get the configured dependencies for a module. * * @param string $moduleName * @return array */ private function getConfiguredDependencies($moduleName) { $config = $this->getModuleConfig($moduleName); if (!isset($config['depends'])) { return []; } return is_array($config['depends']) ? $config['depends'] : [$config['depends']]; } /** * Get the error message for a module dependency that is missing. * * @param \Codeception\Module $module * @param string $missingDependency * @return string */ private function getErrorMessageForDependency($module, $missingDependency) { $depends = $module->_depends(); return $depends[$missingDependency]; } /** * Get the configuration for a module. * * A module with name $moduleName can be configured at two paths in a configuration file: * - modules.config.$moduleName * - modules.enabled.$moduleName * * This method checks both locations for configuration. If there is configuration at both locations * this method merges them, where the configuration at modules.enabled.$moduleName takes precedence * over modules.config.$moduleName if the same parameters are configured at both locations. * * @param string $moduleName * @return array */ private function getModuleConfig($moduleName) { $config = isset($this->config['modules']['config'][$moduleName]) ? $this->config['modules']['config'][$moduleName] : []; if (!isset($this->config['modules']['enabled'])) { return $config; } if (!is_array($this->config['modules']['enabled'])) { return $config; } foreach ($this->config['modules']['enabled'] as $enabledModuleConfig) { if (!is_array($enabledModuleConfig)) { continue; } $enabledModuleName = key($enabledModuleConfig); if ($enabledModuleName === $moduleName) { return Configuration::mergeConfigs(reset($enabledModuleConfig), $config); } } return $config; } /** * Check if there are conflicting modules in this ModuleContainer. * * @throws \Codeception\Exception\ModuleConflictException */ public function validateConflicts() { $canConflict = []; foreach ($this->modules as $moduleName => $module) { $parted = $module instanceof PartedModule && $module->_getConfig('part'); if ($this->active[$moduleName] && !$parted) { $canConflict[] = $module; } } foreach ($canConflict as $module) { foreach ($canConflict as $otherModule) { $this->validateConflict($module, $otherModule); } } } /** * Check if the modules passed as arguments to this method conflict with each other. * * @param \Codeception\Module $module * @param \Codeception\Module $otherModule * @throws \Codeception\Exception\ModuleConflictException */ private function validateConflict($module, $otherModule) { if ($module === $otherModule || !$module instanceof ConflictsWithModule) { return; } $conflicts = $this->normalizeConflictSpecification($module->_conflicts()); if ($otherModule instanceof $conflicts) { throw new ModuleConflictException($module, $otherModule); } } /** * Normalize the return value of ConflictsWithModule::_conflicts() to a class name. * This is necessary because it can return a module name instead of the name of a class or interface. * * @param string $conflicts * @return string */ private function normalizeConflictSpecification($conflicts) { if (interface_exists($conflicts) || class_exists($conflicts)) { return $conflicts; } if ($this->hasModule($conflicts)) { return $this->getModule($conflicts); } return $conflicts; } } codeception/src/Codeception/Scenario.php000077700000010443151323602320014345 0ustar00<?php namespace Codeception; use Codeception\Event\DispatcherWrapper; use Codeception\Event\StepEvent; use Codeception\Exception\ConditionalAssertionFailed; use Codeception\Test\Metadata; class Scenario { use DispatcherWrapper; /** * @var TestInterface */ protected $test; /** * @var Metadata */ protected $metadata; /** * @var array */ protected $steps = []; /** * @var string */ protected $feature; protected $metaStep; /** * Constructor * * @param TestInterface $test */ public function __construct(TestInterface $test) { $this->metadata = $test->getMetadata(); $this->test = $test; } public function setFeature($feature) { $this->metadata->setFeature($feature); } public function getFeature() { return $this->metadata->getFeature(); } public function getGroups() { return $this->metadata->getGroups(); } public function current($key) { return $this->metadata->getCurrent($key); } public function runStep(Step $step) { $step->saveTrace(); if ($this->metaStep instanceof Step\Meta) { $step->setMetaStep($this->metaStep); } $this->steps[] = $step; $result = null; $dispatcher = $this->metadata->getService('dispatcher'); $this->dispatch($dispatcher, Events::STEP_BEFORE, new StepEvent($this->test, $step)); try { $result = $step->run($this->metadata->getService('modules')); } catch (ConditionalAssertionFailed $f) { $result = $this->test->getTestResultObject(); if (is_null($result)) { $this->dispatch($dispatcher, Events::STEP_AFTER, new StepEvent($this->test, $step)); throw $f; } else { $result->addFailure(clone($this->test), $f, $result->time()); } } catch (\Exception $e) { $this->dispatch($dispatcher, Events::STEP_AFTER, new StepEvent($this->test, $step)); throw $e; } $this->dispatch($dispatcher, Events::STEP_AFTER, new StepEvent($this->test, $step)); $step->executed = true; return $result; } public function addStep(Step $step) { $this->steps[] = $step; } /** * Returns the steps of this scenario. * * @return array */ public function getSteps() { return $this->steps; } public function getHtml() { $text = ''; foreach ($this->getSteps() as $step) { /** @var Step $step */ if ($step->getName() !== 'Comment') { $text .= $step->getHtml() . '<br/>'; } else { $text .= trim($step->getHumanizedArguments(), '"') . '<br/>'; } } $text = str_replace(['"\'', '\'"'], ["'", "'"], $text); $text = "<h3>" . mb_strtoupper('I want to ' . $this->getFeature(), 'utf-8') . "</h3>" . $text; return $text; } public function getText() { $text = ''; foreach ($this->getSteps() as $step) { $text .= $step->getPrefix() . "$step \r\n"; } $text = trim(str_replace(['"\'', '\'"'], ["'", "'"], $text)); $text = mb_strtoupper('I want to ' . $this->getFeature(), 'utf-8') . "\r\n\r\n" . $text . "\r\n\r\n"; return $text; } public function comment($comment) { $this->runStep(new \Codeception\Step\Comment($comment, [])); } public function skip($message = '') { throw new \PHPUnit\Framework\SkippedTestError($message); } public function incomplete($message = '') { throw new \PHPUnit\Framework\IncompleteTestError($message); } public function __call($method, $args) { // all methods were deprecated and removed from here trigger_error("Codeception: \$scenario->$method() has been deprecated and removed. Use annotations to pass scenario params", E_USER_DEPRECATED); } /** * @param Step\Meta $metaStep */ public function setMetaStep($metaStep) { $this->metaStep = $metaStep; } /** * @return Step\Meta */ public function getMetaStep() { return $this->metaStep; } } codeception/src/Codeception/Command/GenerateStepObject.php000077700000005075151323602320017702 0ustar00<?php namespace Codeception\Command; use Codeception\Configuration; use Codeception\Lib\Generator\StepObject as StepObjectGenerator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\Question; /** * Generates StepObject class. You will be asked for steps you want to implement. * * * `codecept g:stepobject acceptance AdminSteps` * * `codecept g:stepobject acceptance UserSteps --silent` - skip action questions * */ class GenerateStepObject extends Command { use Shared\FileSystem; use Shared\Config; protected function configure() { $this->setDefinition([ new InputArgument('suite', InputArgument::REQUIRED, 'Suite for StepObject'), new InputArgument('step', InputArgument::REQUIRED, 'StepObject name'), new InputOption('silent', '', InputOption::VALUE_NONE, 'skip verification question'), ]); } public function getDescription() { return 'Generates empty StepObject class'; } public function execute(InputInterface $input, OutputInterface $output) { $suite = $input->getArgument('suite'); $step = $input->getArgument('step'); $config = $this->getSuiteConfig($suite); $class = $this->getShortClassName($step); $path = $this->createDirectoryFor(Configuration::supportDir() . 'Step' . DIRECTORY_SEPARATOR . ucfirst($suite), $step); $dialog = $this->getHelperSet()->get('question'); $filename = $path . $class . '.php'; $helper = $this->getHelper('question'); $question = new Question("Add action to StepObject class (ENTER to exit): "); $gen = new StepObjectGenerator($config, ucfirst($suite) . '\\' . $step); if (!$input->getOption('silent')) { do { $question = new Question('Add action to StepObject class (ENTER to exit): ', null); $action = $dialog->ask($input, $output, $question); if ($action) { $gen->createAction($action); } } while ($action); } $res = $this->createFile($filename, $gen->produce()); if (!$res) { $output->writeln("<error>StepObject $filename already exists</error>"); return 1; } $output->writeln("<info>StepObject was created in $filename</info>"); return 0; } } codeception/src/Codeception/Command/DryRun.php000077700000012212151323602320015377 0ustar00<?php namespace Codeception\Command; use Codeception\Configuration; use Codeception\Event\DispatcherWrapper; use Codeception\Event\SuiteEvent; use Codeception\Event\TestEvent; use Codeception\Events; use Codeception\Subscriber\Bootstrap as BootstrapLoader; use Codeception\Subscriber\Console as ConsolePrinter; use Codeception\SuiteManager; use Codeception\Test\Interfaces\ScenarioDriven; use Codeception\Test\Test; use Codeception\Util\Maybe; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\EventDispatcher\EventDispatcher; /** * Shows step by step execution process for scenario driven tests without actually running them. * * * `codecept dry-run acceptance` * * `codecept dry-run acceptance MyCest` * * `codecept dry-run acceptance checkout.feature` * * `codecept dry-run tests/acceptance/MyCest.php` * */ class DryRun extends Command { use DispatcherWrapper; use Shared\Config; use Shared\Style; protected function configure() { $this->setDefinition( [ new InputArgument('suite', InputArgument::REQUIRED, 'suite to scan for feature files'), new InputArgument('test', InputArgument::OPTIONAL, 'tests to be loaded'), ] ); parent::configure(); } public function getDescription() { return 'Prints step-by-step scenario-driven test or a feature'; } public function execute(InputInterface $input, OutputInterface $output) { $this->addStyles($output); $suite = $input->getArgument('suite'); $test = $input->getArgument('test'); $config = $this->getGlobalConfig(); ini_set( 'memory_limit', isset($config['settings']['memory_limit']) ? $config['settings']['memory_limit'] : '1024M' ); if (! Configuration::isEmpty() && ! $test && strpos($suite, $config['paths']['tests']) === 0) { list(, $suite, $test) = $this->matchTestFromFilename($suite, $config['paths']['tests']); } $settings = $this->getSuiteConfig($suite); $dispatcher = new EventDispatcher(); $dispatcher->addSubscriber(new ConsolePrinter([ 'colors' => !$input->getOption('no-ansi'), 'steps' => true, 'verbosity' => OutputInterface::VERBOSITY_VERBOSE, ])); $dispatcher->addSubscriber(new BootstrapLoader()); $suiteManager = new SuiteManager($dispatcher, $suite, $settings); $moduleContainer = $suiteManager->getModuleContainer(); foreach (Configuration::modules($settings) as $module) { $moduleContainer->mock($module, new Maybe()); } $suiteManager->loadTests($test); $tests = $suiteManager->getSuite()->tests(); $this->dispatch($dispatcher, Events::SUITE_INIT, new SuiteEvent($suiteManager->getSuite(), null, $settings)); $this->dispatch($dispatcher, Events::SUITE_BEFORE, new SuiteEvent($suiteManager->getSuite(), null, $settings)); foreach ($tests as $test) { if ($test instanceof \PHPUnit\Framework\TestSuite\DataProvider) { foreach ($test as $t) { if ($t instanceof Test) { $this->dryRunTest($output, $dispatcher, $t); } } } if ($test instanceof Test and $test instanceof ScenarioDriven) { $this->dryRunTest($output, $dispatcher, $test); } } $this->dispatch($dispatcher, Events::SUITE_AFTER, new SuiteEvent($suiteManager->getSuite())); return 0; } protected function matchTestFromFilename($filename, $tests_path) { $filename = str_replace(['//', '\/', '\\'], '/', $filename); $res = preg_match("~^$tests_path/(.*?)/(.*)$~", $filename, $matches); if (!$res) { throw new \InvalidArgumentException("Test file can't be matched"); } return $matches; } /** * @param OutputInterface $output * @param $dispatcher * @param $test */ protected function dryRunTest(OutputInterface $output, EventDispatcher $dispatcher, Test $test) { $this->dispatch($dispatcher, Events::TEST_START, new TestEvent($test)); $this->dispatch($dispatcher, Events::TEST_BEFORE, new TestEvent($test)); try { $test->test(); } catch (\Exception $e) { } $this->dispatch($dispatcher, Events::TEST_AFTER, new TestEvent($test)); $this->dispatch($dispatcher, Events::TEST_END, new TestEvent($test)); if ($test->getMetadata()->isBlocked()) { $output->writeln(''); if ($skip = $test->getMetadata()->getSkip()) { $output->writeln("<warning> SKIPPED </warning>" . $skip); } if ($incomplete = $test->getMetadata()->getIncomplete()) { $output->writeln("<warning> INCOMPLETE </warning>" . $incomplete); } } $output->writeln(''); } } codeception/src/Codeception/Command/GenerateSnapshot.php000077700000004201151323602320017425 0ustar00<?php namespace Codeception\Command; use Codeception\Configuration; use Codeception\Lib\Generator\Snapshot as SnapshotGenerator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Generates Snapshot. * Snapshot can be used to test dynamical data. * If suite name is provided, an actor class will be included into placeholder * * * `codecept g:snapshot UserEmails` * * `codecept g:snapshot Products` * * `codecept g:snapshot acceptance UserEmails` */ class GenerateSnapshot extends Command { use Shared\FileSystem; use Shared\Config; protected function configure() { $this->setDefinition([ new InputArgument('suite', InputArgument::REQUIRED, 'Suite name or snapshot name)'), new InputArgument('snapshot', InputArgument::OPTIONAL, 'Name of snapshot'), ]); parent::configure(); } public function getDescription() { return 'Generates empty Snapshot class'; } public function execute(InputInterface $input, OutputInterface $output) { $suite = $input->getArgument('suite'); $class = $input->getArgument('snapshot'); if (!$class) { $class = $suite; $suite = null; } $conf = $suite ? $this->getSuiteConfig($suite) : $this->getGlobalConfig(); if ($suite) { $suite = DIRECTORY_SEPARATOR . ucfirst($suite); } $path = $this->createDirectoryFor(Configuration::supportDir() . 'Snapshot' . $suite, $class); $filename = $path . $this->getShortClassName($class) . '.php'; $output->writeln($filename); $gen = new SnapshotGenerator($conf, ucfirst($suite) . '\\' . $class); $res = $this->createFile($filename, $gen->produce()); if (!$res) { $output->writeln("<error>Snapshot $filename already exists</error>"); return 1; } $output->writeln("<info>Snapshot was created in $filename</info>"); return 0; } } codeception/src/Codeception/Command/GenerateTest.php000077700000003366151323602320016560 0ustar00<?php namespace Codeception\Command; use Codeception\Lib\Generator\Test as TestGenerator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Generates skeleton for Unit Test that extends `Codeception\TestCase\Test`. * * * `codecept g:test unit User` * * `codecept g:test unit "App\User"` */ class GenerateTest extends Command { use Shared\FileSystem; use Shared\Config; protected function configure() { $this->setDefinition( [ new InputArgument('suite', InputArgument::REQUIRED, 'suite where tests will be put'), new InputArgument('class', InputArgument::REQUIRED, 'class name'), ] ); parent::configure(); } public function getDescription() { return 'Generates empty unit test file in suite'; } public function execute(InputInterface $input, OutputInterface $output) { $suite = $input->getArgument('suite'); $class = $input->getArgument('class'); $config = $this->getSuiteConfig($suite); $className = $this->getShortClassName($class); $path = $this->createDirectoryFor($config['path'], $class); $filename = $this->completeSuffix($className, 'Test'); $filename = $path . $filename; $gen = new TestGenerator($config, $class); $res = $this->createFile($filename, $gen->produce()); if (!$res) { $output->writeln("<error>Test $filename already exists</error>"); return 1; } $output->writeln("<info>Test was created in $filename</info>"); return 0; } } codeception/src/Codeception/Command/GenerateSuite.php000077700000010335151323602320016724 0ustar00<?php namespace Codeception\Command; use Codeception\Configuration; use Codeception\Lib\Generator\Helper; use Codeception\Util\Template; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Codeception\Lib\Generator\Actor as ActorGenerator; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Yaml\Yaml; /** * Create new test suite. Requires suite name and actor name * * * `` * * `codecept g:suite api` -> api + ApiTester * * `codecept g:suite integration Code` -> integration + CodeTester * * `codecept g:suite frontend Front` -> frontend + FrontTester * */ class GenerateSuite extends Command { use Shared\FileSystem; use Shared\Config; use Shared\Style; protected function configure() { $this->setDefinition([ new InputArgument('suite', InputArgument::REQUIRED, 'suite to be generated'), new InputArgument('actor', InputArgument::OPTIONAL, 'name of new actor class'), ]); } public function getDescription() { return 'Generates new test suite'; } public function execute(InputInterface $input, OutputInterface $output) { $this->addStyles($output); $suite = $input->getArgument('suite'); $actor = $input->getArgument('actor'); if ($this->containsInvalidCharacters($suite)) { $output->writeln("<error>Suite name '$suite' contains invalid characters. ([A-Za-z0-9_]).</error>"); return 1; } $config = $this->getGlobalConfig(); if (!$actor) { $actor = ucfirst($suite) . $config['actor_suffix']; } $dir = Configuration::testsDir(); if (file_exists($dir . $suite . '.suite.yml')) { throw new \Exception("Suite configuration file '$suite.suite.yml' already exists."); } $this->createDirectoryFor($dir . $suite); if ($config['settings']['bootstrap']) { // generate bootstrap file $this->createFile( $dir . $suite . DIRECTORY_SEPARATOR . $config['settings']['bootstrap'], "<?php\n", true ); } $helperName = ucfirst($suite); $file = $this->createDirectoryFor( Configuration::supportDir() . "Helper", "$helperName.php" ) . "$helperName.php"; $gen = new Helper($helperName, $config['namespace']); // generate helper $this->createFile( $file, $gen->produce() ); $output->writeln("Helper <info>" . $gen->getHelperName() . "</info> was created in $file"); $yamlSuiteConfigTemplate = <<<EOF actor: {{actor}} modules: enabled: - {{helper}} EOF; $this->createFile( $dir . $suite . '.suite.yml', $yamlSuiteConfig = (new Template($yamlSuiteConfigTemplate)) ->place('actor', $actor) ->place('helper', $gen->getHelperName()) ->produce() ); Configuration::append(Yaml::parse($yamlSuiteConfig)); $actorGenerator = new ActorGenerator(Configuration::config()); $content = $actorGenerator->produce(); $file = $this->createDirectoryFor( Configuration::supportDir(), $actor ) . $this->getShortClassName($actor); $file .= '.php'; $this->createFile($file, $content); $output->writeln("Actor <info>" . $actor . "</info> was created in $file"); $output->writeln("Suite config <info>$suite.suite.yml</info> was created."); $output->writeln(' '); $output->writeln("Next steps:"); $output->writeln("1. Edit <bold>$suite.suite.yml</bold> to enable modules for this suite"); $output->writeln("2. Create first test with <bold>generate:cest testName</bold> ( or test|cept) command"); $output->writeln("3. Run tests of this suite with <bold>codecept run $suite</bold> command"); $output->writeln("<info>Suite $suite generated</info>"); return 0; } private function containsInvalidCharacters($suite) { return preg_match('#[^A-Za-z0-9_]#', $suite) ? true : false; } } codeception/src/Codeception/Command/Bootstrap.php000077700000004252151323602320016136 0ustar00<?php namespace Codeception\Command; use Codeception\Template\Bootstrap as BootstrapTemplate; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Creates default config, tests directory and sample suites for current project. * Use this command to start building a test suite. * * By default it will create 3 suites **acceptance**, **functional**, and **unit**. * * * `codecept bootstrap` - creates `tests` dir and `codeception.yml` in current dir. * * `codecept bootstrap --empty` - creates `tests` dir without suites * * `codecept bootstrap --namespace Frontend` - creates tests, and use `Frontend` namespace for actor classes and helpers. * * `codecept bootstrap --actor Wizard` - sets actor as Wizard, to have `TestWizard` actor in tests. * * `codecept bootstrap path/to/the/project` - provide different path to a project, where tests should be placed * */ class Bootstrap extends Command { protected function configure() { $this->setDefinition( [ new InputArgument('path', InputArgument::OPTIONAL, 'custom installation dir', null), new InputOption( 'namespace', 's', InputOption::VALUE_OPTIONAL, 'Namespace to add for actor classes and helpers' ), new InputOption('actor', 'a', InputOption::VALUE_OPTIONAL, 'Custom actor instead of Tester'), new InputOption('empty', 'e', InputOption::VALUE_NONE, 'Don\'t create standard suites') ] ); } public function getDescription() { return "Creates default test suites and generates all required files"; } public function execute(InputInterface $input, OutputInterface $output) { $bootstrap = new BootstrapTemplate($input, $output); if ($input->getArgument('path')) { $bootstrap->initDir($input->getArgument('path')); } $bootstrap->setup(); return 0; } } codeception/src/Codeception/Command/GenerateCest.php000077700000003662151323602320016536 0ustar00<?php namespace Codeception\Command; use Codeception\Lib\Generator\Cest as CestGenerator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Generates Cest (scenario-driven object-oriented test) file: * * * `codecept generate:cest suite Login` * * `codecept g:cest suite subdir/subdir/testnameCest.php` * * `codecept g:cest suite LoginCest -c path/to/project` * * `codecept g:cest "App\Login"` * */ class GenerateCest extends Command { use Shared\FileSystem; use Shared\Config; protected function configure() { $this->setDefinition([ new InputArgument('suite', InputArgument::REQUIRED, 'suite where tests will be put'), new InputArgument('class', InputArgument::REQUIRED, 'test name'), ]); } public function getDescription() { return 'Generates empty Cest file in suite'; } public function execute(InputInterface $input, OutputInterface $output) { $suite = $input->getArgument('suite'); $class = $input->getArgument('class'); $config = $this->getSuiteConfig($suite); $className = $this->getShortClassName($class); $path = $this->createDirectoryFor($config['path'], $class); $filename = $this->completeSuffix($className, 'Cest'); $filename = $path . $filename; if (file_exists($filename)) { $output->writeln("<error>Test $filename already exists</error>"); return 1; } $gen = new CestGenerator($class, $config); $res = $this->createFile($filename, $gen->produce()); if (!$res) { $output->writeln("<error>Test $filename already exists</error>"); return 1; } $output->writeln("<info>Test was created in $filename</info>"); return 0; } } codeception/src/Codeception/Command/Clean.php000077700000002447151323602320015207 0ustar00<?php namespace Codeception\Command; use Codeception\Configuration; use Codeception\Util\FileSystem; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Recursively cleans `output` directory and generated code. * * * `codecept clean` * */ class Clean extends Command { use Shared\Config; public function getDescription() { return 'Recursively cleans log and generated code'; } protected function execute(InputInterface $input, OutputInterface $output) { $projectDir = Configuration::projectDir(); $this->cleanProjectsRecursively($output, $projectDir); $output->writeln("Done"); return 0; } private function cleanProjectsRecursively(OutputInterface $output, $projectDir) { $logDir = Configuration::logDir(); $output->writeln("<info>Cleaning up output " . $logDir . "...</info>"); FileSystem::doEmptyDir($logDir); $config = Configuration::config($projectDir); $subProjects = $config['include']; foreach ($subProjects as $subProject) { $subProjectDir = $projectDir . $subProject; $this->cleanProjectsRecursively($output, $subProjectDir); } } } codeception/src/Codeception/Command/Completion.php000077700000005223151323602320016271 0ustar00<?php namespace Codeception\Command; if (!class_exists('Stecman\Component\Symfony\Console\BashCompletion\Completion')) { echo "Please install `stecman/symfony-console-completion\n` to enable auto completion"; return; } use Codeception\Configuration; use Stecman\Component\Symfony\Console\BashCompletion\Completion as ConsoleCompletion; use Stecman\Component\Symfony\Console\BashCompletion\CompletionCommand; use Stecman\Component\Symfony\Console\BashCompletion\CompletionHandler; use Stecman\Component\Symfony\Console\BashCompletion\Completion\ShellPathCompletion; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Completion extends CompletionCommand { protected function configureCompletion(CompletionHandler $handler) { // Can't set for all commands, because it wouldn't work well with generate:suite $suiteCommands = [ 'run', 'config:validate', 'console', 'dry-run', 'generate:cept', 'generate:cest', 'generate:feature', 'generate:phpunit', 'generate:scenarios', 'generate:stepobject', 'generate:test', 'gherkin:snippets', 'gherkin:steps' ]; foreach ($suiteCommands as $suiteCommand) { $handler->addHandler(new ConsoleCompletion( $suiteCommand, 'suite', ConsoleCompletion::TYPE_ARGUMENT, Configuration::suites() )); } $handler->addHandlers([ new ShellPathCompletion( ConsoleCompletion::ALL_COMMANDS, 'path', ConsoleCompletion::TYPE_ARGUMENT ), new ShellPathCompletion( ConsoleCompletion::ALL_COMMANDS, 'test', ConsoleCompletion::TYPE_ARGUMENT ), ]); } protected function execute(InputInterface $input, OutputInterface $output) { if ($input->getOption('generate-hook') && $input->getOption('use-vendor-bin')) { global $argv; $argv[0] = 'vendor/bin/' . basename($argv[0]); } parent::execute($input, $output); return 0; } protected function createDefinition() { $definition = parent::createDefinition(); $definition->addOption(new InputOption( 'use-vendor-bin', null, InputOption::VALUE_NONE, 'Use the vendor bin for autocompletion.' )); return $definition; } } codeception/src/Codeception/Command/GenerateScenarios.php000077700000011407151323602320017562 0ustar00<?php namespace Codeception\Command; use Codeception\Configuration; use Codeception\Exception\ConfigurationException; use Codeception\Test\Cest; use Codeception\Test\Interfaces\ScenarioDriven; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\EventDispatcher\EventDispatcher; /** * Generates user-friendly text scenarios from scenario-driven tests (Cest, Cept). * * * `codecept g:scenarios acceptance` - for all acceptance tests * * `codecept g:scenarios acceptance --format html` - in html format * * `codecept g:scenarios acceptance --path doc` - generate scenarios to `doc` dir */ class GenerateScenarios extends Command { use Shared\FileSystem; use Shared\Config; protected function configure() { $this->setDefinition([ new InputArgument('suite', InputArgument::REQUIRED, 'suite from which texts should be generated'), new InputOption('path', 'p', InputOption::VALUE_REQUIRED, 'Use specified path as destination instead of default'), new InputOption('single-file', '', InputOption::VALUE_NONE, 'Render all scenarios to only one file'), new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Specify output format: html or text (default)', 'text'), ]); parent::configure(); } public function getDescription() { return 'Generates text representation for all scenarios'; } protected function execute(InputInterface $input, OutputInterface $output) { $suite = $input->getArgument('suite'); $suiteConf = $this->getSuiteConfig($suite); $path = $input->getOption('path') ? $input->getOption('path') : Configuration::dataDir() . 'scenarios'; $format = $input->getOption('format'); @mkdir($path); if (!is_writable($path)) { throw new ConfigurationException( "Path $path is not writable. Please, set valid permissions for folder to store scenarios." ); } $path = $path . DIRECTORY_SEPARATOR . $suite; if (!$input->getOption('single-file')) { @mkdir($path); } $suiteManager = new \Codeception\SuiteManager(new EventDispatcher(), $suite, $suiteConf); if ($suiteConf['bootstrap']) { if (file_exists($suiteConf['path'] . $suiteConf['bootstrap'])) { require_once $suiteConf['path'] . $suiteConf['bootstrap']; } } $tests = $this->getTests($suiteManager); $scenarios = ""; foreach ($tests as $test) { if (!($test instanceof ScenarioDriven)) { continue; } $feature = $test->getScenarioText($format); $name = $this->underscore(basename($test->getFileName(), '.php')); // create separate file for each test in Cest if ($test instanceof Cest && !$input->getOption('single-file')) { $name .= '.' . $this->underscore($test->getTestMethod()); } if ($input->getOption('single-file')) { $scenarios .= $feature; $output->writeln("* $name rendered"); } else { $feature = $this->decorate($feature, $format); $this->createFile($path . DIRECTORY_SEPARATOR . $name . $this->formatExtension($format), $feature, true); $output->writeln("* $name generated"); } } if ($input->getOption('single-file')) { $this->createFile($path . $this->formatExtension($format), $this->decorate($scenarios, $format), true); } return 0; } protected function decorate($text, $format) { switch ($format) { case 'text': return $text; case 'html': return "<html><body>$text</body></html>"; } } protected function getTests($suiteManager) { $suiteManager->loadTests(); return $suiteManager->getSuite()->tests(); } protected function formatExtension($format) { switch ($format) { case 'text': return '.txt'; case 'html': return '.html'; } } private function underscore($name) { $name = preg_replace('/([A-Z]+)([A-Z][a-z])/', '\\1_\\2', $name); $name = preg_replace('/([a-z\d])([A-Z])/', '\\1_\\2', $name); $name = str_replace(['/', '\\'], ['.', '.'], $name); $name = preg_replace('/_Cept$/', '', $name); $name = preg_replace('/_Cest$/', '', $name); return $name; } } codeception/src/Codeception/Command/GenerateGroup.php000077700000003207151323602320016727 0ustar00<?php namespace Codeception\Command; use Codeception\Configuration; use Codeception\Lib\Generator\Group as GroupGenerator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Creates empty GroupObject - extension which handles all group events. * * * `codecept g:group Admin` */ class GenerateGroup extends Command { use Shared\FileSystem; use Shared\Config; protected function configure() { $this->setDefinition([ new InputArgument('group', InputArgument::REQUIRED, 'Group class name'), ]); } public function getDescription() { return 'Generates Group subscriber'; } public function execute(InputInterface $input, OutputInterface $output) { $config = $this->getGlobalConfig(); $group = $input->getArgument('group'); $class = ucfirst($group); $path = $this->createDirectoryFor(Configuration::supportDir() . 'Group' . DIRECTORY_SEPARATOR, $class); $filename = $path . $class . '.php'; $gen = new GroupGenerator($config, $group); $res = $this->createFile($filename, $gen->produce()); if (!$res) { $output->writeln("<error>Group $filename already exists</error>"); return 1; } $output->writeln("<info>Group extension was created in $filename</info>"); $output->writeln( 'To use this group extension, include it to "extensions" option of global Codeception config.' ); return 0; } } codeception/src/Codeception/Command/GenerateEnvironment.php000077700000003475151323602320020146 0ustar00<?php namespace Codeception\Command; use Codeception\Configuration; use Codeception\Exception\ConfigurationException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Generates empty environment configuration file into envs dir: * * * `codecept g:env firefox` * * Required to have `envs` path to be specified in `codeception.yml` */ class GenerateEnvironment extends Command { use Shared\FileSystem; use Shared\Config; protected function configure() { $this->setDefinition([ new InputArgument('env', InputArgument::REQUIRED, 'Environment name'), ]); } public function getDescription() { return 'Generates empty environment config'; } public function execute(InputInterface $input, OutputInterface $output) { $conf = $this->getGlobalConfig(); if (!Configuration::envsDir()) { throw new ConfigurationException( "Path for environments configuration is not set.\n" . "Please specify envs path in your `codeception.yml`\n \n" . "envs: tests/_envs" ); } $relativePath = $conf['paths']['envs']; $env = $input->getArgument('env'); $file = "$env.yml"; $path = $this->createDirectoryFor($relativePath, $file); $saved = $this->createFile($path . $file, "# `$env` environment config goes here"); if ($saved) { $output->writeln("<info>$env config was created in $relativePath/$file</info>"); return 0; } else { $output->writeln("<error>File $relativePath/$file already exists</error>"); return 1; } } } codeception/src/Codeception/Command/GenerateFeature.php000077700000003663151323602320017234 0ustar00<?php namespace Codeception\Command; use Codeception\Lib\Generator\Feature; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Generates Feature file (in Gherkin): * * * `codecept generate:feature suite Login` * * `codecept g:feature suite subdir/subdir/login.feature` * * `codecept g:feature suite login.feature -c path/to/project` * */ class GenerateFeature extends Command { use Shared\FileSystem; use Shared\Config; protected function configure() { $this->setDefinition([ new InputArgument('suite', InputArgument::REQUIRED, 'suite to be tested'), new InputArgument('feature', InputArgument::REQUIRED, 'feature to be generated'), new InputOption('config', 'c', InputOption::VALUE_OPTIONAL, 'Use custom path for config'), ]); } public function getDescription() { return 'Generates empty feature file in suite'; } public function execute(InputInterface $input, OutputInterface $output) { $suite = $input->getArgument('suite'); $filename = $input->getArgument('feature'); $config = $this->getSuiteConfig($suite); $this->createDirectoryFor($config['path'], $filename); $gen = new Feature(basename($filename)); if (!preg_match('~\.feature$~', $filename)) { $filename .= '.feature'; } $full_path = rtrim($config['path'], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename; $res = $this->createFile($full_path, $gen->produce()); if (!$res) { $output->writeln("<error>Feature $filename already exists</error>"); return 1; } $output->writeln("<info>Feature was created in $full_path</info>"); return 0; } } codeception/src/Codeception/Command/Run.php000077700000056723151323602320014737 0ustar00<?php namespace Codeception\Command; use Codeception\Codecept; use Codeception\Configuration; use Codeception\Util\PathResolver; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Executes tests. * * Usage: * * * `codecept run acceptance`: run all acceptance tests * * `codecept run tests/acceptance/MyCept.php`: run only MyCept * * `codecept run acceptance MyCept`: same as above * * `codecept run acceptance MyCest:myTestInIt`: run one test from a Cest * * `codecept run acceptance checkout.feature`: run feature-file * * `codecept run acceptance -g slow`: run tests from *slow* group * * `codecept run unit,functional`: run only unit and functional suites * * Verbosity modes: * * * `codecept run -v`: * * `codecept run --steps`: print step-by-step execution * * `codecept run -vv`: * * `codecept run --debug`: print steps and debug information * * `codecept run -vvv`: print internal debug information * * Load config: * * * `codecept run -c path/to/another/config`: from another dir * * `codecept run -c another_config.yml`: from another config file * * Override config values: * * * `codecept run -o "settings: shuffle: true"`: enable shuffle * * `codecept run -o "settings: lint: false"`: disable linting * * `codecept run -o "reporters: report: \Custom\Reporter" --report`: use custom reporter * * Run with specific extension * * * `codecept run --ext Recorder` run with Recorder extension enabled * * `codecept run --ext DotReporter` run with DotReporter printer * * `codecept run --ext "My\Custom\Extension"` run with an extension loaded by class name * * Full reference: * ``` * Arguments: * suite suite to be tested * test test to be run * * Options: * -o, --override=OVERRIDE Override config values (multiple values allowed) * --config (-c) Use custom path for config * --report Show output in compact style * --html Generate html with results (default: "report.html") * --xml Generate JUnit XML Log (default: "report.xml") * --phpunit-xml Generate PhpUnit XML Log (default: "phpunit-report.xml") * --no-redirect Do not redirect to Composer-installed version in vendor/codeception * --tap Generate Tap Log (default: "report.tap.log") * --json Generate Json Log (default: "report.json") * --colors Use colors in output * --no-colors Force no colors in output (useful to override config file) * --silent Only outputs suite names and final results * --steps Show steps in output * --debug (-d) Show debug and scenario output * --bootstrap Execute bootstrap script before the test * --coverage Run with code coverage (default: "coverage.serialized") * --coverage-html Generate CodeCoverage HTML report in path (default: "coverage") * --coverage-xml Generate CodeCoverage XML report in file (default: "coverage.xml") * --coverage-text Generate CodeCoverage text report in file (default: "coverage.txt") * --coverage-phpunit Generate CodeCoverage PHPUnit report in file (default: "coverage-phpunit") * --coverage-cobertura Generate CodeCoverage Cobertura report in file (default: "coverage-cobertura") * --no-exit Don't finish with exit code * --group (-g) Groups of tests to be executed (multiple values allowed) * --skip (-s) Skip selected suites (multiple values allowed) * --skip-group (-x) Skip selected groups (multiple values allowed) * --env Run tests in selected environments. (multiple values allowed, environments can be merged with ',') * --fail-fast (-f) Stop after first failure * --no-rebuild Do not rebuild actor classes on start * --help (-h) Display this help message. * --quiet (-q) Do not output any message. * --verbose (-v|vv|vvv) Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug * --version (-V) Display this application version. * --ansi Force ANSI output. * --no-ansi Disable ANSI output. * --no-interaction (-n) Do not ask any interactive question. * --seed Use the given seed for shuffling tests * ``` * */ class Run extends Command { use Shared\Config; /** * @var Codecept */ protected $codecept; /** * @var integer of executed suites */ protected $executed = 0; /** * @var array of options (command run) */ protected $options = []; /** * @var OutputInterface */ protected $output; /** * Sets Run arguments * @throws \Symfony\Component\Console\Exception\InvalidArgumentException */ protected function configure() { $this->setDefinition([ new InputArgument('suite', InputArgument::OPTIONAL, 'suite to be tested'), new InputArgument('test', InputArgument::OPTIONAL, 'test to be run'), new InputOption('override', 'o', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Override config values'), new InputOption('ext', 'e', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Run with extension enabled'), new InputOption('report', '', InputOption::VALUE_NONE, 'Show output in compact style'), new InputOption('html', '', InputOption::VALUE_OPTIONAL, 'Generate html with results', 'report.html'), new InputOption('xml', '', InputOption::VALUE_OPTIONAL, 'Generate JUnit XML Log', 'report.xml'), new InputOption('phpunit-xml', '', InputOption::VALUE_OPTIONAL, 'Generate PhpUnit XML Log', 'phpunit-report.xml'), new InputOption('tap', '', InputOption::VALUE_OPTIONAL, 'Generate Tap Log', 'report.tap.log'), new InputOption('json', '', InputOption::VALUE_OPTIONAL, 'Generate Json Log', 'report.json'), new InputOption('colors', '', InputOption::VALUE_NONE, 'Use colors in output'), new InputOption( 'no-colors', '', InputOption::VALUE_NONE, 'Force no colors in output (useful to override config file)' ), new InputOption('silent', '', InputOption::VALUE_NONE, 'Only outputs suite names and final results'), new InputOption('steps', '', InputOption::VALUE_NONE, 'Show steps in output'), new InputOption('debug', 'd', InputOption::VALUE_NONE, 'Show debug and scenario output'), new InputOption('bootstrap', '', InputOption::VALUE_OPTIONAL, 'Execute custom PHP script before running tests. Path can be absolute or relative to current working directory', false), new InputOption('no-redirect', '', InputOption::VALUE_NONE, 'Do not redirect to Composer-installed version in vendor/codeception'), new InputOption( 'coverage', '', InputOption::VALUE_OPTIONAL, 'Run with code coverage' ), new InputOption( 'coverage-html', '', InputOption::VALUE_OPTIONAL, 'Generate CodeCoverage HTML report in path' ), new InputOption( 'coverage-xml', '', InputOption::VALUE_OPTIONAL, 'Generate CodeCoverage XML report in file' ), new InputOption( 'coverage-text', '', InputOption::VALUE_OPTIONAL, 'Generate CodeCoverage text report in file' ), new InputOption( 'coverage-crap4j', '', InputOption::VALUE_OPTIONAL, 'Generate CodeCoverage report in Crap4J XML format' ), new InputOption( 'coverage-cobertura', '', InputOption::VALUE_OPTIONAL, 'Generate CodeCoverage report in Cobertura XML format' ), new InputOption( 'coverage-phpunit', '', InputOption::VALUE_OPTIONAL, 'Generate CodeCoverage PHPUnit report in path' ), new InputOption('no-exit', '', InputOption::VALUE_NONE, 'Don\'t finish with exit code'), new InputOption( 'group', 'g', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Groups of tests to be executed' ), new InputOption( 'skip', 's', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Skip selected suites' ), new InputOption( 'skip-group', 'x', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Skip selected groups' ), new InputOption( 'env', '', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Run tests in selected environments.' ), new InputOption('fail-fast', 'f', InputOption::VALUE_NONE, 'Stop after first failure'), new InputOption('no-rebuild', '', InputOption::VALUE_NONE, 'Do not rebuild actor classes on start'), new InputOption( 'seed', '', InputOption::VALUE_REQUIRED, 'Define random seed for shuffle setting' ), new InputOption('no-artifacts', '', InputOption::VALUE_NONE, 'Don\'t report about artifacts'), ]); parent::configure(); } public function getDescription() { return 'Runs the test suites'; } /** * Executes Run * * @param \Symfony\Component\Console\Input\InputInterface $input * @param \Symfony\Component\Console\Output\OutputInterface $output * @return int|null|void * @throws \RuntimeException */ public function execute(InputInterface $input, OutputInterface $output) { $this->ensurePhpExtIsAvailable('CURL'); $this->ensurePhpExtIsAvailable('mbstring'); $this->options = $input->getOptions(); $this->output = $output; if ($this->options['bootstrap']) { Configuration::loadBootstrap($this->options['bootstrap'], getcwd()); } // load config $config = $this->getGlobalConfig(); // update config from options if (count($this->options['override'])) { $config = $this->overrideConfig($this->options['override']); } if ($this->options['ext']) { $config = $this->enableExtensions($this->options['ext']); } if (!$this->options['colors']) { $this->options['colors'] = $config['settings']['colors']; } if (!$this->options['silent']) { $this->output->writeln( Codecept::versionString() . "\nPowered by " . \PHPUnit\Runner\Version::getVersionString() ); if ($this->options['seed']) { $this->output->writeln( "Running with seed: <info>" . $this->options['seed'] . "</info>\n" ); } } if ($this->options['debug']) { $this->output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); } $userOptions = array_intersect_key($this->options, array_flip($this->passedOptionKeys($input))); $userOptions = array_merge( $userOptions, $this->booleanOptions($input, [ 'xml' => 'report.xml', 'phpunit-xml' => 'phpunit-report.xml', 'html' => 'report.html', 'json' => 'report.json', 'tap' => 'report.tap.log', 'coverage' => 'coverage.serialized', 'coverage-xml' => 'coverage.xml', 'coverage-html' => 'coverage', 'coverage-text' => 'coverage.txt', 'coverage-crap4j' => 'crap4j.xml', 'coverage-cobertura' => 'cobertura.xml', 'coverage-phpunit' => 'coverage-phpunit']) ); $userOptions['verbosity'] = $this->output->getVerbosity(); $userOptions['interactive'] = !$input->hasParameterOption(['--no-interaction', '-n']); $userOptions['ansi'] = (!$input->hasParameterOption('--no-ansi') xor $input->hasParameterOption('ansi')); if (!$this->options['seed']) { $userOptions['seed'] = rand(); } else { $userOptions['seed'] = intval($this->options['seed']); } if ($this->options['no-colors'] || !$userOptions['ansi']) { $userOptions['colors'] = false; } if ($this->options['group']) { $userOptions['groups'] = $this->options['group']; } if ($this->options['skip-group']) { $userOptions['excludeGroups'] = $this->options['skip-group']; } if ($this->options['report']) { $userOptions['silent'] = true; } if ($this->options['coverage-xml'] or $this->options['coverage-html'] or $this->options['coverage-text'] or $this->options['coverage-crap4j'] or $this->options['coverage-phpunit']) { $this->options['coverage'] = true; } if (!$userOptions['ansi'] && $input->getOption('colors')) { $userOptions['colors'] = true; // turn on colors even in non-ansi mode if strictly passed } $suite = $input->getArgument('suite'); $test = $input->getArgument('test'); if ($this->options['group']) { $this->output->writeln(sprintf("[Groups] <info>%s</info> ", implode(', ', $this->options['group']))); } if ($input->getArgument('test')) { $this->options['steps'] = true; } if (!$test) { // Check if suite is given and is in an included path if (!empty($suite) && !empty($config['include'])) { $isIncludeTest = false; // Remember original projectDir $projectDir = Configuration::projectDir(); foreach ($config['include'] as $include) { // Find if the suite begins with an include path if (strpos($suite, $include) === 0) { // Use include config $config = Configuration::config($projectDir.$include); if (!isset($config['paths']['tests'])) { throw new \RuntimeException( sprintf("Included '%s' has no tests path configured", $include) ); } $testsPath = $include . DIRECTORY_SEPARATOR. $config['paths']['tests']; try { list(, $suite, $test) = $this->matchTestFromFilename($suite, $testsPath); $isIncludeTest = true; } catch (\InvalidArgumentException $e) { // Incorrect include match, continue trying to find one continue; } } else { $result = $this->matchSingleTest($suite, $config); if ($result) { list(, $suite, $test) = $result; } } } // Restore main config if (!$isIncludeTest) { $config = Configuration::config($projectDir); } } elseif (!empty($suite)) { $result = $this->matchSingleTest($suite, $config); if ($result) { list(, $suite, $test) = $result; } } } if ($test) { $filter = $this->matchFilteredTestName($test); $userOptions['filter'] = $filter; } if (!$this->options['silent'] && $config['settings']['shuffle']) { $this->output->writeln( "[Seed] <info>" . $userOptions['seed'] . "</info>" ); } $this->codecept = new Codecept($userOptions); if ($suite and $test) { $this->codecept->run($suite, $test, $config); } // Run all tests of given suite or all suites if (!$test) { $suites = $suite ? explode(',', $suite) : Configuration::suites(); $this->executed = $this->runSuites($suites, $this->options['skip']); if (!empty($config['include']) and !$suite) { $current_dir = Configuration::projectDir(); $suites += $config['include']; $this->runIncludedSuites($config['include'], $current_dir); } if ($this->executed === 0) { throw new \RuntimeException( sprintf("Suite '%s' could not be found", implode(', ', $suites)) ); } } $this->codecept->printResult(); if (!$input->getOption('no-exit')) { if (!$this->codecept->getResult()->wasSuccessful()) { exit(1); } } return 0; } protected function matchSingleTest($suite, $config) { // Workaround when codeception.yml is inside tests directory and tests path is set to "." // @see https://github.com/Codeception/Codeception/issues/4432 if (isset($config['paths']['tests']) && $config['paths']['tests'] === '.' && !preg_match('~^\.[/\\\]~', $suite)) { $suite = './' . $suite; } // running a single test when suite has a configured path if (isset($config['suites'])) { foreach ($config['suites'] as $s => $suiteConfig) { if (!isset($suiteConfig['path'])) { continue; } $testsPath = $config['paths']['tests'] . DIRECTORY_SEPARATOR . $suiteConfig['path']; if ($suiteConfig['path'] === '.') { $testsPath = $config['paths']['tests']; } if (preg_match("~^$testsPath/(.*?)$~", $suite, $matches)) { $matches[2] = $matches[1]; $matches[1] = $s; return $matches; } } } // Run single test without included tests if (! Configuration::isEmpty() && strpos($suite, $config['paths']['tests']) === 0) { return $this->matchTestFromFilename($suite, $config['paths']['tests']); } } /** * Runs included suites recursively * * @param array $suites * @param string $parent_dir */ protected function runIncludedSuites($suites, $parent_dir) { foreach ($suites as $relativePath) { $current_dir = rtrim($parent_dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $relativePath; $config = Configuration::config($current_dir); $suites = Configuration::suites(); $namespace = $this->currentNamespace(); $this->output->writeln( "\n<fg=white;bg=magenta>\n[$namespace]: tests from $current_dir\n</fg=white;bg=magenta>" ); $this->executed += $this->runSuites($suites, $this->options['skip']); if (!empty($config['include'])) { $this->runIncludedSuites($config['include'], $current_dir); } } } protected function currentNamespace() { $config = Configuration::config(); if (!$config['namespace']) { throw new \RuntimeException( "Can't include into runner suite without a namespace;\n" . "Please add `namespace` section into included codeception.yml file" ); } return $config['namespace']; } protected function runSuites($suites, $skippedSuites = []) { $executed = 0; foreach ($suites as $suite) { if (in_array($suite, $skippedSuites)) { continue; } if (!in_array($suite, Configuration::suites())) { continue; } $this->codecept->run($suite); $executed++; } return $executed; } protected function matchTestFromFilename($filename, $testsPath) { $testsPath = str_replace(['//', '\/', '\\'], '/', $testsPath); $filename = str_replace(['//', '\/', '\\'], '/', $filename); $res = preg_match("~^$testsPath/(.*?)(?>/(.*))?$~", $filename, $matches); if (!$res) { throw new \InvalidArgumentException("Test file can't be matched"); } if (!isset($matches[2])) { $matches[2] = null; } return $matches; } private function matchFilteredTestName(&$path) { $test_parts = explode(':', $path, 2); if (count($test_parts) > 1) { list($path, $filter) = $test_parts; // use carat to signify start of string like in normal regex // phpunit --filter matches against the fully qualified method name, so tests actually begin with : $carat_pos = strpos($filter, '^'); if ($carat_pos !== false) { $filter = substr_replace($filter, ':', $carat_pos, 1); } return $filter; } return null; } protected function passedOptionKeys(InputInterface $input) { $options = []; $request = (string)$input; $tokens = explode(' ', $request); foreach ($tokens as $token) { $token = preg_replace('~=.*~', '', $token); // strip = from options if (empty($token)) { continue; } if ($token == '--') { break; // there should be no options after ' -- ', only arguments } if (substr($token, 0, 2) === '--') { $options[] = substr($token, 2); } elseif ($token[0] === '-') { $shortOption = substr($token, 1); $options[] = $this->getDefinition()->getOptionForShortcut($shortOption)->getName(); } } return $options; } protected function booleanOptions(InputInterface $input, $options = []) { $values = []; $request = (string)$input; foreach ($options as $option => $defaultValue) { if (strpos($request, "--$option")) { $values[$option] = $input->getOption($option) ? $input->getOption($option) : $defaultValue; } else { $values[$option] = false; } } return $values; } /** * @param string $ext * @throws \Exception */ private function ensurePhpExtIsAvailable($ext) { if (!extension_loaded(strtolower($ext))) { throw new \Exception( "Codeception requires \"{$ext}\" extension installed to make tests run\n" . "If you are not sure, how to install \"{$ext}\", please refer to StackOverflow\n\n" . "Notice: PHP for Apache/Nginx and CLI can have different php.ini files.\n" . "Please make sure that your PHP you run from console has \"{$ext}\" enabled." ); } } } codeception/src/Codeception/Command/Init.php000077700000003345151323602320015066 0ustar00<?php namespace Codeception\Command; use Codeception\InitTemplate; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Init extends Command { protected function configure() { $this->setDefinition( [ new InputArgument('template', InputArgument::REQUIRED, 'Init template for the setup'), new InputOption('path', null, InputOption::VALUE_REQUIRED, 'Change current directory', null), new InputOption('namespace', null, InputOption::VALUE_OPTIONAL, 'Namespace to add for actor classes and helpers\'', null), ] ); } public function getDescription() { return "Creates test suites by a template"; } public function execute(InputInterface $input, OutputInterface $output) { $template = $input->getArgument('template'); if (class_exists($template)) { $className = $template; } else { $className = 'Codeception\Template\\' . ucfirst($template); if (!class_exists($className)) { throw new \Exception("Template from a $className can't be loaded; Init can't be executed"); } } $initProcess = new $className($input, $output); if (!$initProcess instanceof InitTemplate) { throw new \Exception("$className is not a valid template"); } if ($input->getOption('path')) { $initProcess->initDir($input->getOption('path')); } $initProcess->setup(); return 0; } } codeception/src/Codeception/Command/GenerateCept.php000077700000003305151323602320016525 0ustar00<?php namespace Codeception\Command; use Codeception\Lib\Generator\Cept; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Generates Cept (scenario-driven test) file: * * * `codecept generate:cept suite Login` * * `codecept g:cept suite subdir/subdir/testnameCept.php` * * `codecept g:cept suite LoginCept -c path/to/project` * */ class GenerateCept extends Command { use Shared\FileSystem; use Shared\Config; protected function configure() { $this->setDefinition([ new InputArgument('suite', InputArgument::REQUIRED, 'suite to be tested'), new InputArgument('test', InputArgument::REQUIRED, 'test to be run'), ]); } public function getDescription() { return 'Generates empty Cept file in suite'; } public function execute(InputInterface $input, OutputInterface $output) { $suite = $input->getArgument('suite'); $filename = $input->getArgument('test'); $config = $this->getSuiteConfig($suite); $this->createDirectoryFor($config['path'], $filename); $filename = $this->completeSuffix($filename, 'Cept'); $gen = new Cept($config); $full_path = rtrim($config['path'], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename; $res = $this->createFile($full_path, $gen->produce()); if (!$res) { $output->writeln("<error>Test $filename already exists</error>"); return 1; } $output->writeln("<info>Test was created in $full_path</info>"); return 0; } } codeception/src/Codeception/Command/GherkinSteps.php000077700000004132151323602320016564 0ustar00<?php namespace Codeception\Command; use Codeception\Test\Loader\Gherkin; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Prints all steps from all Gherkin contexts for a specific suite * * ``` * codecept gherkin:steps acceptance * ``` * */ class GherkinSteps extends Command { use Shared\Config; use Shared\Style; protected function configure() { $this->setDefinition( [ new InputArgument('suite', InputArgument::REQUIRED, 'suite to scan for feature files'), new InputOption('config', 'c', InputOption::VALUE_OPTIONAL, 'Use custom path for config'), ] ); parent::configure(); } public function getDescription() { return 'Prints all defined feature steps'; } public function execute(InputInterface $input, OutputInterface $output) { $this->addStyles($output); $suite = $input->getArgument('suite'); $config = $this->getSuiteConfig($suite); $config['describe_steps'] = true; $loader = new Gherkin($config); $steps = $loader->getSteps(); foreach ($steps as $name => $context) { /** @var $table Table **/ $table = new Table($output); $table->setHeaders(['Step', 'Implementation']); $output->writeln("Steps from <bold>$name</bold> context:"); foreach ($context as $step => $callable) { if (count($callable) < 2) { continue; } $method = $callable[0] . '::' . $callable[1]; $table->addRow([$step, $method]); } $table->render(); } if (!isset($table)) { $output->writeln("No steps are defined, start creating them by running <bold>gherkin:snippets</bold>"); } return 0; } } codeception/src/Codeception/Command/GenerateHelper.php000077700000002674151323602320017061 0ustar00<?php namespace Codeception\Command; use Codeception\Configuration; use Codeception\Lib\Generator\Helper; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Creates empty Helper class. * * * `codecept g:helper MyHelper` * * `codecept g:helper "My\Helper"` * */ class GenerateHelper extends Command { use Shared\FileSystem; use Shared\Config; protected function configure() { $this->setDefinition([ new InputArgument('name', InputArgument::REQUIRED, 'helper name'), ]); } public function getDescription() { return 'Generates new helper'; } public function execute(InputInterface $input, OutputInterface $output) { $name = ucfirst($input->getArgument('name')); $config = $this->getGlobalConfig(); $path = $this->createDirectoryFor(Configuration::supportDir() . 'Helper', $name); $filename = $path . $this->getShortClassName($name) . '.php'; $res = $this->createFile($filename, (new Helper($name, $config['namespace']))->produce()); if ($res) { $output->writeln("<info>Helper $filename created</info>"); return 0; } else { $output->writeln("<error>Error creating helper $filename</error>"); return 1; } } } codeception/src/Codeception/Command/Console.php000077700000010317151323602320015562 0ustar00<?php namespace Codeception\Command; use Codeception\Codecept; use Codeception\Configuration; use Codeception\Event\DispatcherWrapper; use Codeception\Event\SuiteEvent; use Codeception\Event\TestEvent; use Codeception\Events; use Codeception\Exception\ConfigurationException; use Codeception\Lib\Console\Output; use Codeception\Scenario; use Codeception\SuiteManager; use Codeception\Test\Cept; use Codeception\Util\Debug; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\Question; /** * Try to execute test commands in run-time. You may try commands before writing the test. * * * `codecept console acceptance` - starts acceptance suite environment. If you use WebDriver you can manipulate browser with Codeception commands. */ class Console extends Command { use DispatcherWrapper; protected $test; protected $codecept; protected $suite; protected $output; protected $actions = []; protected function configure() { $this->setDefinition([ new InputArgument('suite', InputArgument::REQUIRED, 'suite to be executed'), new InputOption('colors', '', InputOption::VALUE_NONE, 'Use colors in output'), ]); parent::configure(); } public function getDescription() { return 'Launches interactive test console'; } public function execute(InputInterface $input, OutputInterface $output) { $suiteName = $input->getArgument('suite'); $this->output = $output; $config = Configuration::config(); $settings = Configuration::suiteSettings($suiteName, $config); $options = $input->getOptions(); $options['debug'] = true; $options['silent'] = true; $options['interactive'] = false; $options['colors'] = true; Debug::setOutput(new Output($options)); $this->codecept = new Codecept($options); $dispatcher = $this->codecept->getDispatcher(); $suiteManager = new SuiteManager($dispatcher, $suiteName, $settings); $suiteManager->initialize(); $this->suite = $suiteManager->getSuite(); $moduleContainer = $suiteManager->getModuleContainer(); $this->actions = array_keys($moduleContainer->getActions()); $this->test = new Cept(null, null); $this->test->getMetadata()->setServices([ 'dispatcher' => $dispatcher, 'modules' => $moduleContainer ]); $scenario = new Scenario($this->test); if (!$settings['actor']) { throw new ConfigurationException("Interactive shell can't be started without an actor"); } if (isset($config["namespace"])) { $settings['actor'] = $config["namespace"] .'\\' . $settings['actor']; } $actor = $settings['actor']; $I = new $actor($scenario); $this->listenToSignals(); $output->writeln("<info>Interactive console started for suite $suiteName</info>"); $output->writeln("<info>Try Codeception commands without writing a test</info>"); $suiteEvent = new SuiteEvent($this->suite, $this->codecept->getResult(), $settings); $this->dispatch($dispatcher, Events::SUITE_BEFORE, $suiteEvent); $this->dispatch($dispatcher, Events::TEST_PARSED, new TestEvent($this->test)); $this->dispatch($dispatcher, Events::TEST_BEFORE, new TestEvent($this->test)); if (file_exists($settings['bootstrap'])) { require $settings['bootstrap']; } $I->pause(); $this->dispatch($dispatcher, Events::TEST_AFTER, new TestEvent($this->test)); $this->dispatch($dispatcher, Events::SUITE_AFTER, new SuiteEvent($this->suite)); $output->writeln("<info>Bye-bye!</info>"); return 0; } protected function listenToSignals() { if (function_exists('pcntl_signal')) { declare (ticks = 1); pcntl_signal(SIGINT, SIG_IGN); pcntl_signal(SIGTERM, SIG_IGN); } } } codeception/src/Codeception/Command/SelfUpdate.php000077700000006043151323602320016215 0ustar00<?php namespace Codeception\Command; use Humbug\SelfUpdate\Updater; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; use Codeception\Codecept; /** * Auto-updates phar archive from official site: 'http://codeception.com/codecept.phar' . * * * `php codecept.phar self-update` * * @author Franck Cassedanne <franck@cassedanne.com> */ class SelfUpdate extends Command { /** * Class constants */ const NAME = 'Codeception'; const GITHUB_REPO = 'Codeception/Codeception'; const PHAR_URL = 'http://codeception.com/'; const PHAR_URL_PHP56 = 'http://codeception.com/php56/'; /** * Holds the current script filename. * @var string */ protected $filename; /** * {@inheritdoc} */ protected function configure() { if (isset($_SERVER['argv'][0])) { $this->filename = $_SERVER['argv'][0]; } else { $this->filename = \Phar::running(false); } $this ->setAliases(array('selfupdate')) ->setDescription( sprintf( 'Upgrade <comment>%s</comment> to the latest version', $this->filename ) ); parent::configure(); } /** * @return string */ protected function getCurrentVersion() { return Codecept::VERSION; } /** * {@inheritdoc} */ public function execute(InputInterface $input, OutputInterface $output) { $version = $this->getCurrentVersion(); $output->writeln( sprintf( '<info>%s</info> version <comment>%s</comment>', self::NAME, $version ) ); $url = $this->getPharUrl(); $updater = new Updater(null, false); $updater->getStrategy()->setPharUrl($url . 'codecept.phar'); $updater->getStrategy()->setVersionUrl($url . 'codecept.version'); try { if ($updater->hasUpdate()) { $output->writeln("\n<info>Updating...</info>"); $updater->update(); $output->writeln( sprintf("\n<comment>%s</comment> has been updated.\n", $this->filename) ); } else { $output->writeln('You are already using the latest version.'); } } catch (\Exception $e) { $output->writeln( sprintf( "<error>\n%s\n</error>", $e->getMessage() ) ); return 1; } return 0; } /** * Returns base url of phar file for current PHP version * * @return string */ protected function getPharUrl() { if (version_compare(PHP_VERSION, '7.2.0', '<')) { return self::PHAR_URL_PHP56; } return self::PHAR_URL; } } codeception/src/Codeception/Command/ConfigValidate.php000077700000007327151323602320017046 0ustar00<?php namespace Codeception\Command; use Codeception\Configuration; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Validates and prints Codeception config. * Use it do debug Yaml configs * * Check config: * * * `codecept config`: check global config * * `codecept config unit`: check suite config * * Load config: * * * `codecept config:validate -c path/to/another/config`: from another dir * * `codecept config:validate -c another_config.yml`: from another config file * * Check overriding config values (like in `run` command) * * * `codecept config:validate -o "settings: shuffle: true"`: enable shuffle * * `codecept config:validate -o "settings: lint: false"`: disable linting * * `codecept config:validate -o "reporters: report: \Custom\Reporter" --report`: use custom reporter * */ class ConfigValidate extends Command { use Shared\Config; use Shared\Style; protected function configure() { $this->setDefinition( [ new InputArgument('suite', InputArgument::OPTIONAL, 'to show suite configuration'), new InputOption('config', 'c', InputOption::VALUE_OPTIONAL, 'Use custom path for config'), new InputOption('override', 'o', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Override config values'), ] ); parent::configure(); } public function getDescription() { return 'Validates and prints config to screen'; } public function execute(InputInterface $input, OutputInterface $output) { $this->addStyles($output); if ($suite = $input->getArgument('suite')) { $output->write("Validating <bold>$suite</bold> config... "); $config = $this->getSuiteConfig($suite); $output->writeln("Ok"); $output->writeln("------------------------------\n"); $output->writeln("<info>$suite Suite Config</info>:\n"); $output->writeln($this->formatOutput($config)); return 0; } $output->write("Validating global config... "); $config = $this->getGlobalConfig(); $output->writeln($input->getOption('override')); if (count($input->getOption('override'))) { $config = $this->overrideConfig($input->getOption('override')); } $suites = Configuration::suites(); $output->writeln("Ok"); $output->writeln("------------------------------\n"); $output->writeln("<info>Codeception Config</info>:\n"); $output->writeln($this->formatOutput($config)); $output->writeln('<info>Directories</info>:'); $output->writeln("<comment>codecept_root_dir()</comment> " . codecept_root_dir()); $output->writeln("<comment>codecept_output_dir()</comment> " . codecept_output_dir()); $output->writeln("<comment>codecept_data_dir()</comment> " . codecept_data_dir()); $output->writeln(''); $output->writeln("<info>Available suites</info>: " . implode(', ', $suites)); foreach ($suites as $suite) { $output->write("Validating suite <bold>$suite</bold>... "); $this->getSuiteConfig($suite); $output->writeln('Ok'); } $output->writeln("Execute <info>codecept config:validate [<suite>]</info> to see config for a suite"); return 0; } protected function formatOutput($config) { $output = print_r($config, true); return preg_replace('~\[(.*?)\] =>~', "<fg=yellow>$1</fg=yellow> =>", $output); } } codeception/src/Codeception/Command/Shared/Config.php000077700000004326151323602320016576 0ustar00<?php namespace Codeception\Command\Shared; use Codeception\Configuration; use Symfony\Component\Console\Exception\InvalidOptionException; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Yaml; trait Config { protected function getSuiteConfig($suite) { return Configuration::suiteSettings($suite, $this->getGlobalConfig()); } protected function getGlobalConfig($conf = null) { return Configuration::config($conf); } protected function getSuites($conf = null) { return Configuration::suites(); } protected function overrideConfig($configOptions) { $updatedConfig = []; foreach ($configOptions as $option) { $keys = explode(': ', $option); if (count($keys) < 2) { throw new \InvalidArgumentException('--config-option should have config passed as "key:value"'); } $value = array_pop($keys); $yaml = ''; for ($ind = 0; count($keys); $ind += 2) { $yaml .= "\n" . str_repeat(' ', $ind) . array_shift($keys) . ': '; } $yaml .= $value; try { $config = Yaml::parse($yaml); } catch (ParseException $e) { throw new \Codeception\Exception\ParseException("Overridden config can't be parsed: \n$yaml\n" . $e->getParsedLine()); } $updatedConfig = array_merge_recursive($updatedConfig, $config); } return Configuration::append($updatedConfig); } protected function enableExtensions($extensions) { $config = ['extensions' => ['enabled' => []]]; foreach ($extensions as $name) { if (!class_exists($name)) { $className = 'Codeception\\Extension\\' . ucfirst($name); if (!class_exists($className)) { throw new InvalidOptionException("Extension $name can't be loaded (tried by $name and $className)"); } $config['extensions']['enabled'][] = $className; continue; } $config['extensions']['enabled'][] = $name; } return Configuration::append($config); } } codeception/src/Codeception/Command/Shared/Style.php000077700000001203151323602320016460 0ustar00<?php namespace Codeception\Command\Shared; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Output\OutputInterface; trait Style { public function addStyles(OutputInterface $output) { $output->getFormatter()->setStyle('notice', new OutputFormatterStyle('white', 'green', ['bold'])); $output->getFormatter()->setStyle('bold', new OutputFormatterStyle(null, null, ['bold'])); $output->getFormatter()->setStyle('warning', new OutputFormatterStyle(null, 'yellow', ['bold'])); $output->getFormatter()->setStyle('debug', new OutputFormatterStyle('cyan')); } } codeception/src/Codeception/Command/Shared/FileSystem.php000077700000003327151323602320017455 0ustar00<?php namespace Codeception\Command\Shared; use Codeception\Util\Shared\Namespaces; trait FileSystem { use Namespaces; protected function createDirectoryFor($basePath, $className = '') { $basePath = rtrim($basePath, DIRECTORY_SEPARATOR); if ($className) { $className = str_replace(['/', '\\'], [DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR], $className); $path = $basePath . DIRECTORY_SEPARATOR . $className; $basePath = pathinfo($path, PATHINFO_DIRNAME) . DIRECTORY_SEPARATOR; } if (!file_exists($basePath)) { // Second argument should be mode. Well, umask() doesn't seem to return any if not set. Config may fix this. mkdir($basePath, 0775, true); // Third parameter commands to create directories recursively } return $basePath; } protected function completeSuffix($filename, $suffix) { if (strpos(strrev($filename), strrev($suffix)) === 0) { $filename .= '.php'; } if (strpos(strrev($filename), strrev($suffix . '.php')) !== 0) { $filename .= $suffix . '.php'; } if (strpos(strrev($filename), strrev('.php')) !== 0) { $filename .= '.php'; } return $filename; } protected function removeSuffix($classname, $suffix) { $classname = preg_replace('~\.php$~', '', $classname); return preg_replace("~$suffix$~", '', $classname); } protected function createFile($filename, $contents, $force = false, $flags = null) { if (file_exists($filename) && !$force) { return false; } file_put_contents($filename, $contents, $flags); return true; } } codeception/src/Codeception/Command/Shared/.htaccess000077700000000177151323602320016456 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Command/.htaccess000077700000000177151323602320015250 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Command/Build.php000077700000006675151323602320015233 0ustar00<?php namespace Codeception\Command; use Codeception\Configuration; use Codeception\Lib\Generator\Actions as ActionsGenerator; use Codeception\Lib\Generator\Actor as ActorGenerator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Generates Actor classes (initially Guy classes) from suite configs. * Starting from Codeception 2.0 actor classes are auto-generated. Use this command to generate them manually. * * * `codecept build` * * `codecept build path/to/project` * */ class Build extends Command { use Shared\Config; use Shared\FileSystem; protected $inheritedMethodTemplate = ' * @method void %s(%s)'; /** * @var OutputInterface */ protected $output; public function getDescription() { return 'Generates base classes for all suites'; } protected function execute(InputInterface $input, OutputInterface $output) { $this->output = $output; $this->buildActorsForConfig(); return 0; } private function buildActor(array $settings) { $actorGenerator = new ActorGenerator($settings); $this->output->writeln( '<info>' . Configuration::config()['namespace'] . '\\' . $actorGenerator->getActorName() . "</info> includes modules: " . implode(', ', $actorGenerator->getModules()) ); $content = $actorGenerator->produce(); $file = $this->createDirectoryFor( Configuration::supportDir(), $settings['actor'] ) . $this->getShortClassName($settings['actor']); $file .= '.php'; return $this->createFile($file, $content); } private function buildActions(array $settings) { $actionsGenerator = new ActionsGenerator($settings); $content = $actionsGenerator->produce(); $this->output->writeln( " -> {$settings['actor']}Actions.php generated successfully. " . $actionsGenerator->getNumMethods() . " methods added" ); $file = $this->createDirectoryFor(Configuration::supportDir() . '_generated', $settings['actor']); $file .= $this->getShortClassName($settings['actor']) . 'Actions.php'; return $this->createFile($file, $content, true); } private function buildSuiteActors() { $suites = $this->getSuites(); if (!empty($suites)) { $this->output->writeln("<info>Building Actor classes for suites: " . implode(', ', $suites) . '</info>'); } foreach ($suites as $suite) { $settings = $this->getSuiteConfig($suite); if (!$settings['actor']) { continue; // no actor } $this->buildActions($settings); $actorBuilt = $this->buildActor($settings); if ($actorBuilt) { $this->output->writeln("{$settings['actor']}.php created."); } } } protected function buildActorsForConfig($configFile = null) { $config = $this->getGlobalConfig($configFile); $dir = Configuration::projectDir(); $this->buildSuiteActors(); foreach ($config['include'] as $subConfig) { $this->output->writeln("\n<comment>Included Configuration: $subConfig</comment>"); $this->buildActorsForConfig($dir . DIRECTORY_SEPARATOR . $subConfig); } } } codeception/src/Codeception/Command/GherkinSnippets.php000077700000005617151323602320017304 0ustar00<?php namespace Codeception\Command; use Codeception\Lib\Generator\GherkinSnippets as SnippetsGenerator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** * Generates code snippets for matched feature files in a suite. * Code snippets are expected to be implemented in Actor or PageObjects * * Usage: * * * `codecept gherkin:snippets acceptance` - snippets from all feature of acceptance tests * * `codecept gherkin:snippets acceptance/feature/users` - snippets from `feature/users` dir of acceptance tests * * `codecept gherkin:snippets acceptance user_account.feature` - snippets from a single feature file * * `codecept gherkin:snippets acceptance/feature/users/user_accout.feature` - snippets from feature file in a dir */ class GherkinSnippets extends Command { use Shared\Config; use Shared\Style; protected function configure() { $this->setDefinition( [ new InputArgument('suite', InputArgument::REQUIRED, 'suite to scan for feature files'), new InputArgument('test', InputArgument::OPTIONAL, 'test to be scanned'), new InputOption('config', 'c', InputOption::VALUE_OPTIONAL, 'Use custom path for config'), ] ); parent::configure(); } public function getDescription() { return 'Fetches empty steps from feature files of suite and prints code snippets for them'; } public function execute(InputInterface $input, OutputInterface $output) { $this->addStyles($output); $suite = $input->getArgument('suite'); $test = $input->getArgument('test'); $config = $this->getSuiteConfig($suite); $generator = new SnippetsGenerator($config, $test); $snippets = $generator->getSnippets(); $features = $generator->getFeatures(); if (empty($snippets)) { $output->writeln("<notice> All Gherkin steps are defined. Exiting... </notice>"); return 0; } $output->writeln("<comment> Snippets found in: </comment>"); foreach ($features as $feature) { $output->writeln("<info> - {$feature} </info>"); } $output->writeln("<comment> Generated Snippets: </comment>"); $output->writeln("<info> ----------------------------------------- </info>"); foreach ($snippets as $snippet) { $output->writeln($snippet); } $output->writeln("<info> ----------------------------------------- </info>"); $output->writeln(sprintf(' <bold>%d</bold> snippets proposed', count($snippets))); $output->writeln("<notice> Copy generated snippets to {$config['actor']} or a specific Gherkin context </notice>"); return 0; } } codeception/src/Codeception/Command/CompletionFallback.php000077700000002707151323602320017715 0ustar00<?php namespace Codeception\Command; use Codeception\Configuration; use Stecman\Component\Symfony\Console\BashCompletion\Completion as ConsoleCompletion; use Stecman\Component\Symfony\Console\BashCompletion\CompletionCommand; use Stecman\Component\Symfony\Console\BashCompletion\CompletionHandler; use Stecman\Component\Symfony\Console\BashCompletion\Completion\ShellPathCompletion; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class CompletionFallback extends Command { protected function configure() { $this ->setName('_completion') ->setDescription('BASH completion hook.') ->setHelp(<<<END To enable BASH completion, install optional stecman/symfony-console-completion first: <comment>composer require stecman/symfony-console-completion</comment> END ); // Hide this command from listing if supported // Command::setHidden() was not available before Symfony 3.2.0 if (method_exists($this, 'setHidden')) { $this->setHidden(true); } } protected function execute(InputInterface $input, OutputInterface $output) { $output->writeln("Install optional <comment>stecman/symfony-console-completion</comment>"); return 0; } } codeception/src/Codeception/Command/GeneratePageObject.php000077700000004346151323602320017643 0ustar00<?php namespace Codeception\Command; use Codeception\Configuration; use Codeception\Lib\Generator\PageObject as PageObjectGenerator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Generates PageObject. Can be generated either globally, or just for one suite. * If PageObject is generated globally it will act as UIMap, without any logic in it. * * * `codecept g:page Login` * * `codecept g:page Registration` * * `codecept g:page acceptance Login` */ class GeneratePageObject extends Command { use Shared\FileSystem; use Shared\Config; protected function configure() { $this->setDefinition([ new InputArgument('suite', InputArgument::REQUIRED, 'Either suite name or page object name)'), new InputArgument('page', InputArgument::OPTIONAL, 'Page name of pageobject to represent'), ]); parent::configure(); } public function getDescription() { return 'Generates empty PageObject class'; } public function execute(InputInterface $input, OutputInterface $output) { $suite = $input->getArgument('suite'); $class = $input->getArgument('page'); if (!$class) { $class = $suite; $suite = null; } $conf = $suite ? $this->getSuiteConfig($suite) : $this->getGlobalConfig(); if ($suite) { $suite = DIRECTORY_SEPARATOR . ucfirst($suite); } $path = $this->createDirectoryFor(Configuration::supportDir() . 'Page' . $suite, $class); $filename = $path . $this->getShortClassName($class) . '.php'; $output->writeln($filename); $gen = new PageObjectGenerator($conf, ucfirst($suite) . '\\' . $class); $res = $this->createFile($filename, $gen->produce()); if (!$res) { $output->writeln("<error>PageObject $filename already exists</error>"); return 1; } $output->writeln("<info>PageObject was created in $filename</info>"); return 0; } protected function pathToPageObject($class, $suite) { } } codeception/src/Codeception/Configuration.php000077700000057662151323602320015427 0ustar00<?php namespace Codeception; use Codeception\Exception\ConfigurationException; use Codeception\Lib\Notification; use Codeception\Lib\ParamsLoader; use Codeception\Util\Autoload; use Codeception\Util\Template; use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\SplFileInfo; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Yaml; class Configuration { protected static $suites = []; /** * @var array Current configuration */ protected static $config = null; /** * @var array environmental files configuration cache */ protected static $envConfig = []; /** * @var string Directory containing main configuration file. * @see self::projectDir() */ protected static $dir = null; /** * @var string Current project output directory. */ protected static $outputDir = null; /** * @var string Current project data directory. This directory is used to hold * sql dumps and other things needed for current project tests. */ protected static $dataDir = null; /** * @var string Directory with test support files like Actors, Helpers, PageObjects, etc */ protected static $supportDir = null; /** * @var string Directory containing environment configuration files. */ protected static $envsDir = null; /** * @var string Directory containing tests and suites of the current project. */ protected static $testsDir = null; public static $lock = false; protected static $di; /** * @var array Default config */ public static $defaultConfig = [ 'actor_suffix'=> 'Tester', 'namespace' => '', 'include' => [], 'paths' => [], 'extends' => null, 'suites' => [], 'modules' => [], 'extensions' => [ 'enabled' => [], 'config' => [], 'commands' => [], ], 'reporters' => [ 'xml' => 'Codeception\PHPUnit\Log\JUnit', 'html' => 'Codeception\PHPUnit\ResultPrinter\HTML', 'report' => 'Codeception\PHPUnit\ResultPrinter\Report', 'tap' => 'PHPUnit\Util\Log\TAP', 'json' => 'PHPUnit\Util\Log\JSON', 'phpunit-xml' => 'Codeception\PHPUnit\Log\PhpUnit', ], 'groups' => [], 'bootstrap' => false, 'settings' => [ 'colors' => true, 'bootstrap' => false, 'strict_xml' => false, 'lint' => true, 'backup_globals' => true, 'log_incomplete_skipped' => false, 'report_useless_tests' => false, 'disallow_test_output' => false, 'be_strict_about_changes_to_global_state' => false, 'shuffle' => false, ], 'coverage' => [], 'params' => [], 'gherkin' => [] ]; public static $defaultSuiteSettings = [ 'actor' => null, 'class_name' => null, // Codeception <2.3 compatibility 'modules' => [ 'enabled' => [], 'config' => [], 'depends' => [] ], 'step_decorators' => 'Codeception\Step\ConditionalAssertion', 'path' => null, 'extends' => null, 'namespace' => null, 'groups' => [], 'formats' => [], 'shuffle' => false, 'extensions' => [ // suite extensions 'enabled' => [], 'config' => [], ], 'error_level' => 'E_ALL & ~E_STRICT & ~E_DEPRECATED', ]; protected static $params; /** * Loads global config file which is `codeception.yml` by default. * When config is already loaded - returns it. * * @param null $configFile * @return array * @throws Exception\ConfigurationException */ public static function config($configFile = null) { if (!$configFile && self::$config) { return self::$config; } if (self::$config && self::$lock) { return self::$config; } if ($configFile === null) { $configFile = getcwd() . DIRECTORY_SEPARATOR . 'codeception.yml'; } if (is_dir($configFile)) { $configFile = $configFile . DIRECTORY_SEPARATOR . 'codeception.yml'; } $dir = realpath(dirname($configFile)); self::$dir = $dir; $configDistFile = $dir . DIRECTORY_SEPARATOR . 'codeception.dist.yml'; if (!(file_exists($configDistFile) || file_exists($configFile))) { throw new ConfigurationException("Configuration file could not be found.\nRun `bootstrap` to initialize Codeception.", 404); } // Preload config to retrieve params such that they are applied to codeception config file below $tempConfig = self::$defaultConfig; $distConfigContents = ""; if (file_exists($configDistFile)) { $distConfigContents = file_get_contents($configDistFile); $tempConfig = self::mergeConfigs($tempConfig, self::getConfFromContents($distConfigContents, $configDistFile)); } $configContents = ""; if (file_exists($configFile)) { $configContents = file_get_contents($configFile); $tempConfig = self::mergeConfigs($tempConfig, self::getConfFromContents($configContents, $configFile)); } self::prepareParams($tempConfig); // load config using params $config = self::mergeConfigs(self::$defaultConfig, self::getConfFromContents($distConfigContents, $configDistFile)); $config = self::mergeConfigs($config, self::getConfFromContents($configContents, $configFile)); if ($config == self::$defaultConfig) { throw new ConfigurationException("Configuration file is invalid"); } // we check for the "extends" key in the yml file if (isset($config['extends'])) { // and now we search for the file $presetFilePath = codecept_absolute_path($config['extends']); if (file_exists($presetFilePath)) { // and merge it with our configuration file $config = self::mergeConfigs(self::getConfFromFile($presetFilePath), $config); } } self::$config = $config; // compatibility with suites created by Codeception < 2.3.0 if (!isset($config['paths']['output']) and isset($config['paths']['log'])) { $config['paths']['output'] = $config['paths']['log']; } if (isset(self::$config['actor'])) { self::$config['actor_suffix'] = self::$config['actor']; // old compatibility } if (!isset($config['paths']['support']) and isset($config['paths']['helpers'])) { $config['paths']['support'] = $config['paths']['helpers']; } if (!isset($config['paths']['output'])) { throw new ConfigurationException('Output path is not defined by key "paths: output"'); } self::$outputDir = $config['paths']['output']; // fill up includes with wildcard expansions $config['include'] = self::expandWildcardedIncludes($config['include']); // config without tests, for inclusion of other configs if (count($config['include'])) { self::$config = $config; if (!isset($config['paths']['tests'])) { return $config; } } if (!isset($config['paths']['tests'])) { throw new ConfigurationException( 'Tests directory is not defined in Codeception config by key "paths: tests:"' ); } if (!isset($config['paths']['data'])) { throw new ConfigurationException('Data path is not defined Codeception config by key "paths: data"'); } if (!isset($config['paths']['support'])) { throw new ConfigurationException('Helpers path is not defined by key "paths: support"'); } self::$dataDir = $config['paths']['data']; self::$supportDir = $config['paths']['support']; self::$testsDir = $config['paths']['tests']; if (isset($config['paths']['envs'])) { self::$envsDir = $config['paths']['envs']; } Autoload::addNamespace(self::$config['namespace'], self::supportDir()); if ($config['settings']['bootstrap']) { $bootstrap = self::$config['settings']['bootstrap']; Notification::deprecate("'settings: bootstrap: $bootstrap' option is deprecated! Replace it with: 'bootstrap: $bootstrap' (not under settings section). See https://bit.ly/2YrRzVc "); try { self::loadBootstrap($bootstrap, self::testsDir()); } catch (ConfigurationException $exception) { Notification::deprecate("Bootstrap file ($bootstrap) is defined in configuration but can't be loaded. Disable 'settings: bootstrap:' configuration to remove this message"); } } self::loadBootstrap($config['bootstrap'], self::testsDir()); self::loadSuites(); return $config; } public static function loadBootstrap($bootstrap, $path) { if (!$bootstrap) { return; } $bootstrap = \Codeception\Util\PathResolver::isPathAbsolute($bootstrap) ? $bootstrap : rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $bootstrap; if (!file_exists($bootstrap)) { throw new ConfigurationException("Bootstrap file $bootstrap can't be loaded"); } require_once $bootstrap; } protected static function loadSuites() { $suites = Finder::create() ->files() ->name('*.{suite,suite.dist}.yml') ->in(self::$dir . DIRECTORY_SEPARATOR . self::$testsDir) ->depth('< 1') ->sortByName(); self::$suites = []; foreach (array_keys(self::$config['suites']) as $suite) { self::$suites[$suite] = $suite; } /** @var SplFileInfo $suite */ foreach ($suites as $suite) { preg_match('~(.*?)(\.suite|\.suite\.dist)\.yml~', $suite->getFilename(), $matches); self::$suites[$matches[1]] = $matches[1]; } } /** * Returns suite configuration. Requires suite name and global config used (Configuration::config) * * @param string $suite * @param array $config * @return array * @throws \Exception */ public static function suiteSettings($suite, $config) { // cut namespace name from suite name if ($suite != $config['namespace'] && substr($suite, 0, strlen($config['namespace'])) == $config['namespace']) { $suite = substr($suite, strlen($config['namespace'])); } if (!in_array($suite, self::$suites)) { throw new ConfigurationException("Suite $suite was not loaded"); } // load global config $globalConf = $config['settings']; foreach (['modules', 'coverage', 'namespace', 'groups', 'env', 'gherkin', 'extensions'] as $key) { if (isset($config[$key])) { $globalConf[$key] = $config[$key]; } } $settings = self::mergeConfigs(self::$defaultSuiteSettings, $globalConf); // load suite config $settings = self::loadSuiteConfig($suite, $config['paths']['tests'], $settings); // load from environment configs if (isset($config['paths']['envs'])) { $envConf = self::loadEnvConfigs(self::$dir . DIRECTORY_SEPARATOR . $config['paths']['envs']); $settings = self::mergeConfigs($settings, $envConf); } if (!$settings['actor']) { // Codeception 2.2 compatibility $settings['actor'] = $settings['class_name']; } if (!$settings['path']) { // take a suite path from its name $settings['path'] = $suite; } $config['paths']['tests'] = str_replace('/', DIRECTORY_SEPARATOR, $config['paths']['tests']); $settings['path'] = self::$dir . DIRECTORY_SEPARATOR . $config['paths']['tests'] . DIRECTORY_SEPARATOR . $settings['path'] . DIRECTORY_SEPARATOR; return $settings; } /** * Loads environments configuration from set directory * * @param string $path path to the directory * @return array */ protected static function loadEnvConfigs($path) { if (isset(self::$envConfig[$path])) { return self::$envConfig[$path]; } if (!is_dir($path)) { self::$envConfig[$path] = []; return self::$envConfig[$path]; } $envFiles = Finder::create() ->files() ->name('*.yml') ->in($path) ->depth('< 2'); $envConfig = []; /** @var SplFileInfo $envFile */ foreach ($envFiles as $envFile) { $env = str_replace(['.dist.yml', '.yml'], '', $envFile->getFilename()); $envConfig[$env] = []; $envPath = $path; if ($envFile->getRelativePath()) { $envPath .= DIRECTORY_SEPARATOR . $envFile->getRelativePath(); } foreach (['.dist.yml', '.yml'] as $suffix) { $envConf = self::getConfFromFile($envPath . DIRECTORY_SEPARATOR . $env . $suffix, null); if ($envConf === null) { continue; } $envConfig[$env] = self::mergeConfigs($envConfig[$env], $envConf); } } self::$envConfig[$path] = ['env' => $envConfig]; return self::$envConfig[$path]; } /** * Loads configuration from Yaml data * * @param string $contents Yaml config file contents * @param string $filename which is supposed to be loaded * @return array * @throws ConfigurationException */ protected static function getConfFromContents($contents, $filename = '(.yml)') { if (self::$params) { $template = new Template($contents, '%', '%'); $template->setVars(self::$params); $contents = $template->produce(); } try { return Yaml::parse($contents); } catch (ParseException $exception) { throw new ConfigurationException( sprintf( "Error loading Yaml config from `%s`\n \n%s\nRead more about Yaml format https://goo.gl/9UPuEC", $filename, $exception->getMessage() ) ); } } /** * Loads configuration from Yaml file or returns given value if the file doesn't exist * * @param string $filename filename * @param mixed $nonExistentValue value used if filename is not found * @return array * @throws ConfigurationException */ protected static function getConfFromFile($filename, $nonExistentValue = []) { if (file_exists($filename)) { $yaml = file_get_contents($filename); return self::getConfFromContents($yaml, $filename); } return $nonExistentValue; } public static function suites() { return self::$suites; } /** * Return list of enabled modules according suite config. * * @param array $settings suite settings * @return array */ public static function modules($settings) { return array_filter( array_map( function ($m) { return is_array($m) ? key($m) : $m; }, $settings['modules']['enabled'], array_keys($settings['modules']['enabled']) ), function ($m) use ($settings) { if (!isset($settings['modules']['disabled'])) { return true; } return !in_array($m, $settings['modules']['disabled']); } ); } public static function isExtensionEnabled($extensionName) { return isset(self::$config['extensions']['enabled']) && in_array($extensionName, self::$config['extensions']['enabled']); } /** * Returns current path to `_data` dir. * Use it to store database fixtures, sql dumps, or other files required by your tests. * * @return string */ public static function dataDir() { return self::$dir . DIRECTORY_SEPARATOR . self::$dataDir . DIRECTORY_SEPARATOR; } /** * Return current path to `_helpers` dir. * Helpers are custom modules. * * @return string */ public static function supportDir() { return self::$dir . DIRECTORY_SEPARATOR . self::$supportDir . DIRECTORY_SEPARATOR; } /** * Returns actual path to current `_output` dir. * Use it in Helpers or Groups to save result or temporary files. * * @return string * @throws Exception\ConfigurationException */ public static function outputDir() { if (!self::$outputDir) { throw new ConfigurationException("Path for output not specified. Please, set output path in global config"); } $dir = self::$outputDir . DIRECTORY_SEPARATOR; if (!codecept_is_path_absolute($dir)) { $dir = self::$dir . DIRECTORY_SEPARATOR . $dir; } if (!file_exists($dir)) { @mkdir($dir, 0777, true); } if (!is_writable($dir)) { @chmod($dir, 0777); } if (!is_writable($dir)) { throw new ConfigurationException( "Path for output is not writable. Please, set appropriate access mode for output path: {$dir}" ); } return $dir; } /** * Compatibility alias to `Configuration::logDir()` * @return string * @throws ConfigurationException */ public static function logDir() { return self::outputDir(); } /** * Returns path to the root of your project. * Basically returns path to current `codeception.yml` loaded. * Use this method instead of `__DIR__`, `getcwd()` or anything else. * @return string */ public static function projectDir() { return self::$dir . DIRECTORY_SEPARATOR; } /** * Returns path to tests directory * * @return string */ public static function testsDir() { return self::$dir . DIRECTORY_SEPARATOR . self::$testsDir . DIRECTORY_SEPARATOR; } /** * Return current path to `_envs` dir. * Use it to store environment specific configuration. * * @return string */ public static function envsDir() { if (!self::$envsDir) { return null; } return self::$dir . DIRECTORY_SEPARATOR . self::$envsDir . DIRECTORY_SEPARATOR; } /** * Is this a meta-configuration file that just points to other `codeception.yml`? * If so, it may have no tests by itself. * * @return bool */ public static function isEmpty() { return !(bool)self::$testsDir; } /** * Adds parameters to config * * @param array $config * @return array */ public static function append(array $config = []) { self::$config = self::mergeConfigs(self::$config, $config); if (isset(self::$config['paths']['output'])) { self::$outputDir = self::$config['paths']['output']; } if (isset(self::$config['paths']['data'])) { self::$dataDir = self::$config['paths']['data']; } if (isset(self::$config['paths']['support'])) { self::$supportDir = self::$config['paths']['support']; } if (isset(self::$config['paths']['tests'])) { self::$testsDir = self::$config['paths']['tests']; } return self::$config; } public static function mergeConfigs($a1, $a2) { if (!is_array($a1)) { return $a2; } if (!is_array($a2)) { return $a1; } $res = []; // for sequential arrays if (isset($a1[0], $a2[0])) { return array_values(array_unique(array_merge_recursive($a2, $a1), SORT_REGULAR)); } // for associative arrays foreach ($a2 as $k2 => $v2) { if (!isset($a1[$k2])) { // if no such key $res[$k2] = $v2; unset($a1[$k2]); continue; } $res[$k2] = self::mergeConfigs($a1[$k2], $v2); unset($a1[$k2]); } foreach ($a1 as $k1 => $v1) { // only single elements here left $res[$k1] = $v1; } return $res; } /** * Loads config from *.dist.suite.yml and *.suite.yml * * @param $suite * @param $path * @param $settings * @return array * @throws ConfigurationException */ protected static function loadSuiteConfig($suite, $path, $settings) { if (isset(self::$config['suites'][$suite])) { // bundled config return self::mergeConfigs($settings, self::$config['suites'][$suite]); } $suiteDir = self::$dir . DIRECTORY_SEPARATOR . $path; $suiteDistConf = self::getConfFromFile($suiteDir . DIRECTORY_SEPARATOR . "$suite.suite.dist.yml"); $suiteConf = self::getConfFromFile($suiteDir . DIRECTORY_SEPARATOR . "$suite.suite.yml"); // now we check the suite config file, if a extends key is defined if (isset($suiteConf['extends'])) { $presetFilePath = codecept_is_path_absolute($suiteConf['extends']) ? $suiteConf['extends'] // If path is absolute – use it : realpath($suiteDir . DIRECTORY_SEPARATOR . $suiteConf['extends']); // Otherwise try to locate it in the suite dir if (file_exists($presetFilePath)) { $settings = self::mergeConfigs(self::getConfFromFile($presetFilePath), $settings); } } $settings = self::mergeConfigs($settings, $suiteDistConf); $settings = self::mergeConfigs($settings, $suiteConf); return $settings; } /** * Replaces wildcarded items in include array with real paths. * * @param $includes * @return array * @throws ConfigurationException */ protected static function expandWildcardedIncludes(array $includes) { if (empty($includes)) { return $includes; } $expandedIncludes = []; foreach ($includes as $include) { $expandedIncludes = array_merge($expandedIncludes, self::expandWildcardsFor($include)); } return $expandedIncludes; } /** * Finds config files in given wildcarded include path. * Returns the expanded paths or the original if not a wildcard. * * @param $include * @return array * @throws ConfigurationException */ protected static function expandWildcardsFor($include) { if (1 !== preg_match('/[\?\.\*]/', $include)) { return [$include,]; } try { $configFiles = Finder::create()->files() ->name('/codeception(\.dist\.yml|\.yml)/') ->in(self::$dir . DIRECTORY_SEPARATOR . $include); } catch (\InvalidArgumentException $e) { throw new ConfigurationException( "Configuration file(s) could not be found in \"$include\"." ); } $paths = []; foreach ($configFiles as $file) { $paths[] = codecept_relative_path($file->getPath()); } return $paths; } private static function prepareParams($settings) { self::$params = []; $paramsLoader = new ParamsLoader(); foreach ($settings['params'] as $paramStorage) { static::$params = array_merge(self::$params, $paramsLoader->load($paramStorage)); } } } codeception/src/Codeception/Suite.php000077700000003706151323602320013677 0ustar00<?php namespace Codeception; use Codeception\Test\Descriptor; use Codeception\Test\Interfaces\Dependent; class Suite extends \PHPUnit\Framework\TestSuite { protected $modules; protected $baseName; public function reorderDependencies() { $tests = []; foreach ($this->tests as $test) { $tests = array_merge($tests, $this->getDependencies($test)); } $queue = []; $hashes = []; foreach ($tests as $test) { if (in_array(spl_object_hash($test), $hashes)) { continue; } $hashes[] = spl_object_hash($test); $queue[] = $test; } $this->tests = $queue; } protected function getDependencies($test) { if (!$test instanceof Dependent) { return [$test]; } $tests = []; foreach ($test->fetchDependencies() as $requiredTestName) { $required = $this->findMatchedTest($requiredTestName); if (!$required) { continue; } $tests = array_merge($tests, $this->getDependencies($required)); } $tests[] = $test; return $tests; } protected function findMatchedTest($testSignature) { foreach ($this->tests as $test) { $signature = Descriptor::getTestSignature($test); if ($signature === $testSignature) { return $test; } } } /** * @return mixed */ public function getModules() { return $this->modules; } /** * @param mixed $modules */ public function setModules($modules) { $this->modules = $modules; } /** * @return mixed */ public function getBaseName() { return $this->baseName; } /** * @param mixed $baseName */ public function setBaseName($baseName) { $this->baseName = $baseName; } } codeception/src/Codeception/TestInterface.php000077700000000306151323602320015337 0ustar00<?php namespace Codeception; use Codeception\Test\Metadata; interface TestInterface extends \PHPUnit\Framework\Test { /** * @return Metadata */ public function getMetadata(); } codeception/src/Codeception/Example.php000077700000005016151323602320014175 0ustar00<?php namespace Codeception; use Traversable; class Example implements \ArrayAccess, \Countable, \IteratorAggregate { protected $data; public function __construct($data) { $this->data = $data; } /** * Whether a offset exists * @link http://php.net/manual/en/arrayaccess.offsetexists.php * @param mixed $offset <p> * An offset to check for. * </p> * @return boolean true on success or false on failure. * </p> * <p> * The return value will be casted to boolean if non-boolean was returned. * @since 5.0.0 */ public function offsetExists($offset) { return array_key_exists($offset, $this->data); } /** * Offset to retrieve * @link http://php.net/manual/en/arrayaccess.offsetget.php * @param mixed $offset <p> * The offset to retrieve. * </p> * @return mixed Can return all value types. * @since 5.0.0 */ public function offsetGet($offset) { if (!$this->offsetExists($offset)) { throw new \PHPUnit\Framework\AssertionFailedError("Example $offset doesn't exist"); }; return $this->data[$offset]; } /** * Offset to set * @link http://php.net/manual/en/arrayaccess.offsetset.php * @param mixed $offset <p> * The offset to assign the value to. * </p> * @param mixed $value <p> * The value to set. * </p> * @return void * @since 5.0.0 */ public function offsetSet($offset, $value) { $this->data[$offset] = $value; } /** * Offset to unset * @link http://php.net/manual/en/arrayaccess.offsetunset.php * @param mixed $offset <p> * The offset to unset. * </p> * @return void * @since 5.0.0 */ public function offsetUnset($offset) { unset($this->data[$offset]); } /** * Count elements of an object * @link http://php.net/manual/en/countable.count.php * @return int The custom count as an integer. * </p> * <p> * The return value is cast to an integer. * @since 5.1.0 */ public function count() { return count($this->data); } /** * Retrieve an external iterator * @link http://php.net/manual/en/iteratoraggregate.getiterator.php * @return Traversable An instance of an object implementing <b>Iterator</b> or * <b>Traversable</b> * @since 5.0.0 */ public function getIterator() { return new \ArrayIterator($this->data); } } codeception/src/Codeception/Template/Acceptance.php000077700000010527151323602320016406 0ustar00<?php namespace Codeception\Template; use Codeception\InitTemplate; use Codeception\Util\Template; use Symfony\Component\Yaml\Yaml; class Acceptance extends InitTemplate { protected $configTemplate = <<<EOF # suite config suites: acceptance: actor: AcceptanceTester path: . modules: enabled: - WebDriver: url: {{url}} browser: {{browser}} - \Helper\Acceptance # add Codeception\Step\Retry trait to AcceptanceTester to enable retries step_decorators: - Codeception\Step\ConditionalAssertion - Codeception\Step\TryTo - Codeception\Step\Retry extensions: enabled: [Codeception\Extension\RunFailed] params: - env gherkin: [] # additional paths paths: tests: {{baseDir}} output: {{baseDir}}/_output data: {{baseDir}}/_data support: {{baseDir}}/_support envs: {{baseDir}}/_envs settings: shuffle: false lint: true EOF; protected $firstTest = <<<EOF <?php class LoginCest { public function _before(AcceptanceTester \$I) { \$I->amOnPage('/'); } public function loginSuccessfully(AcceptanceTester \$I) { // write a positive login test } public function loginWithInvalidPassword(AcceptanceTester \$I) { // write a negative login test } } EOF; public function setup() { $this->checkInstalled(); $this->say("Let's prepare Codeception for acceptance testing"); $this->say("Create your tests and run them in real browser"); $this->say(""); $dir = $this->ask("Where tests will be stored?", 'tests'); $browser = $this->ask("Select a browser for testing", ['chrome', 'firefox']); if ($browser === 'chrome') { $this->sayInfo("Ensure that you have Selenium Server and ChromeDriver installed before running tests"); } if ($browser === 'firefox') { $this->sayInfo("Ensure that you have Selenium Server and GeckoDriver installed before running tests"); } $url = $this->ask("Start url for tests", "http://localhost"); $this->createEmptyDirectory($outputDir = $dir . DIRECTORY_SEPARATOR . '_output'); $this->createEmptyDirectory($dir . DIRECTORY_SEPARATOR . '_data'); $this->createDirectoryFor($supportDir = $dir . DIRECTORY_SEPARATOR . '_support'); $this->createDirectoryFor($supportDir . DIRECTORY_SEPARATOR . '_generated'); $this->gitIgnore($outputDir); $this->gitIgnore($supportDir . DIRECTORY_SEPARATOR . '_generated'); $this->sayInfo("Created test directories inside at $dir"); if (!class_exists('\\Codeception\\Module\\WebDriver')) { // composer version $this->addModulesToComposer(['WebDriver']); } $configFile = (new Template($this->configTemplate)) ->place('url', $url) ->place('browser', $browser) ->place('baseDir', $dir) ->produce(); if ($this->namespace) { $namespace = rtrim($this->namespace, '\\'); $configFile = "namespace: $namespace\n" . $configFile; } $this->createFile('codeception.yml', $configFile); $this->createHelper('Acceptance', $supportDir); $this->createActor('AcceptanceTester', $supportDir, Yaml::parse($configFile)['suites']['acceptance']); $this->sayInfo("Created global config codeception.yml inside the root directory"); $this->createFile($dir . DIRECTORY_SEPARATOR . 'LoginCest.php', $this->firstTest); $this->sayInfo("Created a demo test LoginCest.php"); $this->say(); $this->saySuccess("INSTALLATION COMPLETE"); $this->say(); $this->say("<bold>Next steps:</bold>"); $this->say('1. Launch Selenium Server and webserver'); $this->say("2. Edit <bold>$dir/LoginCest.php</bold> to test login of your application"); $this->say("3. Run tests using: <comment>codecept run</comment>"); $this->say(); $this->say("HINT: Add '\\Codeception\\Step\\Retry' trait to AcceptanceTester class to enable auto-retries"); $this->say("HINT: See https://codeception.com/docs/03-AcceptanceTests#retry"); $this->say("<bold>Happy testing!</bold>"); } } codeception/src/Codeception/Template/Upgrade4.php000077700000005111151323602320016024 0ustar00<?php namespace Codeception\Template; use Codeception\Configuration; use Codeception\InitTemplate; class Upgrade4 extends InitTemplate { const SURVEY_LINK = 'http://bit.ly/codecept-survey'; const DONATE_LINK = 'https://opencollective.com/codeception'; public function setup() { if (!$this->isInstalled()) { $this->sayWarning('Codeception is not installed in this dir.'); return; } $this->sayInfo('Welcome to Codeception v4 Upgrade wizard!'); $this->say(''); $this->say('Codeception is maintained since 2011, is free & open-source.'); $this->say('To make it better we need your feedback on it!'); $this->say(''); $this->say('Please take a minute and fill in a brief survey:'); $this->say('<bold>' . self::SURVEY_LINK . '</bold>'); sleep(5); $this->say(''); $result = $this->ask('<question>Did you fill in the survey?</question>', true); if ($result) { $this->say('Thank you! '); } else { $this->say('Anyway...'); } $config = Configuration::config(); $modules = []; $suites = Configuration::suites(); if (empty($suites)) { $this->sayError("No suites found in current config."); $this->sayWarning('If you use sub-configs with `include` option, run this script on subconfigs:'); $this->sayWarning('Example: php vendor/bin/codecept init upgrade4 -c backend/'); throw new \Exception("No suites found, can't upgrade"); } foreach (Configuration::suites() as $suite) { $suiteConfig = Configuration::suiteSettings($suite, $config); $modules = array_merge($modules, Configuration::modules($suiteConfig)); } $numPackages = $this->addModulesToComposer($modules); if ($numPackages === 0) { $this->sayWarning("No upgrade needed! Everything is fine already"); return; } $this->saySuccess("Done upgrading!"); $this->say(''); $this->say('Please consider donating to Codeception on regular basis:'); $this->say(''); $this->say('<bold>' . self::DONATE_LINK . '</bold>'); $this->say(''); $this->say('It\'s ok to pay for reliable software.'); $this->say('Talk to your manager & support further development of Codeception!'); } private function isInstalled() { try { $this->checkInstalled(); } catch (\Exception $e) { return true; } return false; } } codeception/src/Codeception/Template/Bootstrap.php000077700000013503151323602320016332 0ustar00<?php namespace Codeception\Template; use Codeception\InitTemplate; use Symfony\Component\Yaml\Yaml; class Bootstrap extends InitTemplate { // defaults protected $supportDir = 'tests/_support'; protected $outputDir = 'tests/_output'; protected $dataDir = 'tests/_data'; protected $envsDir = 'tests/_envs'; public function setup() { $this->checkInstalled($this->workDir); $input = $this->input; if ($input->getOption('namespace')) { $this->namespace = trim($input->getOption('namespace'), '\\') . '\\'; } if ($input->hasOption('actor') && $input->getOption('actor')) { $this->actorSuffix = $input->getOption('actor'); } $this->say( "<fg=white;bg=magenta> Bootstrapping Codeception </fg=white;bg=magenta>\n" ); $this->createGlobalConfig(); $this->say("File codeception.yml created <- global configuration"); $this->createDirs(); if ($input->hasOption('empty') && $input->getOption('empty')) { return; } if (!class_exists('\\Codeception\\Module\\Asserts') || !class_exists('\\Codeception\\Module\\PhpBrowser')) { $this->addModulesToComposer(['PhpBrowser', 'Asserts']); } $this->createUnitSuite(); $this->createFunctionalSuite(); $this->createAcceptanceSuite(); $this->say(" --- "); $this->say(); $this->saySuccess('Codeception is installed for acceptance, functional, and unit testing'); $this->say(); $this->say("<bold>Next steps:</bold>"); $this->say('1. Edit <bold>tests/acceptance.suite.yml</bold> to set url of your application. Change PhpBrowser to WebDriver to enable browser testing'); $this->say("2. Edit <bold>tests/functional.suite.yml</bold> to enable a framework module. Remove this file if you don't use a framework"); $this->say("3. Create your first acceptance tests using <comment>codecept g:cest acceptance First</comment>"); $this->say("4. Write first test in <bold>tests/acceptance/FirstCest.php</bold>"); $this->say("5. Run tests using: <comment>codecept run</comment>"); } protected function createDirs() { $this->createDirectoryFor('tests'); $this->createEmptyDirectory($this->outputDir); $this->createEmptyDirectory($this->dataDir); $this->createDirectoryFor($this->supportDir . DIRECTORY_SEPARATOR . '_generated'); $this->createDirectoryFor($this->supportDir . DIRECTORY_SEPARATOR . "Helper"); $this->gitIgnore('tests/_output'); $this->gitIgnore('tests/_support/_generated'); } protected function createFunctionalSuite($actor = 'Functional') { $suiteConfig = <<<EOF # Codeception Test Suite Configuration # # Suite for functional tests # Emulate web requests and make application process them # Include one of framework modules (Symfony2, Yii2, Laravel5, Phalcon4) to use it # Remove this suite if you don't use frameworks actor: $actor{$this->actorSuffix} modules: enabled: # add a framework module here - \\{$this->namespace}Helper\Functional step_decorators: ~ EOF; $this->createSuite('functional', $actor, $suiteConfig); $this->say("tests/functional created <- functional tests"); $this->say("tests/functional.suite.yml written <- functional tests suite configuration"); } protected function createAcceptanceSuite($actor = 'Acceptance') { $suiteConfig = <<<EOF # Codeception Test Suite Configuration # # Suite for acceptance tests. # Perform tests in browser using the WebDriver or PhpBrowser. # If you need both WebDriver and PHPBrowser tests - create a separate suite. actor: $actor{$this->actorSuffix} modules: enabled: - PhpBrowser: url: http://localhost/myapp - \\{$this->namespace}Helper\Acceptance step_decorators: ~ EOF; $this->createSuite('acceptance', $actor, $suiteConfig); $this->say("tests/acceptance created <- acceptance tests"); $this->say("tests/acceptance.suite.yml written <- acceptance tests suite configuration"); } protected function createUnitSuite($actor = 'Unit') { $suiteConfig = <<<EOF # Codeception Test Suite Configuration # # Suite for unit or integration tests. actor: $actor{$this->actorSuffix} modules: enabled: - Asserts - \\{$this->namespace}Helper\Unit step_decorators: ~ EOF; $this->createSuite('unit', $actor, $suiteConfig); $this->say("tests/unit created <- unit tests"); $this->say("tests/unit.suite.yml written <- unit tests suite configuration"); } public function createGlobalConfig() { $basicConfig = [ 'paths' => [ 'tests' => 'tests', 'output' => $this->outputDir, 'data' => $this->dataDir, 'support' => $this->supportDir, 'envs' => $this->envsDir, ], 'actor_suffix' => 'Tester', 'extensions' => [ 'enabled' => ['Codeception\Extension\RunFailed'] ] ]; $str = Yaml::dump($basicConfig, 4); if ($this->namespace) { $namespace = rtrim($this->namespace, '\\'); $str = "namespace: $namespace\n" . $str; } $this->createFile('codeception.yml', $str); } protected function createSuite($suite, $actor, $config) { $this->createDirectoryFor("tests/$suite", "$suite.suite.yml"); $this->createHelper($actor, $this->supportDir); $this->createActor($actor . $this->actorSuffix, $this->supportDir, Yaml::parse($config)); $this->createFile('tests' . DIRECTORY_SEPARATOR . "$suite.suite.yml", $config); } } codeception/src/Codeception/Template/Unit.php000077700000006077151323602320015304 0ustar00<?php namespace Codeception\Template; use Codeception\InitTemplate; use Codeception\Util\Template; use Symfony\Component\Yaml\Yaml; class Unit extends InitTemplate { protected $configTemplate = <<<EOF suites: unit: path: . {{tester}} settings: shuffle: true lint: true paths: tests: {{dir}} output: {{dir}}/_output support: {{dir}}/_support data: {{dir}} EOF; protected $testerAndModules = <<<EOF actor: UnitTester modules: enabled: # add more modules here - Asserts step_decorators: ~ EOF; public function setup() { $this->sayInfo('This will install Codeception for unit testing only'); $this->say(); $dir = $this->ask("Where tests will be stored?", 'tests'); if (!$this->namespace) { $this->namespace = $this->ask("Enter a default namespace for tests (or skip this step)"); } $this->say(); $this->say("Codeception provides additional features for integration tests"); $this->say("Like accessing frameworks, ORM, Database."); $haveTester = $this->ask("Do you wish to enable them?", false); $this->createEmptyDirectory($outputDir = $dir . DIRECTORY_SEPARATOR . '_output'); $this->createEmptyDirectory($supportDir = $dir . DIRECTORY_SEPARATOR . '_support'); $configFile = (new Template($this->configTemplate)) ->place('dir', $dir) ->place('tester', $haveTester ? $this->testerAndModules : '') ->produce(); if ($this->namespace) { $namespace = rtrim($this->namespace, '\\'); $configFile = "namespace: $namespace\n" . $configFile; } $this->createFile('codeception.yml', $configFile); if (!class_exists('\\Codeception\\Module\\Asserts')) { $this->addModulesToComposer(['Asserts']); } if ($haveTester) { $this->createHelper('Unit', $supportDir); $this->createActor('UnitTester', $supportDir, Yaml::parse($configFile)['suites']['unit']); } $this->gitIgnore($outputDir); $this->sayInfo("Created test directory inside at $dir"); $this->say(); $this->saySuccess("INSTALLATION COMPLETE"); $this->say(); $this->say('Unit tests will be executed in random order'); $this->say('Use @depends annotation to change the order of tests'); if ($haveTester) { $this->say('To access DI, ORM, Database enable corresponding modules in codeception.yml'); $this->say('Use <bold>$this->tester</bold> object inside Codeception\Test\Unit to call their methods'); $this->say("For example: \$this->tester->seeInDatabase('users', ['name' => 'davert'])"); } $this->say(); $this->say("<bold>Next steps:</bold>"); $this->say("Create the first test using <comment>codecept g:test unit MyTest</comment>"); $this->say("Run tests with <comment>codecept run</comment>"); $this->say("<bold>Happy testing!</bold>"); } } codeception/src/Codeception/Template/Api.php000077700000005653151323602320015075 0ustar00<?php namespace Codeception\Template; use Codeception\InitTemplate; use Codeception\Util\Template; use Symfony\Component\Yaml\Yaml; class Api extends InitTemplate { protected $configTemplate = <<<EOF # suite config suites: api: actor: ApiTester path: . modules: enabled: - REST: url: {{url}} depends: PhpBrowser paths: tests: {{baseDir}} output: {{baseDir}}/_output data: {{baseDir}}/_data support: {{baseDir}}/_support settings: shuffle: false lint: true EOF; protected $firstTest = <<<EOF <?php class ApiCest { public function tryApi(ApiTester \$I) { \$I->sendGet('/'); \$I->seeResponseCodeIs(200); \$I->seeResponseIsJson(); } } EOF; public function setup() { $this->checkInstalled(); $this->say("Let's prepare Codeception for REST API testing"); $this->say(""); $dir = $this->ask("Where tests will be stored?", 'tests'); $url = $this->ask("Start url for tests", "http://localhost/api"); if (!class_exists('\\Codeception\\Module\\REST') || !class_exists('\\Codeception\\Module\\PhpBrowser')) { $this->addModulesToComposer(['REST', 'PhpBrowser']); } $this->createEmptyDirectory($outputDir = $dir . DIRECTORY_SEPARATOR . '_output'); $this->createEmptyDirectory($dir . DIRECTORY_SEPARATOR . '_data'); $this->createDirectoryFor($supportDir = $dir . DIRECTORY_SEPARATOR . '_support'); $this->createDirectoryFor($supportDir . DIRECTORY_SEPARATOR . '_generated'); $this->gitIgnore($outputDir); $this->gitIgnore($supportDir . DIRECTORY_SEPARATOR . '_generated'); $this->sayInfo("Created test directories inside at $dir"); $configFile = (new Template($this->configTemplate)) ->place('url', $url) ->place('baseDir', $dir) ->produce(); if ($this->namespace) { $namespace = rtrim($this->namespace, '\\'); $configFile = "namespace: $namespace\n" . $configFile; } $this->createFile('codeception.yml', $configFile); $this->createHelper('Api', $supportDir); $this->createActor('ApiTester', $supportDir, Yaml::parse($configFile)['suites']['api']); $this->sayInfo("Created global config codeception.yml inside the root directory"); $this->createFile($dir . DIRECTORY_SEPARATOR . 'ApiCest.php', $this->firstTest); $this->sayInfo("Created a demo test ApiCest.php"); $this->say(); $this->saySuccess("INSTALLATION COMPLETE"); $this->say(); $this->say("<bold>Next steps:</bold>"); $this->say("1. Edit <bold>$dir/ApiCest.php</bold> to write first API tests"); $this->say("2. Run tests using: <comment>codecept run</comment>"); $this->say(); $this->say("<bold>Happy testing!</bold>"); } } codeception/src/Codeception/Template/.htaccess000077700000000177151323602320015445 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Application.php000077700000013020151323602320015037 0ustar00<?php namespace Codeception; use Codeception\Exception\ConfigurationException; use Symfony\Component\Console\Application as BaseApplication; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Output\OutputInterface; class Application extends BaseApplication { /** * @var ArgvInput */ protected $coreArguments = null; /** * Register commands from config file * * extensions: * commands: * - Project\Command\MyCustomCommand * */ public function registerCustomCommands() { try { $this->readCustomCommandsFromConfig(); } catch (ConfigurationException $e) { if ($e->getCode() === 404) { return; } $this->renderExceptionWrapper($e, new ConsoleOutput()); exit(1); } catch (\Exception $e) { $this->renderExceptionWrapper($e, new ConsoleOutput()); exit(1); } } public function renderExceptionWrapper(\Exception $e, OutputInterface $output) { if (method_exists('Symfony\Component\Console\Application', 'renderException')) { //Symfony 5 parent::renderException($e, $output); } else { parent::renderThrowable($e, $output); } } /** * Search custom commands and register them. * * @throws ConfigurationException */ protected function readCustomCommandsFromConfig() { $this->getCoreArguments(); // Maybe load outside configfile $config = Configuration::config(); if (empty($config['extensions']['commands'])) { return; } foreach ($config['extensions']['commands'] as $commandClass) { $commandName = $this->getCustomCommandName($commandClass); $this->add(new $commandClass($commandName)); } } /** * Validate and get the name of the command * * @param CustomCommandInterface $commandClass * * @throws ConfigurationException * * @return string */ protected function getCustomCommandName($commandClass) { if (!class_exists($commandClass)) { throw new ConfigurationException("Extension: Command class $commandClass not found"); } $interfaces = class_implements($commandClass); if (!in_array('Codeception\CustomCommandInterface', $interfaces)) { throw new ConfigurationException("Extension: Command {$commandClass} must implement " . "the interface `Codeception\\CustomCommandInterface`"); } return $commandClass::getCommandName(); } /** * To cache Class ArgvInput * * @inheritDoc */ public function run(InputInterface $input = null, OutputInterface $output = null) { if ($input === null) { $input = $this->getCoreArguments(); } if (!ini_get('register_argc_argv')) { throw new ConfigurationException('register_argc_argv must be set to On for running Codeception'); } return parent::run($input, $output); } /** * Add global a --config option. * * @return InputDefinition */ protected function getDefaultInputDefinition() { $inputDefinition = parent::getDefaultInputDefinition(); $inputDefinition->addOption( new InputOption('config', 'c', InputOption::VALUE_OPTIONAL, 'Use custom path for config') ); return $inputDefinition; } /** * Search for --config Option and if found will be loaded * * example: * -c file.yml|dir * -cfile.yml|dir * --config file.yml|dir * --config=file.yml|dir * * @return ArgvInput */ protected function getCoreArguments() { if ($this->coreArguments !== null) { return $this->coreArguments; } $argvWithoutConfig = []; if (isset($_SERVER['argv'])) { $argv = $_SERVER['argv']; for ($i = 0; $i < count($argv); $i++) { if (preg_match('/^(?:-([^c-]*)?c|--config(?:=|$))(.*)$/', $argv[$i], $match)) { if (!empty($match[2])) { //same index $this->preloadConfiguration($match[2]); } elseif (isset($argv[$i + 1])) { //next index $this->preloadConfiguration($argv[++$i]); } if (!empty($match[1])) { $argvWithoutConfig[] = "-" . $match[1]; //rest commands } continue; } $argvWithoutConfig[] = $argv[$i]; } } return $this->coreArguments = new ArgvInput($argvWithoutConfig); } /** * Pre load Configuration, the config option is use. * * @param string $configFile Path to Configuration * * @throws ConfigurationException */ protected function preloadConfiguration($configFile) { try { Configuration::config($configFile); } catch (ConfigurationException $e) { if ($e->getCode() == 404) { throw new ConfigurationException("Your configuration file `{$configFile}` could not be found.", 405); } throw $e; } } } codeception/src/Codeception/Subscriber/PrepareTest.php000077700000002115151323602320017140 0ustar00<?php namespace Codeception\Subscriber; use Codeception\Event\TestEvent; use Codeception\Events; use Codeception\Lib\Di; use Codeception\Test\Cest; use Codeception\Test\Unit; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class PrepareTest implements EventSubscriberInterface { use Shared\StaticEvents; public static $events = [ Events::TEST_BEFORE => 'prepare', ]; protected $modules = []; public function prepare(TestEvent $event) { $test = $event->getTest(); /** @var $di Di **/ $prepareMethods = $test->getMetadata()->getParam('prepare'); if (!$prepareMethods) { return; } $di = $test->getMetadata()->getService('di'); foreach ($prepareMethods as $method) { /** @var $module \Codeception\Module **/ if ($test instanceof Cest) { $di->injectDependencies($test->getTestClass(), $method); } if ($test instanceof Unit) { $di->injectDependencies($test, $method); } } } } codeception/src/Codeception/Subscriber/Dependencies.php000077700000002330151323602320017267 0ustar00<?php namespace Codeception\Subscriber; use Codeception\Event\TestEvent; use Codeception\Test\Descriptor; use Codeception\Test\Interfaces\Dependent; use Codeception\TestInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Codeception\Events; class Dependencies implements EventSubscriberInterface { use Shared\StaticEvents; static $events = [ Events::TEST_START => 'testStart', Events::TEST_SUCCESS => 'testSuccess' ]; protected $successfulTests = []; public function testStart(TestEvent $event) { $test = $event->getTest(); if (!$test instanceof Dependent) { return; } $testSignatures = $test->fetchDependencies(); foreach ($testSignatures as $signature) { if (!in_array($signature, $this->successfulTests)) { $test->getMetadata()->setSkip("This test depends on $signature to pass"); return; } } } public function testSuccess(TestEvent $event) { $test = $event->getTest(); if (!$test instanceof TestInterface) { return; } $this->successfulTests[] = Descriptor::getTestSignature($test); } } codeception/src/Codeception/Subscriber/README.md000077700000000437151323602320015455 0ustar00# Event Listeners (Subscribers) Where this is possible Codeception uses the Observer pattern to separate different parts of framework and make them act independently. Events are defined in `Codeception\Event`). New features can be added seamlessly when they are created in Subscribers.codeception/src/Codeception/Subscriber/FailFast.php000077700000000741151323602320016376 0ustar00<?php namespace Codeception\Subscriber; use Codeception\Event\SuiteEvent; use Codeception\Events; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class FailFast implements EventSubscriberInterface { use Shared\StaticEvents; public static $events = [ Events::SUITE_BEFORE => 'stopOnFail', ]; public function stopOnFail(SuiteEvent $e) { $e->getResult()->stopOnError(true); $e->getResult()->stopOnFailure(true); } } codeception/src/Codeception/Subscriber/Bootstrap.php000077700000001243151323602320016660 0ustar00<?php namespace Codeception\Subscriber; use Codeception\Configuration; use Codeception\Event\SuiteEvent; use Codeception\Events; use Codeception\Exception\ConfigurationException; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class Bootstrap implements EventSubscriberInterface { use Shared\StaticEvents; public static $events = [ Events::SUITE_INIT => 'loadBootstrap', ]; public function loadBootstrap(SuiteEvent $e) { $settings = $e->getSettings(); if (!isset($settings['bootstrap'])) { return; } Configuration::loadBootstrap($settings['bootstrap'], $settings['path']); } } codeception/src/Codeception/Subscriber/Module.php000077700000004554151323602320016140 0ustar00<?php namespace Codeception\Subscriber; use Codeception\Event\FailEvent; use Codeception\Event\StepEvent; use Codeception\Event\SuiteEvent; use Codeception\Event\TestEvent; use Codeception\Events; use Codeception\Suite; use Codeception\TestInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class Module implements EventSubscriberInterface { use Shared\StaticEvents; public static $events = [ Events::TEST_BEFORE => 'before', Events::TEST_AFTER => 'after', Events::STEP_BEFORE => 'beforeStep', Events::STEP_AFTER => 'afterStep', Events::TEST_FAIL => 'failed', Events::TEST_ERROR => 'failed', Events::SUITE_BEFORE => 'beforeSuite', Events::SUITE_AFTER => 'afterSuite' ]; protected $modules = []; public function beforeSuite(SuiteEvent $e) { $suite = $e->getSuite(); if (!$suite instanceof Suite) { return; } $this->modules = $suite->getModules(); foreach ($this->modules as $module) { $module->_beforeSuite($e->getSettings()); } } public function afterSuite() { foreach ($this->modules as $module) { $module->_afterSuite(); } } public function before(TestEvent $event) { if (!$event->getTest() instanceof TestInterface) { return; } foreach ($this->modules as $module) { $module->_before($event->getTest()); } } public function after(TestEvent $e) { if (!$e->getTest() instanceof TestInterface) { return; } foreach ($this->modules as $module) { $module->_after($e->getTest()); $module->_resetConfig(); } } public function failed(FailEvent $e) { if (!$e->getTest() instanceof TestInterface) { return; } foreach ($this->modules as $module) { $module->_failed($e->getTest(), $e->getFail()); } } public function beforeStep(StepEvent $e) { foreach ($this->modules as $module) { $module->_beforeStep($e->getStep(), $e->getTest()); } } public function afterStep(StepEvent $e) { foreach ($this->modules as $module) { $module->_afterStep($e->getStep(), $e->getTest()); } } } codeception/src/Codeception/Subscriber/BeforeAfterTest.php000077700000003007151323602320017727 0ustar00<?php namespace Codeception\Subscriber; use Codeception\Event\SuiteEvent; use Codeception\Events; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class BeforeAfterTest implements EventSubscriberInterface { use Shared\StaticEvents; public static $events = [ Events::SUITE_BEFORE => 'beforeClass', Events::SUITE_AFTER => ['afterClass', 100] ]; protected $hooks = []; protected $startedTests = []; protected $unsuccessfulTests = []; public function beforeClass(SuiteEvent $e) { foreach ($e->getSuite()->tests() as $test) { /** @var $test \PHPUnit\Framework\Test * */ if ($test instanceof \PHPUnit\Framework\TestSuite\DataProvider) { $potentialTestClass = strstr($test->getName(), '::', true); $this->hooks[$potentialTestClass] = \PHPUnit\Util\Test::getHookMethods($potentialTestClass); } $testClass = get_class($test); $this->hooks[$testClass] = \PHPUnit\Util\Test::getHookMethods($testClass); } $this->runHooks('beforeClass'); } public function afterClass(SuiteEvent $e) { $this->runHooks('afterClass'); } protected function runHooks($hookName) { foreach ($this->hooks as $className => $hook) { foreach ($hook[$hookName] as $method) { if (is_callable([$className, $method])) { call_user_func([$className, $method]); } } } } } codeception/src/Codeception/Subscriber/ErrorHandler.php000077700000011407151323602320017275 0ustar00<?php namespace Codeception\Subscriber; use Codeception\Event\SuiteEvent; use Codeception\Events; use Codeception\Lib\Notification; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class ErrorHandler implements EventSubscriberInterface { use Shared\StaticEvents; public static $events = [ Events::SUITE_BEFORE => 'handle', Events::SUITE_AFTER => 'onFinish' ]; /** * @var bool $stopped to keep shutdownHandler from possible looping. */ private $stopped = false; /** * @var bool $initialized to avoid double error handler substitution */ private $initialized = false; private $deprecationsInstalled = false; private $oldHandler; private $suiteFinished = false; /** * @var int stores bitmask for errors */ private $errorLevel; public function __construct() { $this->errorLevel = E_ALL & ~E_STRICT & ~E_DEPRECATED; } public function onFinish(SuiteEvent $e) { $this->suiteFinished = true; } public function handle(SuiteEvent $e) { $settings = $e->getSettings(); if ($settings['error_level']) { $this->errorLevel = eval("return {$settings['error_level']};"); } error_reporting($this->errorLevel); if ($this->initialized) { return; } // We must register shutdown function before deprecation error handler to restore previous error handler // and silence DeprecationErrorHandler yelling about 'THE ERROR HANDLER HAS CHANGED!' register_shutdown_function([$this, 'shutdownHandler']); $this->registerDeprecationErrorHandler(); $this->oldHandler = set_error_handler([$this, 'errorHandler']); $this->initialized = true; } public function errorHandler($errno, $errstr, $errfile, $errline, $context = array()) { if (E_USER_DEPRECATED === $errno) { $this->handleDeprecationError($errno, $errstr, $errfile, $errline, $context); return; } if (!(error_reporting() & $errno)) { // This error code is not included in error_reporting return false; } if (strpos($errstr, 'Cannot modify header information') !== false) { return false; } throw new \PHPUnit\Framework\Exception($errstr, $errno); } public function shutdownHandler() { if ($this->deprecationsInstalled) { restore_error_handler(); } if ($this->stopped) { return; } $this->stopped = true; $error = error_get_last(); if (!$this->suiteFinished && ( $error === null || !in_array($error['type'], [E_ERROR, E_COMPILE_ERROR, E_CORE_ERROR]) )) { echo "\n\n\nCOMMAND DID NOT FINISH PROPERLY.\n"; exit(255); } if (!is_array($error)) { return; } if (error_reporting() === 0) { return; } // not fatal if (!in_array($error['type'], [E_ERROR, E_COMPILE_ERROR, E_CORE_ERROR])) { return; } echo "\n\n\nFATAL ERROR. TESTS NOT FINISHED.\n"; echo sprintf("%s \nin %s:%d\n", $error['message'], $error['file'], $error['line']); } private function registerDeprecationErrorHandler() { if (class_exists('\Symfony\Bridge\PhpUnit\DeprecationErrorHandler') && 'disabled' !== getenv('SYMFONY_DEPRECATIONS_HELPER')) { // DeprecationErrorHandler only will be installed if array('PHPUnit\Util\ErrorHandler', 'handleError') // is installed or no other error handlers are installed. // So we will remove Symfony\Component\Debug\ErrorHandler if it's installed. $old = set_error_handler('var_dump'); restore_error_handler(); if ($old && is_array($old) && count($old) > 0 && is_object($old[0]) && get_class($old[0]) === 'Symfony\Component\Debug\ErrorHandler' ) { restore_error_handler(); } $this->deprecationsInstalled = true; \Symfony\Bridge\PhpUnit\DeprecationErrorHandler::register(getenv('SYMFONY_DEPRECATIONS_HELPER')); } } private function handleDeprecationError($type, $message, $file, $line, $context) { if (!($this->errorLevel & $type)) { return; } if (strpos($message, 'Symfony 4.3')) { // skip Symfony 4.3 deprecations return; } if ($this->deprecationsInstalled && $this->oldHandler) { call_user_func($this->oldHandler, $type, $message, $file, $line, $context); return; } Notification::deprecate("$message", "$file:$line"); } } codeception/src/Codeception/Subscriber/AutoRebuild.php000077700000004247151323602320017131 0ustar00<?php namespace Codeception\Subscriber; use Codeception\Configuration; use Codeception\Event\SuiteEvent; use Codeception\Events; use Codeception\Lib\Generator\Actions; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class AutoRebuild implements EventSubscriberInterface { use Shared\StaticEvents; public static $events = [ Events::SUITE_INIT => 'updateActor' ]; public function updateActor(SuiteEvent $e) { $settings = $e->getSettings(); if (!$settings['actor']) { codecept_debug('actor is empty'); return; // no actor } $modules = $e->getSuite()->getModules(); $actorActionsFile = Configuration::supportDir() . '_generated' . DIRECTORY_SEPARATOR . $settings['actor'] . 'Actions.php'; if (!file_exists($actorActionsFile)) { codecept_debug("Generating {$settings['actor']}Actions..."); $this->generateActorActions($actorActionsFile, $settings); return; } // load actor class to see hash $handle = @fopen($actorActionsFile, "r"); if ($handle and is_writable($actorActionsFile)) { $line = @fgets($handle); if (preg_match('~\[STAMP\] ([a-f0-9]*)~', $line, $matches)) { $hash = $matches[1]; $currentHash = Actions::genHash($modules, $settings); // regenerate actor class when hashes do not match if ($hash != $currentHash) { codecept_debug("Rebuilding {$settings['actor']}..."); @fclose($handle); $this->generateActorActions($actorActionsFile, $settings); return; } } @fclose($handle); } } protected function generateActorActions($actorActionsFile, $settings) { if (!file_exists(Configuration::supportDir() . '_generated')) { @mkdir(Configuration::supportDir() . '_generated'); } $actionsGenerator = new Actions($settings); $generated = $actionsGenerator->produce(); @file_put_contents($actorActionsFile, $generated); } } codeception/src/Codeception/Subscriber/ExtensionLoader.php000077700000007721151323602320020015 0ustar00<?php namespace Codeception\Subscriber; use Codeception\Configuration; use Codeception\Event\SuiteEvent; use Codeception\Events; use Codeception\Exception\ConfigurationException; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class ExtensionLoader implements EventSubscriberInterface { use Shared\StaticEvents; public static $events = [ Events::MODULE_INIT => 'registerSuiteExtensions', Events::SUITE_AFTER => 'stopSuiteExtensions' ]; protected $config; protected $options = []; protected $globalExtensions = []; protected $suiteExtensions = []; /** * @var EventDispatcher */ protected $dispatcher; public function __construct(EventDispatcher $dispatcher) { $this->dispatcher = $dispatcher; $this->config = Configuration::config(); } public function bootGlobalExtensions($options) { $this->options = $options; $this->globalExtensions = $this->bootExtensions($this->config); } public function registerGlobalExtensions() { foreach ($this->globalExtensions as $extension) { $this->dispatcher->addSubscriber($extension); } } public function registerSuiteExtensions(SuiteEvent $e) { $suiteConfig = $e->getSettings(); $extensions = $this->bootExtensions($suiteConfig); $this->suiteExtensions = []; foreach ($extensions as $extension) { $extensionClass = get_class($extension); if (isset($this->globalExtensions[$extensionClass])) { continue; // already globally enabled } $this->dispatcher->addSubscriber($extension); $this->suiteExtensions[$extensionClass] = $extension; } } public function stopSuiteExtensions() { foreach ($this->suiteExtensions as $extension) { $this->dispatcher->removeSubscriber($extension); } $this->suiteExtensions = []; } protected function bootExtensions($config) { $extensions = []; foreach ($config['extensions']['enabled'] as $extensionClass) { if (is_array($extensionClass)) { $extensionClass = key($extensionClass); } if (!class_exists($extensionClass)) { throw new ConfigurationException( "Class `$extensionClass` is not defined. Autoload it or include into " . "'_bootstrap.php' file of 'tests' directory" ); } $extensionConfig = $this->getExtensionConfig($extensionClass, $config); $extension = new $extensionClass($extensionConfig, $this->options); if (!$extension instanceof EventSubscriberInterface) { throw new ConfigurationException( "Class $extensionClass is not an EventListener. Please create it as Extension or GroupObject." ); } $extensions[get_class($extension)] = $extension; } return $extensions; } private function getExtensionConfig($extension, $config) { $extensionConfig = isset($config['extensions']['config'][$extension]) ? $config['extensions']['config'][$extension] : []; if (!isset($config['extensions']['enabled'])) { return $extensionConfig; } if (!is_array($config['extensions']['enabled'])) { return $extensionConfig; } foreach ($config['extensions']['enabled'] as $enabledExtensionsConfig) { if (!is_array($enabledExtensionsConfig)) { continue; } $enabledExtension = key($enabledExtensionsConfig); if ($enabledExtension === $extension) { return Configuration::mergeConfigs(reset($enabledExtensionsConfig), $extensionConfig); } } return $extensionConfig; } } codeception/src/Codeception/Subscriber/GracefulTermination.php000077700000003033151323602320020644 0ustar00<?php declare (ticks = 1); namespace Codeception\Subscriber; use Codeception\Event\SuiteEvent; use Codeception\Events; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class GracefulTermination implements EventSubscriberInterface { const SIGNAL_FUNC = 'pcntl_signal'; const ASYNC_SIGNAL_HANDLING_FUNC = 'pcntl_async_signals'; /** * @var SuiteEvent */ protected $suiteEvent; public function handleSuite(SuiteEvent $event) { if (PHP_MAJOR_VERSION === 7 && PHP_MINOR_VERSION === 0) { // skip for PHP 7.0: https://github.com/Codeception/Codeception/issues/3607 return; } if (function_exists(self::ASYNC_SIGNAL_HANDLING_FUNC)) { pcntl_async_signals(true); } if (function_exists(self::SIGNAL_FUNC)) { pcntl_signal(SIGTERM, [$this, 'terminate']); pcntl_signal(SIGINT, [$this, 'terminate']); } $this->suiteEvent = $event; } public function terminate() { if ($this->suiteEvent) { $this->suiteEvent->getResult()->stopOnError(true); $this->suiteEvent->getResult()->stopOnFailure(true); } throw new \RuntimeException( "\n\n---------------------------\nTESTS EXECUTION TERMINATED\n---------------------------\n" ); } public static function getSubscribedEvents() { if (!function_exists(self::SIGNAL_FUNC)) { return []; } return [Events::SUITE_BEFORE => 'handleSuite']; } } codeception/src/Codeception/Subscriber/Console.php000077700000050256151323602320016315 0ustar00<?php namespace Codeception\Subscriber; use Codeception\Event\FailEvent; use Codeception\Event\PrintResultEvent; use Codeception\Event\StepEvent; use Codeception\Event\SuiteEvent; use Codeception\Event\TestEvent; use Codeception\Events; use Codeception\Lib\Console\Message; use Codeception\Lib\Console\MessageFactory; use Codeception\Lib\Console\Output; use Codeception\Lib\Notification; use Codeception\Step; use Codeception\Step\Comment; use Codeception\Suite; use Codeception\Test\Descriptor; use Codeception\Test\Interfaces\ScenarioDriven; use Codeception\TestInterface; use Codeception\Util\Debug; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class Console implements EventSubscriberInterface { use Shared\StaticEvents; /** * @var string[] */ public static $events = [ Events::SUITE_BEFORE => 'beforeSuite', Events::SUITE_AFTER => 'afterSuite', Events::TEST_START => 'startTest', Events::TEST_END => 'endTest', Events::STEP_BEFORE => 'beforeStep', Events::STEP_AFTER => 'afterStep', Events::TEST_SUCCESS => 'testSuccess', Events::TEST_FAIL => 'testFail', Events::TEST_ERROR => 'testError', Events::TEST_INCOMPLETE => 'testIncomplete', Events::TEST_SKIPPED => 'testSkipped', Events::TEST_WARNING => 'testWarning', Events::TEST_FAIL_PRINT => 'printFail', Events::RESULT_PRINT_AFTER => 'afterResult', ]; /** * @var Step */ protected $metaStep; /** * @var Message */ protected $message = null; protected $steps = true; protected $debug = false; protected $ansi = true; protected $silent = false; protected $lastTestFailed = false; protected $printedTest = null; protected $rawStackTrace = false; protected $traceLength = 5; protected $width; /** * @var OutputInterface */ protected $output; protected $conditionalFails = []; protected $failedStep = []; protected $reports = []; protected $namespace = ''; protected $chars = ['success' => '+', 'fail' => 'x', 'of' => ':']; protected $options = [ 'debug' => false, 'ansi' => false, 'steps' => true, 'verbosity' => 0, 'xml' => null, 'phpunit-xml' => null, 'html' => null, 'tap' => null, 'json' => null, 'no-artifacts' => false, ]; /** * @var MessageFactory */ protected $messageFactory; public function __construct($options) { $this->prepareOptions($options); $this->output = new Output($options); $this->messageFactory = new MessageFactory($this->output); if ($this->debug) { Debug::setOutput($this->output); } $this->detectWidth(); if ($this->options['ansi'] && !$this->isWin()) { $this->chars['success'] = '✔'; $this->chars['fail'] = '✖'; } foreach (['html', 'xml', 'phpunit-xml', 'tap', 'json'] as $report) { if (!$this->options[$report]) { continue; } $path = $this->absolutePath($this->options[$report]); $this->reports[] = sprintf( "- <bold>%s</bold> report generated in <comment>file://%s</comment>", strtoupper($report), $path ); } } // triggered for scenario based tests: cept, cest public function beforeSuite(SuiteEvent $e) { $this->namespace = ""; $settings = $e->getSettings(); if (isset($settings['namespace'])) { $this->namespace = $settings['namespace']; } $this->message("%s Tests (%d) ") ->with(ucfirst($e->getSuite()->getName()), $e->getSuite()->count()) ->style('bold') ->width($this->width, '-') ->prepend("\n") ->writeln(); if ($e->getSuite() instanceof Suite) { $message = $this->message( implode( ', ', array_map( function ($module) { return $module->_getName(); }, $e->getSuite()->getModules() ) ) ); $message->style('info') ->prepend('Modules: ') ->writeln(OutputInterface::VERBOSITY_VERBOSE); } $this->message('')->width($this->width, '-')->writeln(OutputInterface::VERBOSITY_VERBOSE); } // triggered for all tests public function startTest(TestEvent $e) { $this->conditionalFails = []; $test = $e->getTest(); $this->printedTest = $test; $this->message = null; if (!$this->output->isInteractive() and !$this->isDetailed($test)) { return; } $this->writeCurrentTest($test); if ($this->isDetailed($test)) { $this->output->writeln(''); $this->message(Descriptor::getTestSignature($test)) ->style('info') ->prepend('Signature: ') ->writeln(); $this->message(codecept_relative_path(Descriptor::getTestFullName($test))) ->style('info') ->prepend('Test: ') ->writeln(); if ($this->steps) { $this->message('Scenario --')->style('comment')->writeln(); $this->output->waitForDebugOutput = false; } } } public function afterStep(StepEvent $e) { $step = $e->getStep(); if (!$step->hasFailed()) { return; } if ($step instanceof Step\ConditionalAssertion) { $this->conditionalFails[] = $step; return; } $this->failedStep[] = $step; } /** * @param PrintResultEvent $event */ public function afterResult(PrintResultEvent $event) { $result = $event->getResult(); if ($result->skippedCount() + $result->notImplementedCount() > 0 and $this->options['verbosity'] < OutputInterface::VERBOSITY_VERBOSE) { $this->output->writeln("run with `-v` to get more info about skipped or incomplete tests"); } foreach ($this->reports as $message) { $this->output->writeln($message); } } private function absolutePath($path) { if ((strpos($path, '/') === 0) or (strpos($path, ':') === 1)) { // absolute path return $path; } return codecept_output_dir() . $path; } public function testSuccess(TestEvent $e) { if ($this->isDetailed($e->getTest())) { $this->message('PASSED')->center(' ')->style('ok')->append("\n")->writeln(); return; } $this->writelnFinishedTest($e, $this->message($this->chars['success'])->style('ok')); } public function endTest(TestEvent $e) { $this->metaStep = null; $this->printedTest = null; } public function testWarning(TestEvent $e) { if ($this->isDetailed($e->getTest())) { $this->message('WARNING')->center(' ')->style('pending')->append("\n")->writeln(); return; } $this->writelnFinishedTest($e, $this->message('W')->style('pending')); } public function testFail(FailEvent $e) { if ($this->isDetailed($e->getTest())) { $this->message('FAIL')->center(' ')->style('fail')->append("\n")->writeln(); return; } $this->writelnFinishedTest($e, $this->message($this->chars['fail'])->style('fail')); } public function testError(FailEvent $e) { if ($this->isDetailed($e->getTest())) { $this->message('ERROR')->center(' ')->style('fail')->append("\n")->writeln(); return; } $this->writelnFinishedTest($e, $this->message('E')->style('fail')); } public function testSkipped(FailEvent $e) { if ($this->isDetailed($e->getTest())) { $msg = $e->getFail()->getMessage(); $this->message('SKIPPED')->append($msg ? ": $msg" : '')->center(' ')->style('pending')->writeln(); return; } $this->writelnFinishedTest($e, $this->message('S')->style('pending')); } public function testIncomplete(FailEvent $e) { if ($this->isDetailed($e->getTest())) { $msg = $e->getFail()->getMessage(); $this->message('INCOMPLETE')->append($msg ? ": $msg" : '')->center(' ')->style('pending')->writeln(); return; } $this->writelnFinishedTest($e, $this->message('I')->style('pending')); } protected function isDetailed($test) { return $test instanceof ScenarioDriven && $this->steps; } public function beforeStep(StepEvent $e) { if (!$this->steps or !$e->getTest() instanceof ScenarioDriven) { return; } $metaStep = $e->getStep()->getMetaStep(); if ($metaStep and $this->metaStep != $metaStep) { $this->message(' ' . $metaStep->getPrefix()) ->style('bold') ->append($metaStep->__toString()) ->writeln(); } $this->metaStep = $metaStep; $this->printStep($e->getStep()); } private function printStep(Step $step) { if ($step instanceof Comment and $step->__toString() == '') { return; // don't print empty comments } $msg = $this->message(' '); if ($this->metaStep) { $msg->append(' '); } $msg->append($step->getPrefix()); $prefixLength = $msg->getLength(); if (!$this->metaStep) { $msg->style('bold'); } $maxLength = $this->width - $prefixLength; $msg->append(OutputFormatter::escape($step->toString($maxLength))); if ($this->metaStep) { $msg->style('info'); } $msg->writeln(); } public function afterSuite(SuiteEvent $e) { $this->message()->width($this->width, '-')->writeln(); $messages = Notification::all(); foreach (array_count_values($messages) as $message => $count) { if ($count > 1) { $message = $count . 'x ' . $message; } $this->output->notification($message); } } public function printFail(FailEvent $e) { $failedTest = $e->getTest(); $fail = $e->getFail(); $this->output->write($e->getCount() . ") "); $this->writeCurrentTest($failedTest, false); $this->output->writeln(''); $this->message("<error> Test </error> ") ->append(codecept_relative_path(Descriptor::getTestFullName($failedTest))) ->write(); if ($failedTest instanceof ScenarioDriven) { $this->printScenarioFail($failedTest, $fail); $this->printReports($failedTest); return; } $this->printException($fail); $this->printExceptionTrace($fail); } public function printReports(TestInterface $failedTest) { if ($this->options['no-artifacts']) { return; } $reports = $failedTest->getMetadata()->getReports(); if (count($reports)) { $this->output->writeln('<comment>Artifacts:</comment>'); $this->output->writeln(''); } foreach ($reports as $type => $report) { $type = ucfirst($type); $this->output->writeln("$type: <debug>$report</debug>"); } } public function printException($e, $cause = null) { if ($e instanceof \PHPUnit\Framework\SkippedTestError or $e instanceof \PHPUnit\Framework_IncompleteTestError) { if ($e->getMessage()) { $this->message(OutputFormatter::escape($e->getMessage()))->prepend("\n")->writeln(); } return; } $class = $e instanceof \PHPUnit\Framework\ExceptionWrapper ? $e->getClassname() : get_class($e); if (strpos($class, 'Codeception\Exception') === 0) { $class = substr($class, strlen('Codeception\Exception\\')); } $this->output->writeln(''); $message = $this->message(OutputFormatter::escape($e->getMessage())); if ($e instanceof \PHPUnit\Framework\ExpectationFailedException) { $comparisonFailure = $e->getComparisonFailure(); if ($comparisonFailure) { $message->append($this->messageFactory->prepareComparisonFailureMessage($comparisonFailure)); } } $isFailure = $e instanceof \PHPUnit\Framework\AssertionFailedError || $class === 'PHPUnit\Framework\ExpectationFailedException' || $class === 'PHPUnit\Framework\AssertionFailedError'; if (!$isFailure) { $message->prepend("[$class] ")->block('error'); } if ($isFailure && $cause) { $cause = OutputFormatter::escape(ucfirst($cause)); $message->prepend("<error> Step </error> $cause\n<error> Fail </error> "); } $message->writeln(); } public function printScenarioFail(ScenarioDriven $failedTest, $fail) { if ($this->conditionalFails) { $failedStep = (string) array_shift($this->conditionalFails); } else { $failedStep = (string) $failedTest->getScenario()->getMetaStep(); if ($failedStep === '') { $failedStep = (string) array_shift($this->failedStep); } } $this->printException($fail, $failedStep); $this->printScenarioTrace($failedTest); if ($this->output->getVerbosity() == OutputInterface::VERBOSITY_DEBUG) { $this->printExceptionTrace($fail); return; } if (!$fail instanceof \PHPUnit\Framework\AssertionFailedError) { $this->printExceptionTrace($fail); return; } } public function printExceptionTrace($e) { static $limit = 10; if ($e instanceof \PHPUnit\Framework\SkippedTestError or $e instanceof \PHPUnit\Framework_IncompleteTestError) { return; } if ($this->rawStackTrace) { $this->message(OutputFormatter::escape(\PHPUnit\Util\Filter::getFilteredStacktrace($e, true, false)))->writeln(); return; } $trace = \PHPUnit\Util\Filter::getFilteredStacktrace($e, false); $i = 0; foreach ($trace as $step) { if ($i >= $limit) { break; } $i++; $message = $this->message($i)->prepend('#')->width(4); if (!isset($step['file'])) { foreach (['class', 'type', 'function'] as $info) { if (!isset($step[$info])) { continue; } $message->append($step[$info]); } $message->writeln(); continue; } $message->append($step['file'] . ':' . $step['line']); $message->writeln(); } $prev = $e->getPrevious(); if ($prev) { $this->printExceptionTrace($prev); } } /** * @param $failedTest */ public function printScenarioTrace(ScenarioDriven $failedTest) { $trace = array_reverse($failedTest->getScenario()->getSteps()); $length = $stepNumber = count($trace); if (!$length) { return; } $this->message("\nScenario Steps:\n")->style('comment')->writeln(); foreach ($trace as $step) { /** * @var $step Step */ if (!$step->__toString()) { continue; } $message = $this ->message($stepNumber) ->prepend(' ') ->width(strlen($length)) ->append(". "); $message->append(OutputFormatter::escape($step->getPhpCode($this->width - $message->getLength()))); if ($step->hasFailed()) { $message->style('bold'); } $line = $step->getLine(); if ($line and (!$step instanceof Comment)) { $message->append(" at <info>$line</info>"); } $stepNumber--; $message->writeln(); if (($length - $stepNumber - 1) >= $this->traceLength) { break; } } $this->output->writeln(""); } public function detectWidth() { $this->width = 60; if (!$this->isWin() && (php_sapi_name() === "cli") && (getenv('TERM')) && (getenv('TERM') != 'unknown') ) { // try to get terminal width from ENV variable (bash), see also https://github.com/Codeception/Codeception/issues/3788 if (getenv('COLUMNS')) { $this->width = getenv('COLUMNS'); } else { $this->width = (int) (`command -v tput >> /dev/null 2>&1 && tput cols`) - 2; } } elseif ($this->isWin() && (php_sapi_name() === "cli")) { exec('mode con', $output); if (isset($output[4])) { preg_match('/^ +.* +(\d+)$/', $output[4], $matches); if (!empty($matches[1])) { $this->width = (int) $matches[1]; } } } return $this->width; } private function isWin() { return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; } /** * @param \PHPUnit\Framework\SelfDescribing $test * @param bool $inProgress */ protected function writeCurrentTest(\PHPUnit\Framework\SelfDescribing $test, $inProgress = true) { $prefix = ($this->output->isInteractive() and !$this->isDetailed($test) and $inProgress) ? '- ' : ''; $testString = Descriptor::getTestAsString($test); $testString = preg_replace('~^([^:]+):\s~', "<focus>$1{$this->chars['of']}</focus> ", $testString); $this ->message($testString) ->prepend($prefix) ->write(); } protected function writelnFinishedTest(TestEvent $event, Message $result) { $test = $event->getTest(); if ($this->isDetailed($test)) { return; } if ($this->output->isInteractive()) { $this->output->write("\x0D"); } $result->append(' ')->write(); $this->writeCurrentTest($test, false); $conditionalFailsMessage = ""; $numFails = count($this->conditionalFails); if ($numFails == 1) { $conditionalFailsMessage = "[F]"; } elseif ($numFails) { $conditionalFailsMessage = "{$numFails}x[F]"; } $conditionalFailsMessage = "<error>$conditionalFailsMessage</error> "; $this->message($conditionalFailsMessage)->write(); $this->writeTimeInformation($event); $this->output->writeln(''); } /** * @param $string * @return Message */ private function message($string = '') { return $this->messageFactory->message($string); } /** * @param TestEvent $event */ protected function writeTimeInformation(TestEvent $event) { $time = $event->getTime(); if ($time) { $this ->message(number_format(round($time, 2), 2)) ->prepend('(') ->append('s)') ->style('info') ->write(); } } /** * @param $options */ private function prepareOptions($options) { $this->options = array_merge($this->options, $options); $this->debug = $this->options['debug'] || $this->options['verbosity'] >= OutputInterface::VERBOSITY_VERY_VERBOSE; $this->steps = $this->debug || $this->options['steps']; $this->rawStackTrace = ($this->options['verbosity'] === OutputInterface::VERBOSITY_DEBUG); } } codeception/src/Codeception/Subscriber/Shared/StaticEvents.php000077700000000244151323602320020525 0ustar00<?php namespace Codeception\Subscriber\Shared; trait StaticEvents { public static function getSubscribedEvents() { return static::$events; } } codeception/src/Codeception/Subscriber/Shared/.htaccess000077700000000177151323602320017203 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Subscriber/.htaccess000077700000000177151323602320015775 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/SuiteManager.php000077700000017175151323602320015177 0ustar00<?php namespace Codeception; use Codeception\Event\DispatcherWrapper; use Codeception\Lib\Di; use Codeception\Lib\GroupManager; use Codeception\Lib\ModuleContainer; use Codeception\Lib\Notification; use Codeception\Test\Interfaces\ScenarioDriven; use Codeception\Test\Loader; use Codeception\Test\Descriptor; use Symfony\Component\EventDispatcher\EventDispatcher; class SuiteManager { use DispatcherWrapper; public static $environment; public static $name; /** * @var \PHPUnit\Framework\TestSuite */ protected $suite = null; /** * @var null|\Symfony\Component\EventDispatcher\EventDispatcher */ protected $dispatcher = null; /** * @var GroupManager */ protected $groupManager; /** * @var Loader */ protected $testLoader; /** * @var ModuleContainer */ protected $moduleContainer; /** * @var Di */ protected $di; protected $tests = []; protected $debug = false; protected $path = ''; protected $printer = null; protected $env = null; protected $settings; public function __construct(EventDispatcher $dispatcher, $name, array $settings) { $this->settings = $settings; $this->dispatcher = $dispatcher; $this->di = new Di(); $this->path = $settings['path']; $this->groupManager = new GroupManager($settings['groups']); $this->moduleContainer = new ModuleContainer($this->di, $settings); $modules = Configuration::modules($this->settings); foreach ($modules as $moduleName) { $this->moduleContainer->create($moduleName); } $this->moduleContainer->validateConflicts(); if (isset($settings['current_environment'])) { $this->env = $settings['current_environment']; } $this->suite = $this->createSuite($name); } public function initialize() { $this->dispatch($this->dispatcher, Events::MODULE_INIT, new Event\SuiteEvent($this->suite, null, $this->settings)); foreach ($this->moduleContainer->all() as $module) { $module->_initialize(); } if ($this->settings['actor'] && !file_exists(Configuration::supportDir() . $this->settings['actor'] . '.php')) { throw new Exception\ConfigurationException( $this->settings['actor'] . " class doesn't exist in suite folder.\nRun the 'build' command to generate it" ); } $this->dispatch($this->dispatcher, Events::SUITE_INIT, new Event\SuiteEvent($this->suite, null, $this->settings)); ini_set('xdebug.show_exception_trace', 0); // Issue https://github.com/symfony/symfony/issues/7646 } public function loadTests($path = null) { $testLoader = new Loader($this->settings); $testLoader->loadTests($path); $tests = $testLoader->getTests(); if ($this->settings['shuffle']) { shuffle($tests); } foreach ($tests as $test) { $this->addToSuite($test); } $this->suite->reorderDependencies(); } protected function addToSuite($test) { $this->configureTest($test); if ($test instanceof \PHPUnit\Framework\DataProviderTestSuite) { foreach ($test->tests() as $t) { $this->addToSuite($t); } return; } if ($test instanceof TestInterface) { $this->checkEnvironmentExists($test); if (!$this->isExecutedInCurrentEnvironment($test)) { return; // skip tests from other environments } } $groups = $this->groupManager->groupsForTest($test); $this->suite->addTest($test, $groups); if (!empty($groups) && $test instanceof TestInterface) { $test->getMetadata()->setGroups($groups); } } protected function createSuite($name) { $suite = new Suite(); $suite->setBaseName(preg_replace('~\s.+$~', '', $name)); // replace everything after space (env name) if ($this->settings['namespace']) { $name = $this->settings['namespace'] . ".$name"; } $suite->setName($name); if (isset($this->settings['backup_globals'])) { $suite->setBackupGlobals((bool) $this->settings['backup_globals']); } if (isset($this->settings['be_strict_about_changes_to_global_state']) && method_exists($suite, 'setbeStrictAboutChangesToGlobalState')) { $suite->setbeStrictAboutChangesToGlobalState((bool)$this->settings['be_strict_about_changes_to_global_state']); } $suite->setModules($this->moduleContainer->all()); return $suite; } public function run(PHPUnit\Runner $runner, \PHPUnit\Framework\TestResult $result, $options) { $runner->prepareSuite($this->suite, $options); $this->dispatch($this->dispatcher, Events::SUITE_BEFORE, new Event\SuiteEvent($this->suite, $result, $this->settings)); try { $runner->doEnhancedRun($this->suite, $result, $options); } finally { $this->dispatch($this->dispatcher, Events::SUITE_AFTER, new Event\SuiteEvent($this->suite, $result, $this->settings)); } } /** * @return \Codeception\Suite */ public function getSuite() { return $this->suite; } /** * @return ModuleContainer */ public function getModuleContainer() { return $this->moduleContainer; } protected function getActor() { if (!$this->settings['actor']) { return null; } return $this->settings['namespace'] ? rtrim($this->settings['namespace'], '\\') . '\\' . $this->settings['actor'] : $this->settings['actor']; } protected function checkEnvironmentExists(TestInterface $test) { $envs = $test->getMetadata()->getEnv(); if (empty($envs)) { return; } if (!isset($this->settings['env'])) { Notification::warning("Environments are not configured", Descriptor::getTestFullName($test)); return; } $availableEnvironments = array_keys($this->settings['env']); $listedEnvironments = explode(',', implode(',', $envs)); foreach ($listedEnvironments as $env) { if (!in_array($env, $availableEnvironments)) { Notification::warning("Environment $env was not configured but used in test", Descriptor::getTestFullName($test)); } } } protected function isExecutedInCurrentEnvironment(TestInterface $test) { $envs = $test->getMetadata()->getEnv(); if (empty($envs)) { return true; } $currentEnvironments = explode(',', $this->env); foreach ($envs as $envList) { $envList = explode(',', $envList); if (count($envList) == count(array_intersect($currentEnvironments, $envList))) { return true; } } return false; } /** * @param $t * @throws Exception\InjectionException */ protected function configureTest($t) { if (!$t instanceof TestInterface) { return; } $t->getMetadata()->setServices([ 'di' => clone($this->di), 'dispatcher' => $this->dispatcher, 'modules' => $this->moduleContainer ]); $t->getMetadata()->setCurrent([ 'actor' => $this->getActor(), 'env' => $this->env, 'modules' => $this->moduleContainer->all() ]); if ($t instanceof ScenarioDriven) { $t->preload(); } } } codeception/src/Codeception/Codecept.php000077700000020075151323602320014332 0ustar00<?php namespace Codeception; use Codeception\Event\DispatcherWrapper; use Codeception\Exception\ConfigurationException; use Codeception\Subscriber\ExtensionLoader; use Symfony\Component\EventDispatcher\EventDispatcher; class Codecept { use DispatcherWrapper; const VERSION = '4.1.15'; /** * @var \Codeception\PHPUnit\Runner */ protected $runner; /** * @var \PHPUnit\Framework\TestResult */ protected $result; /** * @var \Codeception\CodeCoverage */ protected $coverage; /** * @var \Symfony\Component\EventDispatcher\EventDispatcher */ protected $dispatcher; /** * @var ExtensionLoader */ protected $extensionLoader; /** * @var array */ protected $options = [ 'silent' => false, 'debug' => false, 'steps' => false, 'html' => false, 'xml' => false, 'phpunit-xml' => false, 'no-redirect' => true, 'json' => false, 'tap' => false, 'report' => false, 'colors' => false, 'coverage' => false, 'coverage-xml' => false, 'coverage-html' => false, 'coverage-text' => false, 'coverage-crap4j' => false, 'coverage-cobertura' => false, 'coverage-phpunit'=> false, 'groups' => null, 'excludeGroups' => null, 'filter' => null, 'env' => null, 'fail-fast' => false, 'ansi' => true, 'verbosity' => 1, 'interactive' => true, 'no-rebuild' => false, 'quiet' => false, ]; protected $config = []; /** * @var array */ protected $extensions = []; public function __construct($options = []) { $this->result = new \PHPUnit\Framework\TestResult; $this->dispatcher = new EventDispatcher(); $this->extensionLoader = new ExtensionLoader($this->dispatcher); $baseOptions = $this->mergeOptions($options); $this->extensionLoader->bootGlobalExtensions($baseOptions); // extensions may override config $this->config = Configuration::config(); $this->options = $this->mergeOptions($options); // options updated from config $this->registerSubscribers(); $this->registerPHPUnitListeners(); $this->registerPrinter(); } /** * Merges given options with default values and current configuration * * @param array $options options * @return array * @throws ConfigurationException */ protected function mergeOptions($options) { $config = Configuration::config(); $baseOptions = array_merge($this->options, $config['settings']); return array_merge($baseOptions, $options); } protected function registerPHPUnitListeners() { $listener = new PHPUnit\Listener($this->dispatcher); $this->result->addListener($listener); } public function registerSubscribers() { // required $this->dispatcher->addSubscriber(new Subscriber\GracefulTermination()); $this->dispatcher->addSubscriber(new Subscriber\ErrorHandler()); $this->dispatcher->addSubscriber(new Subscriber\Dependencies()); $this->dispatcher->addSubscriber(new Subscriber\Bootstrap()); $this->dispatcher->addSubscriber(new Subscriber\PrepareTest()); $this->dispatcher->addSubscriber(new Subscriber\Module()); $this->dispatcher->addSubscriber(new Subscriber\BeforeAfterTest()); // optional if (!$this->options['no-rebuild']) { $this->dispatcher->addSubscriber(new Subscriber\AutoRebuild()); } if (!$this->options['silent']) { $this->dispatcher->addSubscriber(new Subscriber\Console($this->options)); } if ($this->options['fail-fast']) { $this->dispatcher->addSubscriber(new Subscriber\FailFast()); } if ($this->options['coverage']) { $this->dispatcher->addSubscriber(new Coverage\Subscriber\Local($this->options)); $this->dispatcher->addSubscriber(new Coverage\Subscriber\LocalServer($this->options)); $this->dispatcher->addSubscriber(new Coverage\Subscriber\RemoteServer($this->options)); $this->dispatcher->addSubscriber(new Coverage\Subscriber\Printer($this->options)); } $this->dispatcher->addSubscriber($this->extensionLoader); $this->extensionLoader->registerGlobalExtensions(); } public function run($suite, $test = null, array $config = null) { ini_set( 'memory_limit', isset($this->config['settings']['memory_limit']) ? $this->config['settings']['memory_limit'] : '1024M' ); $config = $config ?: Configuration::config(); $config = Configuration::suiteSettings($suite, $config); $selectedEnvironments = $this->options['env']; if (!$selectedEnvironments || empty($config['env'])) { $this->runSuite($config, $suite, $test); return; } // Iterate over all unique environment sets and runs the given suite with each of the merged configurations. foreach (array_unique($selectedEnvironments) as $envList) { $envSet = explode(',', $envList); $suiteEnvConfig = $config; // contains a list of the environments used in this suite configuration env set. $envConfigs = []; foreach ($envSet as $currentEnv) { if (isset($config['env'])) { // The $settings['env'] actually contains all parsed configuration files as a // filename => filecontents key-value array. If there is no configuration file for the // $currentEnv the merge will be skipped. if (!array_key_exists($currentEnv, $config['env'])) { return; } // Merge configuration consecutively with already build configuration $suiteEnvConfig = Configuration::mergeConfigs($suiteEnvConfig, $config['env'][$currentEnv]); $envConfigs[] = $currentEnv; } } $suiteEnvConfig['current_environment'] = implode(',', $envConfigs); if (empty($suiteEnvConfig)) { continue; } $suiteToRun = $suite; if (!empty($envList)) { $suiteToRun .= ' (' . implode(', ', $envSet) . ')'; } $this->runSuite($suiteEnvConfig, $suiteToRun, $test); } } public function runSuite($settings, $suite, $test = null) { $suiteManager = new SuiteManager($this->dispatcher, $suite, $settings); $suiteManager->initialize(); srand($this->options['seed']); $suiteManager->loadTests($test); srand(); $suiteManager->run($this->runner, $this->result, $this->options); return $this->result; } public static function versionString() { return 'Codeception PHP Testing Framework v' . self::VERSION; } public function printResult() { $result = $this->getResult(); $result->flushListeners(); $printer = $this->runner->getPrinter(); $printer->printResult($result); $this->dispatch($this->dispatcher, Events::RESULT_PRINT_AFTER, new Event\PrintResultEvent($result, $printer)); } /** * @return \PHPUnit\Framework\TestResult */ public function getResult() { return $this->result; } public function getOptions() { return $this->options; } /** * @return EventDispatcher */ public function getDispatcher() { return $this->dispatcher; } protected function registerPrinter() { $printer = new PHPUnit\ResultPrinter\UI($this->dispatcher, $this->options); $this->runner = new PHPUnit\Runner(); $this->runner->setPrinter($printer); } } codeception/src/Codeception/Exception/ConfigurationException.php000077700000000134151323602320021222 0ustar00<?php namespace Codeception\Exception; class ConfigurationException extends \Exception { } codeception/src/Codeception/Exception/InjectionException.php000077700000000130151323602320020331 0ustar00<?php namespace Codeception\Exception; class InjectionException extends \Exception { } codeception/src/Codeception/Exception/ModuleConfigException.php000077700000000752151323602320020774 0ustar00<?php namespace Codeception\Exception; class ModuleConfigException extends \Exception { public function __construct($module, $message, \Exception $previous = null) { if (is_object($module)) { $module = get_class($module); } $module = str_replace('Codeception\Module\\', '', ltrim($module, '\\')); parent::__construct($message, 0, $previous); $this->message = $module . " module is not configured!\n \n" . $this->message; } } codeception/src/Codeception/Exception/ParseException.php000077700000000124151323602320017464 0ustar00<?php namespace Codeception\Exception; class ParseException extends \Exception { } codeception/src/Codeception/Exception/ExtensionException.php000077700000000607151323602320020374 0ustar00<?php namespace Codeception\Exception; class ExtensionException extends \Exception { public function __construct($extension, $message, \Exception $previous = null) { parent::__construct($message, $previous); if (is_object($extension)) { $extension = get_class($extension); } $this->message = $extension . "\n\n" . $this->message; } } codeception/src/Codeception/Exception/TestRuntimeException.php000077700000000141151323602320020674 0ustar00<?php namespace Codeception\Exception; class TestRuntimeException extends \RuntimeException { } codeception/src/Codeception/Exception/MalformedLocatorException.php000077700000000410151323602320021642 0ustar00<?php namespace Codeception\Exception; class MalformedLocatorException extends TestRuntimeException { public function __construct($locator, $type = "CSS or XPath") { parent::__construct(ucfirst($type) . " locator is malformed: $locator"); } } codeception/src/Codeception/Exception/ModuleConflictException.php000077700000002003151323602320021317 0ustar00<?php namespace Codeception\Exception; class ModuleConflictException extends \Exception { public function __construct($module, $conflicted, $additional = '') { if (is_object($module)) { $module = get_class($module); } if (is_object($conflicted)) { $conflicted = get_class($conflicted); } $module = ltrim(str_replace('Codeception\Module\\', '', $module), '\\'); $conflicted = ltrim(str_replace('Codeception\Module\\', '', $conflicted), '\\'); $this->message = "$module module conflicts with $conflicted\n\n--\n" . "This usually happens when you enable two modules with the same actions but with different backends.\n" . "For instance, you can't run PhpBrowser, WebDriver, Laravel5 modules in one suite,\n" . "as they implement similar methods but use different drivers to execute them.\n" . "You can load a part of module (like: ORM) to avoid conflict.\n" . $additional; } } codeception/src/Codeception/Exception/ConditionalAssertionFailed.php000077700000000175151323602320022001 0ustar00<?php namespace Codeception\Exception; class ConditionalAssertionFailed extends \PHPUnit\Framework\AssertionFailedError { } codeception/src/Codeception/Exception/ContentNotFound.php000077700000000162151323602320017624 0ustar00<?php namespace Codeception\Exception; class ContentNotFound extends \PHPUnit\Framework\AssertionFailedError { } codeception/src/Codeception/Exception/ModuleException.php000077700000000721151323602320017642 0ustar00<?php namespace Codeception\Exception; class ModuleException extends \Exception { protected $module; public function __construct($module, $message) { if (is_object($module)) { $module = get_class($module); } $module = ltrim(str_replace('Codeception\Module\\', '', $module), '\\'); $this->module = $module; parent::__construct($message); $this->message = "$module: {$this->message}"; } } codeception/src/Codeception/Exception/TestParseException.php000077700000000667151323602320020340 0ustar00<?php namespace Codeception\Exception; class TestParseException extends \Exception { public function __construct($fileName, $errors = null, $line = null) { if ($line) { $this->message = "Couldn't parse test '$fileName' on line $line"; } else { $this->message = "Couldn't parse test '$fileName'"; } if ($errors) { $this->message .= "\n$errors"; } } } codeception/src/Codeception/Exception/RemoteException.php000077700000000373151323602320017653 0ustar00<?php namespace Codeception\Exception; class RemoteException extends \Exception { public function __construct($message) { parent::__construct($message); $this->message = "Remote Application Error:\n" . $this->message; } } codeception/src/Codeception/Exception/ElementNotFound.php000077700000000677151323602320017616 0ustar00<?php namespace Codeception\Exception; use Codeception\Util\Locator; class ElementNotFound extends \PHPUnit\Framework\AssertionFailedError { public function __construct($selector, $message = null) { if (!is_string($selector) || strpos($selector, "'") === false) { $selector = Locator::humanReadableString($selector); } parent::__construct($message . " element with $selector was not found."); } } codeception/src/Codeception/Exception/.htaccess000077700000000177151323602320015630 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Exception/ModuleRequireException.php000077700000000705151323602320021201 0ustar00<?php namespace Codeception\Exception; class ModuleRequireException extends \Exception { public function __construct($module, $message) { if (is_object($module)) { $module = get_class($module); } $module = str_replace('Codeception\\Module\\', '', ltrim($module, '\\')); parent::__construct($message); $this->message = "[$module] module requirements not met --\n \n" . $this->message; } } codeception/src/Codeception/GroupObject.php000077700000001235151323602320015024 0ustar00<?php namespace Codeception; use Codeception\Event\TestEvent; abstract class GroupObject extends Extension { public static $group; public function _before(TestEvent $e) { } public function _after(TestEvent $e) { } public static function getSubscribedEvents() { $inheritedEvents = parent::getSubscribedEvents(); $events = []; if (static::$group) { $events = [ Events::TEST_BEFORE . '.' . static::$group => '_before', Events::TEST_AFTER . '.' . static::$group => '_after', ]; } return array_merge($events, $inheritedEvents); } } codeception/src/Codeception/Event/PrintResultEvent.php000077700000001331151323602320017154 0ustar00<?php namespace Codeception\Event; use Symfony\Component\EventDispatcher\Event; class PrintResultEvent extends Event { /** * @var \PHPUnit\Framework\TestResult */ protected $result; /** * @var \PHPUnit\Util\Printer */ protected $printer; public function __construct(\PHPUnit\Framework\TestResult $result, \PHPUnit\Util\Printer $printer) { $this->result = $result; $this->printer = $printer; } /** * @return \PHPUnit\Util\Printer */ public function getPrinter() { return $this->printer; } /** * @return \PHPUnit\Framework\TestResult */ public function getResult() { return $this->result; } } codeception/src/Codeception/Event/DispatcherWrapper.php000077700000001714151323602320017313 0ustar00<?php namespace Codeception\Event; use Symfony\Component\EventDispatcher\Event; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as ContractsEventDispatcherInterface; trait DispatcherWrapper { /** * Compatibility wrapper for dispatcher change between Symfony 4 and 5 * @param EventDispatcher $dispatcher * @param string $eventType * @param Event $eventObject */ protected function dispatch(EventDispatcher $dispatcher, $eventType, Event $eventObject) { // The `EventDispatcherInterface` of `Symfony\Contracts` is only implemented in Symfony 4.3 or higher if ($dispatcher instanceof ContractsEventDispatcherInterface) { //Symfony 4.3 or higher $dispatcher->dispatch($eventObject, $eventType); } else { //Symfony 4.2 or lower $dispatcher->dispatch($eventType, $eventObject); } } } codeception/src/Codeception/Event/FailEvent.php000077700000001024151323602320015533 0ustar00<?php namespace Codeception\Event; class FailEvent extends TestEvent { /** * @var \Exception */ protected $fail; /** * @var int */ protected $count; public function __construct(\PHPUnit\Framework\Test $test, $time, $e, $count = 0) { parent::__construct($test, $time); $this->fail = $e; $this->count = $count; } public function getCount() { return $this->count; } public function getFail() { return $this->fail; } } codeception/src/Codeception/Event/SuiteEvent.php000077700000001675151323602320015765 0ustar00<?php namespace Codeception\Event; use Codeception\Suite; use Symfony\Component\EventDispatcher\Event; class SuiteEvent extends Event { /** * @var \PHPUnit\Framework\TestSuite */ protected $suite; /** * @var \PHPUnit\Framework\TestResult */ protected $result; /** * @var array */ protected $settings; public function __construct( \PHPUnit\Framework\TestSuite $suite, \PHPUnit\Framework\TestResult $result = null, $settings = [] ) { $this->suite = $suite; $this->result = $result; $this->settings = $settings; } /** * @return Suite */ public function getSuite() { return $this->suite; } /** * @return \PHPUnit\Framework\TestResult */ public function getResult() { return $this->result; } public function getSettings() { return $this->settings; } } codeception/src/Codeception/Event/TestEvent.php000077700000001176151323602320015607 0ustar00<?php namespace Codeception\Event; use Symfony\Component\EventDispatcher\Event; class TestEvent extends Event { /** * @var \PHPUnit\Framework\Test */ protected $test; /** * @var float Time taken */ protected $time; public function __construct(\PHPUnit\Framework\Test $test, $time = 0) { $this->test = $test; $this->time = $time; } /** * @return float */ public function getTime() { return $this->time; } /** * @return \Codeception\TestInterface */ public function getTest() { return $this->test; } } codeception/src/Codeception/Event/.htaccess000077700000000177151323602320014753 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Event/StepEvent.php000077700000001142151323602320015574 0ustar00<?php namespace Codeception\Event; use Codeception\Step; use Codeception\TestInterface; use Symfony\Component\EventDispatcher\Event; class StepEvent extends Event { /** * @var Step */ protected $step; /** * @var TestInterface */ protected $test; public function __construct(TestInterface $test, Step $step) { $this->test = $test; $this->step = $step; } public function getStep() { return $this->step; } /** * @return TestInterface */ public function getTest() { return $this->test; } } codeception/src/Codeception/Extension.php000077700000006311151323602320014555 0ustar00<?php namespace Codeception; use Codeception\Configuration as Config; use Codeception\Event\SuiteEvent; use Codeception\Exception\ModuleRequireException; use Codeception\Lib\Console\Output; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** * A base class for all Codeception Extensions and GroupObjects * * Available Properties: * * * config: current extension configuration * * options: passed running options * */ abstract class Extension implements EventSubscriberInterface { protected $config = []; protected $options; protected $output; protected $globalConfig; private $modules = []; public function __construct($config, $options) { $this->config = array_merge($this->config, $config); $this->options = $options; $this->output = new Output($options); $this->_initialize(); } public static function getSubscribedEvents() { if (!isset(static::$events)) { return [Events::SUITE_INIT => 'receiveModuleContainer']; } if (isset(static::$events[Events::SUITE_INIT])) { if (!is_array(static::$events[Events::SUITE_INIT])) { static::$events[Events::SUITE_INIT] = [[static::$events[Events::SUITE_INIT]]]; } static::$events[Events::SUITE_INIT][] = ['receiveModuleContainer']; } else { static::$events[Events::SUITE_INIT] = 'receiveModuleContainer'; } return static::$events; } public function receiveModuleContainer(SuiteEvent $e) { $this->modules = $e->getSuite()->getModules(); } /** * Pass config variables that should be injected into global config. * * @param array $config */ public function _reconfigure($config = []) { if (is_array($config)) { Config::append($config); } } /** * You can do all preparations here. No need to override constructor. * Also you can skip calling `_reconfigure` if you don't need to. */ public function _initialize() { $this->_reconfigure(); // hook for BC only. } protected function write($message) { if (!$this->options['silent']) { $this->output->write($message); } } protected function writeln($message) { if (!$this->options['silent']) { $this->output->writeln($message); } } public function hasModule($name) { return isset($this->modules[$name]); } public function getCurrentModuleNames() { return array_keys($this->modules); } public function getModule($name) { if (!$this->hasModule($name)) { throw new ModuleRequireException($name, "module is not enabled"); } return $this->modules[$name]; } public function getTestsDir() { return Config::testsDir(); } public function getLogDir() { return Config::outputDir(); } public function getDataDir() { return Config::dataDir(); } public function getRootDir() { return Config::projectDir(); } public function getGlobalConfig() { return Config::config(); } } codeception/src/Codeception/Events.php000077700000007103151323602320014045 0ustar00<?php namespace Codeception; /** * Contains all events dispatched by Codeception. * * @author tiger-seo <tiger.seo@gmail.com> */ final class Events { /** * Private constructor. This class cannot be instantiated. */ private function __construct() { } /** * The <b>MODULE_INIT</b> event occurs before modules are initialized. * * The event listener method receives a {@link Codeception\Event\SuiteEvent} instance. */ const MODULE_INIT = 'module.init'; /** * The <b>SUITE_INIT</b> event occurs when suite is initialized. * Modules are created and initialized, but Actor class is not loaded. * * The event listener method receives a {@link Codeception\Event\SuiteEvent} instance. */ const SUITE_INIT = 'suite.init'; /** * The <b>SUITE_BEFORE</b> event occurs before suite is executed. * * The event listener method receives a {@link Codeception\Event\SuiteEvent} instance. */ const SUITE_BEFORE = 'suite.before'; /** * The <b>SUITE_AFTER</b> event occurs after suite has been executed. * * The event listener method receives a {@link Codeception\Event\SuiteEvent} instance. */ const SUITE_AFTER = 'suite.after'; /** * The event listener method receives a {@link Codeception\Event\TestEvent} instance. */ const TEST_START = 'test.start'; /** * The event listener method receives a {@link Codeception\Event\TestEvent} instance. */ const TEST_BEFORE = 'test.before'; /** * The event listener method receives a {@link Codeception\Event\StepEvent} instance. */ const STEP_BEFORE = 'step.before'; /** * The event listener method receives a {@link Codeception\Event\StepEvent} instance. */ const STEP_AFTER = 'step.after'; /** * The <b>TEST_FAIL</b> event occurs whenever test has failed. * * The event listener method receives a {@link Codeception\Event\FailEvent} instance. */ const TEST_FAIL = 'test.fail'; /** * The <b>TEST_ERROR</b> event occurs whenever test got an error while being executed. * * The event listener method receives a {@link Codeception\Event\FailEvent} instance. */ const TEST_ERROR = 'test.error'; /** * The event listener method receives a {@link Codeception\Event\TestEvent} instance. */ const TEST_PARSED = 'test.parsed'; /** * The event listener method receives a {@link Codeception\Event\FailEvent} instance. */ const TEST_INCOMPLETE = 'test.incomplete'; /** * The event listener method receives a {@link Codeception\Event\FailEvent} instance. */ const TEST_SKIPPED = 'test.skipped'; /** * The event listener method receives a {@link Codeception\Event\FailEvent} instance. */ const TEST_WARNING = 'test.warning'; /** * The event listener method receives a {@link Codeception\Event\TestEvent} instance. */ const TEST_SUCCESS = 'test.success'; /** * The event listener method receives a {@link Codeception\Event\TestEvent} instance. */ const TEST_AFTER = 'test.after'; /** * The event listener method receives a {@link Codeception\Event\TestEvent} instance. */ const TEST_END = 'test.end'; /** * The event listener method receives a {@link Codeception\Event\FailEvent} instance. */ const TEST_FAIL_PRINT = 'test.fail.print'; /** * The event listener method receives a {@link Codeception\Event\PrintResultEvent} instance. */ const RESULT_PRINT_AFTER = 'result.print.after'; } codeception/src/Codeception/.htaccess000077700000000177151323602320013672 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Util/Xml.php000077700000002572151323602320014263 0ustar00<?php namespace Codeception\Util; class Xml { /** * @static * * @param \DOMDocument $xml * @param \DOMNode $node * @param array $array * * @return \DOMDocument */ public static function arrayToXml(\DOMDocument $xml, \DOMNode $node, $array = []) { foreach ($array as $el => $val) { if (is_array($val)) { self::arrayToXml($xml, $node->$el, $val); } else { $node->appendChild($xml->createElement($el, $val)); } } return $xml; } /** * @static * * @param $xml * * @return \DOMDocument|\DOMNode */ public static function toXml($xml) { if ($xml instanceof XmlBuilder) { return $xml->getDom(); } if ($xml instanceof \DOMDocument) { return $xml; } $dom = new \DOMDocument(); $dom->preserveWhiteSpace = false; if ($xml instanceof \DOMNode) { $xml = $dom->importNode($xml, true); $dom->appendChild($xml); return $dom; } if (is_array($xml)) { return self::arrayToXml($dom, $dom, $xml); } if (!empty($xml)) { $dom->loadXML($xml); } return $dom; } public static function build() { return new XmlBuilder(); } } codeception/src/Codeception/Util/Locator.php000077700000024063151323602320015125 0ustar00<?php namespace Codeception\Util; use Facebook\WebDriver\WebDriverBy; use Symfony\Component\CssSelector\CssSelectorConverter; use Symfony\Component\CssSelector\Exception\ParseException; use Symfony\Component\CssSelector\XPath\Translator; /** * Set of useful functions for using CSS and XPath locators. * Please check them before writing complex functional or acceptance tests. * */ class Locator { /** * Applies OR operator to any number of CSS or XPath selectors. * You can mix up CSS and XPath selectors here. * * ```php * <?php * use \Codeception\Util\Locator; * * $I->see('Title', Locator::combine('h1','h2','h3')); * ?> * ``` * * This will search for `Title` text in either `h1`, `h2`, or `h3` tag. * You can also combine CSS selector with XPath locator: * * ```php * <?php * use \Codeception\Util\Locator; * * $I->fillField(Locator::combine('form input[type=text]','//form/textarea[2]'), 'qwerty'); * ?> * ``` * * As a result the Locator will produce a mixed XPath value that will be used in fillField action. * * @static * * @param $selector1 * @param $selector2 * * @throws \Exception * * @return string */ public static function combine($selector1, $selector2) { $selectors = func_get_args(); foreach ($selectors as $k => $v) { $selectors[$k] = self::toXPath($v); if (!$selectors[$k]) { throw new \Exception("$v is invalid CSS or XPath"); } } return implode(' | ', $selectors); } /** * Matches the *a* element with given URL * * ```php * <?php * use \Codeception\Util\Locator; * * $I->see('Log In', Locator::href('/login.php')); * ?> * ``` * * @static * * @param $url * * @return string */ public static function href($url) { return sprintf('//a[@href=normalize-space(%s)]', Translator::getXpathLiteral($url)); } /** * Matches the element with given tab index * * Do you often use the `TAB` key to navigate through the web page? How do your site respond to this navigation? * You could try to match elements by their tab position using `tabIndex` method of `Locator` class. * ```php * <?php * use \Codeception\Util\Locator; * * $I->fillField(Locator::tabIndex(1), 'davert'); * $I->fillField(Locator::tabIndex(2) , 'qwerty'); * $I->click('Login'); * ?> * ``` * * @static * * @param $index * * @return string */ public static function tabIndex($index) { return sprintf('//*[@tabindex = normalize-space(%d)]', $index); } /** * Matches option by text: * * ```php * <?php * use Codeception\Util\Locator; * * $I->seeElement(Locator::option('Male'), '#select-gender'); * ``` * * @param $value * * @return string */ public static function option($value) { return sprintf('//option[.=normalize-space("%s")]', $value); } protected static function toXPath($selector) { try { $xpath = (new CssSelectorConverter())->toXPath($selector); return $xpath; } catch (ParseException $e) { if (self::isXPath($selector)) { return $selector; } } return null; } /** * Finds element by it's attribute(s) * * ```php * <?php * use \Codeception\Util\Locator; * * $I->seeElement(Locator::find('img', ['title' => 'diagram'])); * ``` * * @static * * @param $element * @param $attributes * * @return string */ public static function find($element, array $attributes) { $operands = []; foreach ($attributes as $attribute => $value) { if (is_int($attribute)) { $operands[] = '@' . $value; } else { $operands[] = '@' . $attribute . ' = ' . Translator::getXpathLiteral($value); } } return sprintf('//%s[%s]', $element, implode(' and ', $operands)); } /** * Checks that provided string is CSS selector * * ```php * <?php * Locator::isCSS('#user .hello') => true * Locator::isCSS('body') => true * Locator::isCSS('//body/p/user') => false * ``` * * @param $selector * * @return bool */ public static function isCSS($selector) { try { (new CssSelectorConverter())->toXPath($selector); } catch (ParseException $e) { return false; } return true; } /** * Checks that locator is an XPath * * ```php * <?php * Locator::isXPath('#user .hello') => false * Locator::isXPath('body') => false * Locator::isXPath('//body/p/user') => true * ``` * * @param $locator * * @return bool */ public static function isXPath($locator) { $document = new \DOMDocument('1.0', 'UTF-8'); $xpath = new \DOMXPath($document); return @$xpath->evaluate($locator, $document) !== false; } /** * @param $locator * @return bool */ public static function isPrecise($locator) { if (is_array($locator)) { return true; } if ($locator instanceof WebDriverBy) { return true; } if (Locator::isID($locator)) { return true; } if (strpos($locator, '//') === 0) { return true; // simple xpath check } return false; } /** * Checks that a string is valid CSS ID * * ```php * <?php * Locator::isID('#user') => true * Locator::isID('body') => false * Locator::isID('//body/p/user') => false * ``` * * @param $id * * @return bool */ public static function isID($id) { return (bool)preg_match('~^#[\w\.\-\[\]\=\^\~\:]+$~', $id); } /** * Checks that a string is valid CSS class * * ```php * <?php * Locator::isClass('.hello') => true * Locator::isClass('body') => false * Locator::isClass('//body/p/user') => false * ``` * * @param $class * @return bool */ public static function isClass($class) { return (bool)preg_match('~^\.[\w\.\-\[\]\=\^\~\:]+$~', $class); } /** * Locates an element containing a text inside. * Either CSS or XPath locator can be passed, however they will be converted to XPath. * * ```php * <?php * use Codeception\Util\Locator; * * Locator::contains('label', 'Name'); // label containing name * Locator::contains('div[@contenteditable=true]', 'hello world'); * ``` * * @param $element * @param $text * * @return string */ public static function contains($element, $text) { $text = Translator::getXpathLiteral($text); return sprintf('%s[%s]', self::toXPath($element), "contains(., $text)"); } /** * Locates element at position. * Either CSS or XPath locator can be passed as locator, * position is an integer. If a negative value is provided, counting starts from the last element. * First element has index 1 * * ```php * <?php * use Codeception\Util\Locator; * * Locator::elementAt('//table/tr', 2); // second row * Locator::elementAt('//table/tr', -1); // last row * Locator::elementAt('table#grind>tr', -2); // previous than last row * ``` * * @param string $element CSS or XPath locator * @param int $position xpath index * * @return mixed */ public static function elementAt($element, $position) { if (is_int($position) && $position < 0) { $position++; // -1 points to the last element $position = 'last()-'.abs($position); } if ($position === 0) { throw new \InvalidArgumentException( '0 is not valid element position. XPath expects first element to have index 1' ); } return sprintf('(%s)[position()=%s]', self::toXPath($element), $position); } /** * Locates first element of group elements. * Either CSS or XPath locator can be passed as locator, * Equal to `Locator::elementAt($locator, 1)` * * ```php * <?php * use Codeception\Util\Locator; * * Locator::firstElement('//table/tr'); * ``` * * @param $element * * @return mixed */ public static function firstElement($element) { return self::elementAt($element, 1); } /** * Locates last element of group elements. * Either CSS or XPath locator can be passed as locator, * Equal to `Locator::elementAt($locator, -1)` * * ```php * <?php * use Codeception\Util\Locator; * * Locator::lastElement('//table/tr'); * ``` * * @param $element * * @return mixed */ public static function lastElement($element) { return self::elementAt($element, 'last()'); } /** * Transforms strict locator, \Facebook\WebDriver\WebDriverBy into a string represenation * * @param $selector * * @return string */ public static function humanReadableString($selector) { if (is_string($selector)) { return "'$selector'"; } if (is_array($selector)) { $type = strtolower(key($selector)); $locator = $selector[$type]; return "$type '$locator'"; } if (class_exists('\Facebook\WebDriver\WebDriverBy')) { if ($selector instanceof WebDriverBy) { $type = $selector->getMechanism(); $locator = $selector->getValue(); return "$type '$locator'"; } } throw new \InvalidArgumentException("Unrecognized selector"); } } codeception/src/Codeception/Util/Autoload.php000077700000011206151323602320015265 0ustar00<?php namespace Codeception\Util; /** * Autoloader, which is fully compatible with PSR-4, * and can be used to autoload your `Helper`, `Page`, and `Step` classes. */ class Autoload { protected static $registered = false; /** * An associative array where the key is a namespace prefix and the value * is an array of base directories for classes in that namespace. * @var array */ protected static $map = []; private function __construct() { } /** * Adds a base directory for a namespace prefix. * * Example: * * ```php * <?php * // app\Codeception\UserHelper will be loaded from '/path/to/helpers/UserHelper.php' * Autoload::addNamespace('app\Codeception', '/path/to/helpers'); * * // LoginPage will be loaded from '/path/to/pageobjects/LoginPage.php' * Autoload::addNamespace('', '/path/to/pageobjects'); * * Autoload::addNamespace('app\Codeception', '/path/to/controllers'); * ?> * ``` * * @param string $prefix The namespace prefix. * @param string $base_dir A base directory for class files in the namespace. * @param bool $prepend If true, prepend the base directory to the stack instead of appending it; * this causes it to be searched first rather than last. * @return void */ public static function addNamespace($prefix, $base_dir, $prepend = false) { if (!self::$registered) { spl_autoload_register([__CLASS__, 'load']); self::$registered = true; } // normalize namespace prefix $prefix = trim($prefix, '\\') . '\\'; // normalize the base directory with a trailing separator $base_dir = rtrim($base_dir, '/') . DIRECTORY_SEPARATOR; $base_dir = rtrim($base_dir, DIRECTORY_SEPARATOR) . '/'; // initialize the namespace prefix array if (isset(self::$map[$prefix]) === false) { self::$map[$prefix] = []; } // retain the base directory for the namespace prefix if ($prepend) { array_unshift(self::$map[$prefix], $base_dir); } else { self::$map[$prefix][] = $base_dir; } } public static function load($class) { // the current namespace prefix $prefix = $class; // work backwards through the namespace names of the fully-qualified class name to find a mapped file name while (false !== ($pos = strrpos($prefix, '\\'))) { // retain the trailing namespace separator in the prefix $prefix = substr($class, 0, $pos + 1); // the rest is the relative class name $relative_class = substr($class, $pos + 1); // try to load a mapped file for the prefix and relative class $mapped_file = self::loadMappedFile($prefix, $relative_class); if ($mapped_file) { return $mapped_file; } // remove the trailing namespace separator for the next iteration of strrpos() $prefix = rtrim($prefix, '\\'); } // fix for empty prefix if (isset(self::$map['\\']) && ($class[0] != '\\')) { return self::load('\\' . $class); } // backwards compatibility with old autoloader // :TODO: it should be removed if (strpos($class, '\\') !== false) { $relative_class = substr(strrchr($class, '\\'), 1); // Foo\Bar\ClassName -> ClassName $mapped_file = self::loadMappedFile('\\', $relative_class); if ($mapped_file) { return $mapped_file; } } return false; } /** * Load the mapped file for a namespace prefix and relative class. * * @param string $prefix The namespace prefix. * @param string $relative_class The relative class name. * @return mixed Boolean false if no mapped file can be loaded, or the name of the mapped file that was loaded. */ protected static function loadMappedFile($prefix, $relative_class) { if (!isset(self::$map[$prefix])) { return false; } foreach (self::$map[$prefix] as $base_dir) { $file = $base_dir . str_replace('\\', '/', $relative_class) . '.php'; // 'static' is for testing purposes if (static::requireFile($file)) { return $file; } } return false; } protected static function requireFile($file) { if (file_exists($file)) { require_once $file; return true; } return false; } } codeception/src/Codeception/Util/Debug.php000077700000002326151323602320014546 0ustar00<?php namespace Codeception\Util; use Codeception\Lib\Console\Output; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\Question; /** * This class is used only when Codeception is executed in `--debug` mode. * In other cases method of this class won't be seen. */ class Debug { /** * @var Output null */ protected static $output = null; public static function setOutput(Output $output) { self::$output = $output; } /** * Prints data to screen. Message can be any time of data * * @param $message */ public static function debug($message) { if (!self::$output) { return; } self::$output->debug($message); } public static function isEnabled() { return (bool) self::$output; } public static function confirm($question) { if (!self::$output) { return; } $questionHelper = new QuestionHelper(); return $questionHelper->ask(new ArgvInput(), self::$output, new ConfirmationQuestion($question)); } } codeception/src/Codeception/Util/Fixtures.php000077700000001645151323602320015334 0ustar00<?php namespace Codeception\Util; /** * Really basic class to store data in global array and use it in Cests/Tests. * * ```php * <?php * Fixtures::add('user1', ['name' => 'davert']); * Fixtures::get('user1'); * Fixtures::exists('user1'); * * ?> * ``` * */ class Fixtures { protected static $fixtures = []; public static function add($name, $data) { self::$fixtures[$name] = $data; } public static function get($name) { if (!self::exists($name)) { throw new \RuntimeException("$name not found in fixtures"); } return self::$fixtures[$name]; } public static function cleanup($name = null) { if (self::exists($name)) { unset(self::$fixtures[$name]); return; } self::$fixtures = []; } public static function exists($name) { return isset(self::$fixtures[$name]); } } codeception/src/Codeception/Util/README.md000077700000000377151323602320014272 0ustar00# Util Classes Codeception Util classes are used by various parts of framework and can be used in tests or in other projects. They do not depend on Codeception core, nor they do know of Codeception itself. Thus, some of those classes have static methods.codeception/src/Codeception/Util/FileSystem.php000077700000004473151323602320015611 0ustar00<?php namespace Codeception\Util; /** * Set of functions to work with Filesystem * */ class FileSystem { /** * @param $path */ public static function doEmptyDir($path) { /** @var $iterator \RecursiveIteratorIterator|\SplFileObject[] */ $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($path), \RecursiveIteratorIterator::CHILD_FIRST ); foreach ($iterator as $path) { $basename = basename((string)$path); if ($basename === '.' || $basename === '..' || $basename === '.gitignore') { continue; } if ($path->isDir()) { rmdir((string)$path); } else { unlink((string)$path); } } } /** * @param $dir * @return bool */ public static function deleteDir($dir) { if (!file_exists($dir)) { return true; } if (!is_dir($dir) || is_link($dir)) { return @unlink($dir); } if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { $dir = str_replace('/', '\\', $dir); exec('rd /s /q "'.$dir.'"'); return true; } foreach (scandir($dir) as $item) { if ($item === '.' || $item === '..') { continue; } if (!self::deleteDir($dir . DIRECTORY_SEPARATOR . $item)) { chmod($dir . DIRECTORY_SEPARATOR . $item, 0777); if (!self::deleteDir($dir . DIRECTORY_SEPARATOR . $item)) { return false; } } } return @rmdir($dir); } /** * @param $src * @param $dst */ public static function copyDir($src, $dst) { $dir = opendir($src); @mkdir($dst); while (false !== ($file = readdir($dir))) { if (($file != '.') && ($file != '..')) { if (is_dir($src . DIRECTORY_SEPARATOR . $file)) { self::copyDir($src . DIRECTORY_SEPARATOR . $file, $dst . DIRECTORY_SEPARATOR . $file); } else { copy($src . DIRECTORY_SEPARATOR . $file, $dst . DIRECTORY_SEPARATOR . $file); } } } closedir($dir); } } codeception/src/Codeception/Util/XmlBuilder.php000077700000007505151323602320015573 0ustar00<?php namespace Codeception\Util; /** * That's a pretty simple yet powerful class to build XML structures in jQuery-like style. * With no XML line actually written! * Uses DOM extension to manipulate XML data. * * * ```php * <?php * $xml = new \Codeception\Util\XmlBuilder(); * $xml->users * ->user * ->val(1) * ->email * ->val('davert@mail.ua') * ->attr('valid','true') * ->parent() * ->cart * ->attr('empty','false') * ->items * ->item * ->val('useful item'); * ->parents('user') * ->active * ->val(1); * echo $xml; * ``` * * This will produce this XML * * ```xml * <?xml version="1.0"?> * <users> * <user> * 1 * <email valid="true">davert@mail.ua</email> * <cart empty="false"> * <items> * <item>useful item</item> * </items> * </cart> * <active>1</active> * </user> * </users> * ``` * * ### Usage * * Builder uses chained calls. So each call to builder returns a builder object. * Except for `getDom` and `__toString` methods. * * * `$xml->node` - create new xml node and go inside of it. * * `$xml->node->val('value')` - sets the inner value of node * * `$xml->attr('name','value')` - set the attribute of node * * `$xml->parent()` - go back to parent node. * * `$xml->parents('user')` - go back through all parents to `user` node. * * Export: * * * `$xml->getDom` - get a DOMDocument object * * `$xml->__toString` - get a string representation of XML. * * [Source code](https://github.com/Codeception/Codeception/blob/4.0/src/Codeception/Util/XmlBuilder.php) */ class XmlBuilder { /** * @var \DOMDocument */ protected $__dom__; /** * @var \DOMElement */ protected $__currentNode__; public function __construct() { $this->__dom__ = new \DOMDocument(); $this->__currentNode__ = $this->__dom__; } /** * Appends child node * * @param $tag * * @return XmlBuilder */ public function __get($tag) { $node = $this->__dom__->createElement($tag); $this->__currentNode__->appendChild($node); $this->__currentNode__ = $node; return $this; } /** * @param $val * * @return XmlBuilder */ public function val($val) { $this->__currentNode__->nodeValue = $val; return $this; } /** * Sets attribute for current node * * @param $attr * @param $val * * @return XmlBuilder */ public function attr($attr, $val) { $this->__currentNode__->setAttribute($attr, $val); return $this; } /** * Traverses to parent * * @return XmlBuilder */ public function parent() { $this->__currentNode__ = $this->__currentNode__->parentNode; return $this; } /** * Traverses to parent with $name * * @param $tag * * @return XmlBuilder * @throws \Exception */ public function parents($tag) { $traverseNode = $this->__currentNode__; $elFound = false; while ($traverseNode->parentNode) { $traverseNode = $traverseNode->parentNode; if ($traverseNode->tagName == $tag) { $this->__currentNode__ = $traverseNode; $elFound = true; break; } } if (!$elFound) { throw new \Exception("Parent $tag not found in XML"); } return $this; } public function __toString() { return $this->__dom__->saveXML(); } /** * @return \DOMDocument */ public function getDom() { return $this->__dom__; } } codeception/src/Codeception/Util/ReflectionPropertyAccessor.php000077700000006760151323602320021050 0ustar00<?php namespace Codeception\Util; use InvalidArgumentException; use ReflectionClass; use ReflectionException; use function get_class; use function get_parent_class; use function gettype; use function is_object; class ReflectionPropertyAccessor { /** * @param object $obj * @param string $field * @return mixed * @throws ReflectionException */ public function getProperty($obj, $field) { if (!$obj || !is_object($obj)) { throw new InvalidArgumentException('Cannot get property "' . $field . '" of "' . gettype($obj) . '", expecting object'); } $class = get_class($obj); do { $reflectedEntity = new ReflectionClass($class); if ($reflectedEntity->hasProperty($field)) { $property = $reflectedEntity->getProperty($field); $property->setAccessible(true); return $property->getValue($obj); } $class = get_parent_class($class); } while ($class); throw new InvalidArgumentException('Property "' . $field . '" does not exists in class "' . get_class($obj) . '" and its parents'); } /** * @param object|null $obj * @param string $class * @param array $data * @return object|null * @throws ReflectionException */ private function setPropertiesForClass($obj, $class, array $data) { $reflectedEntity = new ReflectionClass($class); if (!$obj) { $constructorParameters = []; $constructor = $reflectedEntity->getConstructor(); if (null !== $constructor) { foreach ($constructor->getParameters() as $parameter) { if ($parameter->isOptional()) { $constructorParameters[] = $parameter->getDefaultValue(); } elseif (array_key_exists($parameter->getName(), $data)) { $constructorParameters[] = $data[$parameter->getName()]; } else { throw new InvalidArgumentException( 'Constructor parameter "'.$parameter->getName().'" missing' ); } } } $obj = $reflectedEntity->newInstance(...$constructorParameters); } foreach ($reflectedEntity->getProperties() as $property) { if (isset($data[$property->name])) { $property->setAccessible(true); $property->setValue($obj, $data[$property->name]); } } return $obj; } /** * @param object|null $obj * @param array $data * @throws ReflectionException */ public function setProperties($obj, array $data) { if (!$obj || !is_object($obj)) { throw new InvalidArgumentException('Cannot set properties for "' . gettype($obj) . '", expecting object'); } $class = get_class($obj); do { $obj = $this->setPropertiesForClass($obj, $class, $data); $class = get_parent_class($class); } while ($class); } /** * @param string $class * @param array $data * @return object * @throws ReflectionException */ public function createWithProperties($class, array $data) { $obj = null; do { $obj = $this->setPropertiesForClass($obj, $class, $data); $class = get_parent_class($class); } while ($class); return $obj; } } codeception/src/Codeception/Util/XmlStructure.php000077700000004532151323602320016202 0ustar00<?php namespace Codeception\Util; use Codeception\Exception\ElementNotFound; use Codeception\Exception\MalformedLocatorException; use Symfony\Component\CssSelector\CssSelectorConverter; use Symfony\Component\CssSelector\Exception\ParseException; use Codeception\Util\Soap as XmlUtils; class XmlStructure { /** * @var \DOMDocument|\DOMNode */ protected $xml; public function __construct($xml) { $this->xml = XmlUtils::toXml($xml); } public function matchesXpath($xpath) { $path = new \DOMXPath($this->xml); $res = $path->query($xpath); if ($res === false) { throw new MalformedLocatorException($xpath); } return $res->length > 0; } /** * @param $cssOrXPath * @return \DOMElement */ public function matchElement($cssOrXPath) { $xpath = new \DOMXpath($this->xml); try { $selector = (new CssSelectorConverter())->toXPath($cssOrXPath); $els = $xpath->query($selector); if ($els) { return $els->item(0); } } catch (ParseException $e) { } $els = $xpath->query($cssOrXPath); if ($els->length) { return $els->item(0); } throw new ElementNotFound($cssOrXPath); } /** * @param $xml * @return bool */ public function matchXmlStructure($xml) { $xml = XmlUtils::toXml($xml); $root = $xml->firstChild; $els = $this->xml->getElementsByTagName($root->nodeName); if (empty($els)) { throw new ElementNotFound($root->nodeName, 'Element'); } $matches = false; foreach ($els as $node) { $matches |= $this->matchForNode($root, $node); } return $matches; } protected function matchForNode($schema, $xml) { foreach ($schema->childNodes as $node1) { $matched = false; foreach ($xml->childNodes as $node2) { if ($node1->nodeName == $node2->nodeName) { $matched = $this->matchForNode($node1, $node2); if ($matched) { break; } } } if (!$matched) { return false; } } return true; } } codeception/src/Codeception/Util/Stub.php000077700000002006151323602320014430 0ustar00<?php namespace Codeception\Util; use Codeception\Lib\Notification; use Codeception\Stub\Expected; class Stub extends \Codeception\Stub { public static function never($params = null) { Notification::deprecate("Stub::never is deprecated in favor of \Codeception\Stub\Expected::never"); return Expected::never($params); } public static function once($params = null) { Notification::deprecate("Stub::once is deprecated in favor of \Codeception\Stub\Expected::once"); return Expected::once($params); } public static function atLeastOnce($params = null) { Notification::deprecate("Stub::atLeastOnce is deprecated in favor of \Codeception\Stub\Expected::atLeastOnce"); return Expected::atLeastOnce($params); } public static function exactly($count, $params = null) { Notification::deprecate("Stub::exactly is deprecated in favor of \Codeception\Stub\Expected::exactly"); return Expected::exactly($count, $params); } } codeception/src/Codeception/Util/ReflectionHelper.php000077700000013252151323602320016752 0ustar00<?php namespace Codeception\Util; use ReflectionException; use ReflectionParameter; use ReflectionProperty; use ReflectionMethod; /** * This class contains helper methods to help with common Reflection tasks. */ class ReflectionHelper { /** * Read a private property of an object. * * @param object $object * @param string $property * @param string|null $class * @return mixed * @throws ReflectionException */ public static function readPrivateProperty($object, $property, $class = null) { if (is_null($class)) { $class = $object; } $property = new ReflectionProperty($class, $property); $property->setAccessible(true); return $property->getValue($object); } /** * Invoke a private method of an object. * * @param object $object * @param string $method * @param array $args * @param string|null $class * @return mixed * @throws ReflectionException */ public static function invokePrivateMethod($object, $method, $args = [], $class = null) { if (is_null($class)) { $class = $object; } $method = new ReflectionMethod($class, $method); $method->setAccessible(true); return $method->invokeArgs($object, $args); } /** * Returns class name without namespace * * (does not use reflection actually) * * @param $object * @return mixed */ public static function getClassShortName($object) { $path = explode('\\', get_class($object)); return array_pop($path); } /** * Adapted from https://github.com/Behat/Behat/pull/1313 * * @param ReflectionParameter $parameter * @return string|null */ public static function getClassFromParameter(ReflectionParameter $parameter) { if (PHP_VERSION_ID < 70100) { $class = $parameter->getClass(); if ($class !== null) { return $class->name; } return $class; } $type = $parameter->getType(); if ($type === null || $type->isBuiltin()) { return null; } $typeString = $type->getName(); if ($typeString === 'self') { return $parameter->getDeclaringClass()->getName(); } elseif ($typeString === 'parent') { return $parameter->getDeclaringClass()->getParentClass()->getName(); } return $typeString; } /** * Infer default parameter from the reflection object and format it as PHP (code) string * * @param ReflectionParameter $param * * @return string */ public static function getDefaultValue(ReflectionParameter $param) { if ($param->isDefaultValueAvailable()) { if (method_exists($param, 'isDefaultValueConstant') && $param->isDefaultValueConstant()) { $constName = $param->getDefaultValueConstantName(); if (false !== strpos($constName, '::')) { list($class, $const) = explode('::', $constName); if (in_array($class, ['self', 'static'])) { $constName = '\\' . $param->getDeclaringClass()->getName() . '::' . $const; } elseif (substr($class, 0, 1) !== '\\') { $constName = '\\' . $constName; } } return $constName; } return self::phpEncodeValue($param->getDefaultValue()); } // Default to 'null' for PHP versions < 7.1. if (PHP_VERSION_ID < 70100) { return 'null'; } $type = $param->getType(); // Default to 'null' if explicitly allowed or there is no specific type hint. if (!$type || $type->allowsNull() || !$type->isBuiltin()) { return 'null'; } // Default value should match the parameter type if 'null' is NOT allowed. switch ($type->getName()) { case 'string': return "''"; case 'array': return '[]'; case 'boolean': return 'false'; case 'int': case 'integer': case 'float': case 'double': case 'number': case 'numeric': return '0'; default: return 'null'; } } /** * PHP encode value * * @param mixed $value * * @return string */ public static function phpEncodeValue($value) { if (is_array($value)) { return self::phpEncodeArray($value); } if (is_string($value)) { return json_encode($value); } return var_export($value, true); } /** * Recursively PHP encode an array * * @param array $array * * @return string */ public static function phpEncodeArray(array $array) { $isPlainArray = function (array $value) { return ((count($value) === 0) || ( (array_keys($value) === range(0, count($value) - 1)) && (0 === count(array_filter(array_keys($value), 'is_string')))) ); }; if ($isPlainArray($array)) { return '[' . implode(', ', array_map([self::class, 'phpEncodeValue'], $array)) . ']'; } $values = array_map( function ($key) use ($array) { return self::phpEncodeValue($key) . ' => ' . self::phpEncodeValue($array[$key]); }, array_keys($array) ); return '[' . implode(', ', $values) . ']'; } } codeception/src/Codeception/Util/Template.php000077700000004065151323602320015275 0ustar00<?php namespace Codeception\Util; /** * Basic template engine used for generating initial Cept/Cest/Test files. */ class Template { protected $template; protected $vars = []; protected $placeholderStart; protected $placeholderEnd; /** * Takes a template string * * @param $template */ public function __construct($template, $placeholderStart = '{{', $placeholderEnd = '}}') { $this->template = $template; $this->placeholderStart = $placeholderStart; $this->placeholderEnd = $placeholderEnd; } /** * Replaces {{var}} string with provided value * * @param $var * @param $val * @return $this */ public function place($var, $val) { $this->vars[$var] = $val; return $this; } /** * Sets all template vars * * @param array $vars */ public function setVars(array $vars) { $this->vars = $vars; } public function getVar($name) { if (isset($this->vars[$name])) { return $this->vars[$name]; } } /** * Fills up template string with placed variables. * * @return mixed */ public function produce() { $result = $this->template; $regex = sprintf('~%s([\w\.]+)%s~m', $this->placeholderStart, $this->placeholderEnd); $matched = preg_match_all($regex, $result, $matches, PREG_SET_ORDER); if (!$matched) { return $result; } foreach ($matches as $match) { // fill in placeholders $placeholder = $match[1]; $value = $this->vars; foreach (explode('.', $placeholder) as $segment) { if (is_array($value) && array_key_exists($segment, $value)) { $value = $value[$segment]; } else { continue 2; } } $result = str_replace($this->placeholderStart . $placeholder . $this->placeholderEnd, $value, $result); } return $result; } } codeception/src/Codeception/Util/ArrayContainsComparator.php000077700000007264151323602320020333 0ustar00<?php namespace Codeception\Util; class ArrayContainsComparator { /** * @var array */ protected $haystack = []; public function __construct($haystack) { $this->haystack = $haystack; } /** * @return array */ public function getHaystack() { return $this->haystack; } public function containsArray(array $needle) { return $needle == $this->arrayIntersectRecursive($needle, $this->haystack); } /** * @author nleippe@integr8ted.com * @author tiger.seo@gmail.com * @link http://www.php.net/manual/en/function.array-intersect-assoc.php#39822 * * @param mixed $arr1 * @param mixed $arr2 * * @return array|bool */ private function arrayIntersectRecursive($arr1, $arr2) { if (!is_array($arr1) || !is_array($arr2)) { return false; } // if it is not an associative array we do not compare keys if ($this->arrayIsSequential($arr1) && $this->arrayIsSequential($arr2)) { return $this->sequentialArrayIntersect($arr1, $arr2); } return $this->associativeArrayIntersect($arr1, $arr2); } /** * This array has sequential keys? * * @param array $array * * @return bool */ private function arrayIsSequential(array $array) { return array_keys($array) === range(0, count($array) - 1); } /** * @param array $arr1 * @param array $arr2 * @return array */ private function sequentialArrayIntersect(array $arr1, array $arr2) { $ret = []; // Do not match the same item of $arr2 against multiple items of $arr1 $matchedKeys = []; foreach ($arr1 as $key1 => $value1) { foreach ($arr2 as $key2 => $value2) { if (isset($matchedKeys[$key2])) { continue; } $return = $this->arrayIntersectRecursive($value1, $value2); if ($return !== false && $return == $value1) { $ret[$key1] = $return; $matchedKeys[$key2] = true; break; } if ($this->isEqualValue($value1, $value2)) { $ret[$key1] = $value1; $matchedKeys[$key2] = true; break; } } } return $ret; } /** * @param array $arr1 * @param array $arr2 * * @return array|bool|null */ private function associativeArrayIntersect(array $arr1, array $arr2) { $commonKeys = array_intersect(array_keys($arr1), array_keys($arr2)); $ret = []; foreach ($commonKeys as $key) { $return = $this->arrayIntersectRecursive($arr1[$key], $arr2[$key]); if ($return !== false) { $ret[$key] = $return; continue; } if ($this->isEqualValue($arr1[$key], $arr2[$key])) { $ret[$key] = $arr1[$key]; } } if (empty($commonKeys)) { foreach ($arr2 as $arr) { $return = $this->arrayIntersectRecursive($arr1, $arr); if ($return && $return == $arr1) { return $return; } } } if (count($ret) < min(count($arr1), count($arr2))) { return null; } return $ret; } private function isEqualValue($val1, $val2) { if (is_numeric($val1)) { $val1 = (string) $val1; } if (is_numeric($val2)) { $val2 = (string) $val2; } return $val1 === $val2; } } codeception/src/Codeception/Util/Annotation.php000077700000010475151323602320015636 0ustar00<?php namespace Codeception\Util; /** * Simple annotation parser. Take only key-value annotations for methods or class. */ class Annotation { protected static $reflectedClasses = []; protected static $regex = '/@%s(?:[ \t]*(.*?))?[ \t]*(?:\*\/)?\r?$/m'; protected static $lastReflected = null; /** * @var \ReflectionClass */ protected $reflectedClass; protected $currentReflectedItem; /** * Grabs annotation values. * * Usage example: * * ``` php * <?php * Annotation::forClass('MyTestCase')->fetch('guy'); * Annotation::forClass('MyTestCase')->method('testData')->fetch('depends'); * Annotation::forClass('MyTestCase')->method('testData')->fetchAll('depends'); * * ?> * ``` * * @param $class * * @return $this */ public static function forClass($class) { if (is_object($class)) { $class = get_class($class); } if (!isset(static::$reflectedClasses[$class])) { static::$reflectedClasses[$class] = new \ReflectionClass($class); } return new static(static::$reflectedClasses[$class]); } /** * @param $class * @param $method * * @return $this */ public static function forMethod($class, $method) { return self::forClass($class)->method($method); } /** * Parses raw comment for annotations * * @param $docblock * @param $annotation * @return array */ public static function fetchAnnotationsFromDocblock($annotation, $docblock) { if (preg_match_all(sprintf(self::$regex, $annotation), $docblock, $matched)) { return $matched[1]; } return []; } /** * Fetches all available annotations * * @param $docblock * @return array */ public static function fetchAllAnnotationsFromDocblock($docblock) { $annotations = []; if (!preg_match_all(sprintf(self::$regex, '(\w+)'), $docblock, $matched)) { return $annotations; } foreach ($matched[1] as $k => $annotation) { if (!isset($annotations[$annotation])) { $annotations[$annotation] = []; } $annotations[$annotation][] = $matched[2][$k]; }; return $annotations; } public function __construct(\ReflectionClass $class) { $this->currentReflectedItem = $this->reflectedClass = $class; } /** * @param $method * * @return $this */ public function method($method) { $this->currentReflectedItem = $this->reflectedClass->getMethod($method); return $this; } /** * @param $annotation * @return null */ public function fetch($annotation) { $docBlock = $this->currentReflectedItem->getDocComment(); if (preg_match(sprintf(self::$regex, $annotation), $docBlock, $matched)) { return $matched[1]; } return null; } /** * @param $annotation * @return array */ public function fetchAll($annotation) { $docBlock = $this->currentReflectedItem->getDocComment(); if (preg_match_all(sprintf(self::$regex, $annotation), $docBlock, $matched)) { return $matched[1]; } return []; } public function raw() { return $this->currentReflectedItem->getDocComment(); } /** * Returns an associative array value of annotation * Either JSON or Doctrine-annotation style allowed * Returns null if not a valid array data * * @param $annotation * @return array|mixed|string */ public static function arrayValue($annotation) { $annotation = trim($annotation); $openingBrace = substr($annotation, 0, 1); // json-style data format if (in_array($openingBrace, ['{', '['])) { return json_decode($annotation, true); } // doctrine-style data format if ($openingBrace === '(') { preg_match_all('~(\w+)\s*?=\s*?"(.*?)"\s*?[,)]~', $annotation, $matches, PREG_SET_ORDER); $data = []; foreach ($matches as $item) { $data[$item[1]] = $item[2]; } return $data; } return null; } } codeception/src/Codeception/Util/Shared/Namespaces.php000077700000001757151323602320017014 0ustar00<?php namespace Codeception\Util\Shared; trait Namespaces { protected function breakParts($class) { // removing leading slashes and dots first $class = str_replace('/', '\\', ltrim($class, './\\')); return explode('\\', $class); } protected function getShortClassName($class) { $namespaces = $this->breakParts($class); return array_pop($namespaces); } protected function getNamespaceString($class) { $namespaces = $this->getNamespaces($class); return implode('\\', $namespaces); } protected function getNamespaceHeader($class) { $str = $this->getNamespaceString($class); if (!$str) { return ""; } return "namespace $str;\n"; } protected function getNamespaces($class) { $namespaces = $this->breakParts($class); array_pop($namespaces); $namespaces = array_filter($namespaces, 'strlen'); return $namespaces; } } codeception/src/Codeception/Util/Shared/.htaccess000077700000000177151323602320016015 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Util/.htaccess000077700000000177151323602320014607 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/src/Codeception/Util/Soap.php000077700000000556151323602320014425 0ustar00<?php namespace Codeception\Util; /** * This class is left for BC compatibility. * Most of its contents moved to parent * * Class Soap * @package Codeception\Util */ class Soap extends Xml { public static function request() { return new XmlBuilder(); } public static function response() { return new XmlBuilder(); } } codeception/src/Codeception/Util/PathResolver.php000077700000012603151323602320016135 0ustar00<?php namespace Codeception\Util; use Codeception\Exception\ConfigurationException; class PathResolver { /** * Returns path to a given directory relative to $projDir. * @param string $path * @param string $projDir * @param string $dirSep * @return string */ public static function getRelativeDir($path, $projDir, $dirSep = DIRECTORY_SEPARATOR) { // ensure $projDir ends with a trailing $dirSep $projDir = preg_replace('/'.preg_quote($dirSep, '/').'*$/', $dirSep, $projDir); // if $path is a within $projDir if (self::fsCaseStrCmp(substr($path, 0, strlen($projDir)), $projDir, $dirSep) == 0) { // simply chop it off the front return substr($path, strlen($projDir)); } // Identify any absoluteness prefix (like '/' in Unix or "C:\\" in Windows) $pathAbsPrefix = self::getPathAbsolutenessPrefix($path, $dirSep); $projDirAbsPrefix = self::getPathAbsolutenessPrefix($projDir, $dirSep); $sameAbsoluteness = (self::fsCaseStrCmp($pathAbsPrefix['wholePrefix'], $projDirAbsPrefix['wholePrefix'], $dirSep) == 0); if (!$sameAbsoluteness) { // if the $projDir and $path aren't relative to the same // thing, we can't make a relative path. // if we're relative to the same device ... if (strlen($pathAbsPrefix['devicePrefix']) && (self::fsCaseStrCmp($pathAbsPrefix['devicePrefix'], $projDirAbsPrefix['devicePrefix'], $dirSep) == 0) ) { // ... shave that off return substr($path, strlen($pathAbsPrefix['devicePrefix'])); } // Return the input unaltered return $path; } // peel off optional absoluteness prefixes and convert // $path and $projDir to an subdirectory path array $relPathParts = array_filter(explode($dirSep, substr($path, strlen($pathAbsPrefix['wholePrefix']))), 'strlen'); $relProjDirParts = array_filter(explode($dirSep, substr($projDir, strlen($projDirAbsPrefix['wholePrefix']))), 'strlen'); // While there are any, peel off any common parent directories // from the beginning of the $projDir and $path while ((count($relPathParts) > 0) && (count($relProjDirParts) > 0) && (self::fsCaseStrCmp($relPathParts[0], $relProjDirParts[0], $dirSep) == 0) ) { array_shift($relPathParts); array_shift($relProjDirParts); } if (count($relProjDirParts) > 0) { // prefix $relPath with '..' for all remaining unmatched $projDir // subdirectories $relPathParts = array_merge(array_fill(0, count($relProjDirParts), '..'), $relPathParts); } // only append a trailing seperator if one is already present $trailingSep = preg_match('/'.preg_quote($dirSep, '/').'$/', $path) ? $dirSep : ''; // convert array of dir paths back into a string path return implode($dirSep, $relPathParts).$trailingSep; } /** * FileSystem Case String Compare * compare two strings with the filesystem's case-sensitiveness * * @param string $str1 * @param string $str2 * @param string $dirSep * @return int -1 / 0 / 1 for < / = / > respectively */ private static function fsCaseStrCmp($str1, $str2, $dirSep = DIRECTORY_SEPARATOR) { $cmpFn = self::isWindows($dirSep) ? 'strcasecmp' : 'strcmp'; return $cmpFn($str1, $str2); } /** * What part of this path (leftmost 0-3 characters) what * it is absolute relative to: * * On Unix: * This is simply '/' for an absolute path or * '' for a relative path * * On Windows this is more complicated: * If the first two characters are a letter followed * by a ':', this indicates that the path is * on a specific device. * With or without a device specified, a path MAY * start with a '\\' to indicate an absolute path * on the device or '' to indicate a path relative * to the device's CWD * * @param string $path * @param string $dirSep * @return string */ private static function getPathAbsolutenessPrefix($path, $dirSep = DIRECTORY_SEPARATOR) { $devLetterPrefixPattern = ''; if (self::isWindows($dirSep)) { $devLetterPrefixPattern = '([A-Za-z]:|)'; } $matches = []; if (!preg_match('/^'.$devLetterPrefixPattern.preg_quote($dirSep, '/').'?/', $path, $matches)) { // This should match, even if it matches 0 characters throw new ConfigurationException("INTERNAL ERROR: This must be a regex problem."); } return [ 'wholePrefix' => $matches[0], // The optional device letter followed by the optional $dirSep 'devicePrefix' => self::isWindows($dirSep) ? $matches[1] : '']; } /** * Are we in a Windows style filesystem? * * @param string $dirSep * @return bool */ private static function isWindows($dirSep = DIRECTORY_SEPARATOR) { return ($dirSep == '\\'); } public static function isPathAbsolute($path) { if (DIRECTORY_SEPARATOR === '/') { return substr($path, 0, 1) === DIRECTORY_SEPARATOR; } return preg_match('#^[A-Z]:(?![^/\\\])#i', $path) === 1; } } codeception/src/Codeception/Util/ActionSequence.php000077700000006705151323602320016433 0ustar00<?php namespace Codeception\Util; use Codeception\Step\Action; /** * Class for defining an array actions to be executed inside `performOn` of WebDriver * * ```php * <?php * (new ActionSequence)->click('do')->click('undo'); * ActionSequence::build()->click('do')->click('undo'); * ``` * * @method $this see([optional]) * @method $this dontSee([optional]) * @method $this seeElement([optional]) * @method $this dontSeeElement([optional]) * @method $this click([optional]) * @method $this wait([optional]) * @method $this waitForElementChange([optional]) * @method $this waitForElement([optional]) * @method $this waitForElementVisible([optional]) * @method $this waitForElementNotVisible([optional]) * @method $this waitForText([optional]) * @method $this submitForm([optional]) * @method $this seeLink([optional]) * @method $this dontSeeLink([optional]) * @method $this seeCheckboxIsChecked([optional]) * @method $this dontSeeCheckboxIsChecked([optional]) * @method $this seeInField([optional]) * @method $this dontSeeInField([optional]) * @method $this seeInFormFields([optional]) * @method $this dontSeeInFormFields([optional]) * @method $this selectOption([optional]) * @method $this checkOption([optional]) * @method $this uncheckOption([optional]) * @method $this fillField([optional]) * @method $this attachFile([optional]) * @method $this seeNumberOfElements([optional]) * @method $this seeOptionIsSelected([optional]) * @method $this dontSeeOptionIsSelected([optional]) */ class ActionSequence { protected $actions = []; /** * Creates an instance * @return ActionSequence */ public static function build() { return new self; } public function __call($action, $arguments) { $this->addAction($action, $arguments); return $this; } protected function addAction($action, $arguments) { if (!is_array($arguments)) { $arguments = [$arguments]; } $this->actions[] = new Action($action, $arguments); } /** * Creates action sequence from associative array, * where key is action, and value is action arguments * * @param array $actions * @return $this */ public function fromArray(array $actions) { foreach ($actions as $action => $arguments) { $this->addAction($action, $arguments); } return $this; } /** * Returns a list of logged actions as associative array * @return array */ public function toArray() { return $this->actions; } /** * Executes sequence of action as methods of passed object. * * @param $context */ public function run($context) { foreach ($this->actions as $step) { /** @var $step Action **/ codecept_debug("- $step"); try { call_user_func_array([$context, $step->getAction()], $step->getArguments()); } catch (\Exception $e) { $class = get_class($e); // rethrow exception for a specific action throw new $class($e->getMessage() . "\nat $step"); } } } public function __toString() { $actionsLog = []; foreach ($this->actions as $step) { $args = str_replace('"', "'", $step->getArgumentsAsString(20)); $actionsLog[] = $step->getAction() . ": $args"; } return implode(', ', $actionsLog); } } codeception/src/Codeception/Util/Uri.php000077700000007054151323602320014262 0ustar00<?php namespace Codeception\Util; use GuzzleHttp\Psr7\Uri as Psr7Uri; class Uri { /** * Merges the passed $add argument onto $base. * * If a relative URL is passed as the 'path' part of the $add url * array, the relative URL is mapped using the base 'path' part as * its base. * * @param string $baseUri the base URL * @param string $uri the URL to merge * @return array the merged array */ public static function mergeUrls($baseUri, $uri) { $base = new Psr7Uri($baseUri); $parts = parse_url($uri); //If the relative URL does not parse, attempt to parse the entire URL. //PHP Known bug ( https://bugs.php.net/bug.php?id=70942 ) if ($parts === false) { $parts = parse_url($base.$uri); } if ($parts === false) { throw new \InvalidArgumentException("Invalid URI $uri"); } if (isset($parts['host']) and isset($parts['scheme'])) { // if this is an absolute url, replace with it return $uri; } if (isset($parts['host'])) { $base = $base->withHost($parts['host']); $base = $base->withPath(''); $base = $base->withQuery(''); $base = $base->withFragment(''); } if (isset($parts['path'])) { $path = $parts['path']; $basePath = $base->getPath(); if ((strpos($path, '/') !== 0) && !empty($path)) { if ($basePath) { // if it ends with a slash, relative paths are below it if (preg_match('~/$~', $basePath)) { $path = $basePath . $path; } else { // remove double slashes $dir = rtrim(dirname($basePath), '\\/'); $path = $dir . '/' . $path; } } else { $path = '/' . ltrim($path, '/'); } } $base = $base->withPath($path); $base = $base->withQuery(''); $base = $base->withFragment(''); } if (isset($parts['query'])) { $base = $base->withQuery($parts['query']); $base = $base->withFragment(''); } if (isset($parts['fragment'])) { $base = $base->withFragment($parts['fragment']); } return (string) $base; } /** * Retrieve /path?query#fragment part of URL * @param $url * @return string */ public static function retrieveUri($url) { $uri = new Psr7Uri($url); return (string)(new Psr7Uri()) ->withPath($uri->getPath()) ->withQuery($uri->getQuery()) ->withFragment($uri->getFragment()); } public static function retrieveHost($url) { $urlParts = parse_url($url); if (!isset($urlParts['host']) or !isset($urlParts['scheme'])) { throw new \InvalidArgumentException("Wrong URL passes, host and scheme not set"); } $host = $urlParts['scheme'] . '://' . $urlParts['host']; if (isset($urlParts['port'])) { $host .= ':' . $urlParts['port']; } return $host; } public static function appendPath($url, $path) { $uri = new Psr7Uri($url); $cutUrl = (string)$uri->withQuery('')->withFragment(''); if ($path === '' || $path[0] === '#') { return $cutUrl . $path; } return rtrim($cutUrl, '/') . '/' . ltrim($path, '/'); } } codeception/src/Codeception/Util/Maybe.php000077700000014106151323602320014554 0ustar00<?php namespace Codeception\Util; /** * Class to represent any type of content. * This class can act as an object, array, or string. * Method or property calls to this class won't cause any errors. * * Maybe was used in Codeception 1.x to represent data on parsing step. * Not widely used in 2.0 anymore, but left for compatibility. * * For instance, you may use `Codeception\Util\Maybe` as a test dummies. * * ```php * <?php * $user = new Maybe; * $user->posts->comments->count(); * ?> * ``` */ class Maybe implements \ArrayAccess, \Iterator, \JsonSerializable { protected $position = 0; protected $val = null; protected $assocArray = null; public function __construct($val = null) { $this->val = $val; if (is_array($this->val)) { $this->assocArray = $this->isAssocArray($this->val); } $this->position = 0; } private function isAssocArray($arr) { return array_keys($arr) !== range(0, count($arr) - 1); } public function __toString() { if ($this->val === null) { return "?"; } if (is_scalar($this->val)) { return (string)$this->val; } if (is_object($this->val) && method_exists($this->val, '__toString')) { return $this->val->__toString(); } return $this->val; } public function __get($key) { if ($this->val === null) { return new Maybe(); } if (is_object($this->val)) { if (isset($this->val->{$key}) || property_exists($this->val, $key)) { return $this->val->{$key}; } } return $this->val->key; } public function __set($key, $val) { if ($this->val === null) { return; } if (is_object($this->val)) { $this->val->{$key} = $val; return; } $this->val->key = $val; } public function __call($method, $args) { if ($this->val === null) { return new Maybe(); } return call_user_func_array([$this->val, $method], $args); } public function __clone() { if (is_object($this->val)) { $this->val = clone $this->val; } } public function __unset($key) { if (is_object($this->val)) { if (isset($this->val->{$key}) || property_exists($this->val, $key)) { unset($this->val->{$key}); return; } } } public function offsetExists($offset) { if (is_array($this->val) || ($this->val instanceof \ArrayAccess)) { return isset($this->val[$offset]); } return false; } public function offsetGet($offset) { if (is_array($this->val) || ($this->val instanceof \ArrayAccess)) { return $this->val[$offset]; } return new Maybe(); } public function offsetSet($offset, $value) { if (is_array($this->val) || ($this->val instanceof \ArrayAccess)) { $this->val[$offset] = $value; } } public function offsetUnset($offset) { if (is_array($this->val) || ($this->val instanceof \ArrayAccess)) { unset($this->val[$offset]); } } public function __value() { $val = $this->val; if (is_array($val)) { foreach ($val as $k => $v) { if ($v instanceof self) { $v = $v->__value(); } $val[$k] = $v; } } return $val; } /** * (PHP 5 >= 5.0.0)<br/> * Return the current element * @link http://php.net/manual/en/iterator.current.php * @return mixed Can return any type. */ public function current() { if (!is_array($this->val)) { return null; } if ($this->assocArray) { $keys = array_keys($this->val); return $this->val[$keys[$this->position]]; } return $this->val[$this->position]; } /** * (PHP 5 >= 5.0.0)<br/> * Move forward to next element * @link http://php.net/manual/en/iterator.next.php * @return void Any returned value is ignored. */ public function next() { ++$this->position; } /** * (PHP 5 >= 5.0.0)<br/> * Return the key of the current element * @link http://php.net/manual/en/iterator.key.php * @return mixed scalar on success, or null on failure. */ public function key() { if ($this->assocArray) { $keys = array_keys($this->val); return $keys[$this->position]; } return $this->position; } /** * (PHP 5 >= 5.0.0)<br/> * Checks if current position is valid * @link http://php.net/manual/en/iterator.valid.php * @return boolean The return value will be casted to boolean and then evaluated. * Returns true on success or false on failure. */ public function valid() { if (!is_array($this->val)) { return null; } if ($this->assocArray) { $keys = array_keys($this->val); return isset($keys[$this->position]); } return isset($this->val[$this->position]); } /** * (PHP 5 >= 5.0.0)<br/> * Rewind the Iterator to the first element * @link http://php.net/manual/en/iterator.rewind.php * @return void Any returned value is ignored. */ public function rewind() { if (is_array($this->val)) { $this->assocArray = $this->isAssocArray($this->val); } $this->position = 0; } /** * (PHP 5 >= 5.4.0) * Serializes the object to a value that can be serialized natively by json_encode(). * @link http://docs.php.net/manual/en/jsonserializable.jsonserialize.php * @return mixed Returns data which can be serialized by json_encode(), which is a value of any type other than a resource. */ public function jsonSerialize() { return $this->__value(); } } codeception/src/Codeception/InitTemplate.php000077700000024306151323602320015204 0ustar00<?php namespace Codeception; use Codeception\Command\Shared\FileSystem; use Codeception\Command\Shared\Style; use Codeception\Lib\ModuleContainer; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\Question; /** * Codeception templates allow creating a customized setup and configuration for your project. * An abstract class for installation template. Each init template should extend it and implement a `setup` method. * Use it to build a custom setup class which can be started with `codecept init` command. * * * ```php * <?php * namespace Codeception\Template; // it is important to use this namespace so codecept init could locate this template * class CustomInstall extends \Codeception\InitTemplate * { * public function setup() * { * // implement this * } * } * ``` * This class provides various helper methods for building customized setup */ abstract class InitTemplate { use FileSystem; use Style; const GIT_IGNORE = '.gitignore'; /** * @var string */ protected $namespace = ''; /** * @var string */ protected $actorSuffix = 'Tester'; /** * @var string */ protected $workDir = '.'; /** * @var InputInterface */ protected $input; /** * @var OutputInterface */ protected $output; public function __construct(InputInterface $input, OutputInterface $output) { $this->input = $input; $this->addStyles($output); $this->output = $output; } /** * Change the directory where Codeception should be installed. */ public function initDir($workDir) { $this->checkInstalled($workDir); $this->sayInfo("Initializing Codeception in $workDir"); $this->createDirectoryFor($workDir); chdir($workDir); $this->workDir = $workDir; } /** * Override this class to create customized setup. * @return mixed */ abstract public function setup(); /** * ```php * <?php * // propose firefox as default browser * $this->ask('select the browser of your choice', 'firefox'); * * // propose firefox or chrome possible options * $this->ask('select the browser of your choice', ['firefox', 'chrome']); * * // ask true/false question * $this->ask('do you want to proceed (y/n)', true); * ``` * * @param string $question * @param mixed $answer * @return mixed|string */ protected function ask($question, $answer = null) { $question = "? $question"; $dialog = new QuestionHelper(); if (is_array($answer)) { $question .= " <info>(" . $answer[0] . ")</info> "; return $dialog->ask($this->input, $this->output, new ChoiceQuestion($question, $answer, 0)); } if (is_bool($answer)) { $question .= " (y/n) "; return $dialog->ask($this->input, $this->output, new ConfirmationQuestion($question, $answer)); } if ($answer) { $question .= " <info>($answer)</info>"; } return $dialog->ask($this->input, $this->output, new Question("$question ", $answer)); } /** * Print a message to console. * * ```php * <?php * $this->say('Welcome to Setup'); * ``` * * * @param string $message */ protected function say($message = '') { $this->output->writeln($message); } /** * Print a successful message * @param string $message */ protected function saySuccess($message) { $this->say("<notice> $message </notice>"); } /** * Print error message * @param string $message */ protected function sayError($message) { $this->say("<error> $message </error>"); } /** * Print warning message * @param $message */ protected function sayWarning($message) { $this->say("<warning> $message </warning>"); } /** * Print info message * @param string $message */ protected function sayInfo($message) { $this->say("<debug> $message</debug>"); } /** * Create a helper class inside a directory * * @param $name * @param $directory */ protected function createHelper($name, $directory) { $file = $this->createDirectoryFor( $dir = $directory . DIRECTORY_SEPARATOR . "Helper", "$name.php" ) . "$name.php"; $gen = new Lib\Generator\Helper($name, $this->namespace); // generate helper $this->createFile( $file, $gen->produce() ); require_once $file; $this->sayInfo("$name helper has been created in $dir"); } /** * Create an empty directory and add a placeholder file into it * @param $dir */ protected function createEmptyDirectory($dir) { $this->createDirectoryFor($dir); $this->createFile($dir . DIRECTORY_SEPARATOR . '.gitkeep', ''); } protected function gitIgnore($path) { if (file_exists(self::GIT_IGNORE)) { file_put_contents($path . DIRECTORY_SEPARATOR . self::GIT_IGNORE, "*\n!" . self::GIT_IGNORE); } } protected function checkInstalled($dir = '.') { if (file_exists($dir . DIRECTORY_SEPARATOR . 'codeception.yml') || file_exists($dir . DIRECTORY_SEPARATOR . 'codeception.dist.yml')) { throw new \Exception("Codeception is already installed in this directory"); } } /** * Create an Actor class and generate actions for it. * Requires a suite config as array in 3rd parameter. * * @param $name * @param $directory * @param $suiteConfig */ protected function createActor($name, $directory, $suiteConfig) { $file = $this->createDirectoryFor( $directory, $name ) . $this->getShortClassName($name); $file .= '.php'; $suiteConfig['namespace'] = $this->namespace; $config = Configuration::mergeConfigs(Configuration::$defaultSuiteSettings, $suiteConfig); $actorGenerator = new Lib\Generator\Actor($config); $content = $actorGenerator->produce(); $this->createFile($file, $content); $this->sayInfo("$name actor has been created in $directory"); $actionsGenerator = new Lib\Generator\Actions($config); $content = $actionsGenerator->produce(); $generatedDir = $directory . DIRECTORY_SEPARATOR . '_generated'; $this->createDirectoryFor($generatedDir, 'Actions.php'); $this->createFile($generatedDir . DIRECTORY_SEPARATOR . $actorGenerator->getActorName() . 'Actions.php', $content); $this->sayInfo("Actions have been loaded"); } protected function addModulesToComposer($modules) { $packages = ModuleContainer::$packages; $section = null; if (!file_exists('composer.json')) { $this->say(''); $this->sayWarning('Can\'t locate composer.json, please add following packages into "require-dev" section of composer.json:'); $this->say(''); foreach (array_unique($modules) as $module) { if (!isset($packages[$module])) { continue; } $package = $packages[$module]; $this->say(sprintf('"%s": "%s"', $package, "^1.0.0")); $composer[$section][$package] = "^1.0.0"; } $this->say(''); return; } $composer = json_decode(file_get_contents('composer.json'), true); if ($composer === null) { throw new \Exception("Invalid composer.json file. JSON can't be decoded"); } $section = null; if (isset($composer['require'])) { if (isset($composer['require']['codeception/codeception'])) { $section = 'require'; } } if (isset($composer['require-dev'])) { if (isset($composer['require-dev']['codeception/codeception'])) { $section = 'require-dev'; } } if (!$section) { $section = 'require'; } $packageCounter = 0; foreach (array_unique($modules) as $module) { if (!isset($packages[$module])) { continue; } $package = $packages[$module]; if (isset($composer[$section][$package])) { continue; } $this->sayInfo("Adding $package for $module to composer.json"); $composer[$section][$package] = "^1.0.0"; $packageCounter++; } file_put_contents('composer.json', json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); if ($packageCounter) { $this->say("$packageCounter new packages added to $section"); } if ($packageCounter && $this->ask('composer.json updated. Do you want to run "composer update"?', true)) { $this->sayInfo('Running composer update'); exec('composer update', $output, $status); if ($status !== 0) { $this->sayInfo('Composer installation failed. Please check composer.json and try to run "composer update" manually'); return; } if (!empty($composer['config']['vendor_dir'])) { $this->updateComposerClassMap($composer['config']['vendor_dir']); } else { $this->updateComposerClassMap(); } } return $packageCounter; } private function updateComposerClassMap($vendorDir = 'vendor') { $loader = require $vendorDir . '/autoload.php'; $classMap = require $vendorDir . '/composer/autoload_classmap.php'; $loader->addClassMap($classMap); $map = require $vendorDir . '/composer/autoload_psr4.php'; foreach ($map as $namespace => $path) { $loader->setPsr4($namespace, $path); } } } codeception/src/.htaccess000077700000000177151323602320011436 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/LICENSE000077700000002116151323602320010051 0ustar00The MIT License (MIT) Copyright (c) 2011 Michael Bodnarchuk and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. codeception/codecept.bat000077700000000173151323602320011323 0ustar00@echo off if "%PHP_PEAR_PHP_BIN%" neq "" ( set PHPBIN=%PHP_PEAR_PHP_BIN% ) else set PHPBIN=php "%PHPBIN%" "codecept" %* codeception/autoload.php000077700000007147151323602320011376 0ustar00<?php $autoloadFile = './vendor/codeception/codeception/autoload.php'; if (( !isset($argv) || (isset($argv) && !in_array('--no-redirect', $argv)) ) && file_exists('./vendor/autoload.php') && file_exists($autoloadFile) && __FILE__ != realpath($autoloadFile)) { //for global installation or phar file fwrite( STDERR, "\n==== Redirecting to Composer-installed version in vendor/codeception. You can skip this using --no-redirect ====\n" ); if (file_exists('./vendor/codeception/codeception/app.php')) { //codeception v4+ require './vendor/codeception/codeception/app.php'; } else { //older version require $autoloadFile; //require package/bin instead of codecept to avoid printing hashbang line require './vendor/codeception/codeception/package/bin'; } die; } elseif (file_exists(__DIR__ . '/vendor/autoload.php')) { // for phar require_once __DIR__ . '/vendor/autoload.php'; } elseif (file_exists(__DIR__ . '/../../autoload.php')) { //for composer require_once __DIR__ . '/../../autoload.php'; } unset($autoloadFile); if (isset($argv)) { $argv = array_values(array_diff($argv, ['--no-redirect'])); } if (isset($_SERVER['argv'])) { $_SERVER['argv'] = array_values(array_diff($_SERVER['argv'], ['--no-redirect'])); } // @codingStandardsIgnoreStart include_once __DIR__ . DIRECTORY_SEPARATOR . 'shim.php'; // compat if (PHP_MAJOR_VERSION < 7) { if (false === interface_exists('Throwable', false)) { interface Throwable {}; } if (false === class_exists('ParseError', false)) { class ParseError extends \Exception {}; } } // @codingStandardsIgnoreEnd // function not autoloaded in PHP, thus its a good place for them if (!function_exists('codecept_debug')) { function codecept_debug($data) { \Codeception\Util\Debug::debug($data); } } if (!function_exists('codecept_root_dir')) { function codecept_root_dir($appendPath = '') { return \Codeception\Configuration::projectDir() . $appendPath; } } if (!function_exists('codecept_output_dir')) { function codecept_output_dir($appendPath = '') { return \Codeception\Configuration::outputDir() . $appendPath; } } if (!function_exists('codecept_log_dir')) { function codecept_log_dir($appendPath = '') { return \Codeception\Configuration::outputDir() . $appendPath; } } if (!function_exists('codecept_data_dir')) { function codecept_data_dir($appendPath = '') { return \Codeception\Configuration::dataDir() . $appendPath; } } if (!function_exists('codecept_relative_path')) { function codecept_relative_path($path) { return \Codeception\Util\PathResolver::getRelativeDir( $path, \Codeception\Configuration::projectDir(), DIRECTORY_SEPARATOR ); } } if (!function_exists('codecept_absolute_path')) { /** * If $path is absolute, it will be returned without changes. * If $path is relative, it will be passed to `codecept_root_dir()` function * to make it absolute. * * @param string $path * @return string the absolute path */ function codecept_absolute_path($path) { return codecept_is_path_absolute($path) ? $path : codecept_root_dir($path); } } if (!function_exists('codecept_is_path_absolute')) { /** * Check whether the given $path is absolute. * * @param string $path * @return bool * @since 2.4.4 */ function codecept_is_path_absolute($path) { return \Codeception\Util\PathResolver::isPathAbsolute($path); } } codeception/package/bin000077700000000050151323602320011125 0ustar00<?php require __DIR__ . '/../app.php'; codeception/package/.htaccess000077700000000177151323602320012242 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/.dockerignore000077700000000126151323602320011517 0ustar00/package /.github /tests /vendor /*.md /*.yml /codecept.bat /nitpick.json /ruleset.xmlcodeception/ext/README.md000077700000014536151323602320011134 0ustar00# Official Extensions ## DotReporter [See Source](https://github.com/Codeception/Codeception/blob/4.0/ext/DotReporter.php) DotReporter provides less verbose output for test execution. Like PHPUnit printer it prints dots "." for successful testes and "F" for failures.  ```bash .......... .......... .......... .......... .......... .......... .......... .......... Time: 2.07 seconds, Memory: 20.00MB OK (80 tests, 124 assertions) ``` Enable this reporter with `--ext option` ``` codecept run --ext DotReporter ``` Failures and Errors are printed by a standard Codeception reporter. Use this extension as an example for building custom reporters. ## Logger [See Source](https://github.com/Codeception/Codeception/blob/4.0/ext/Logger.php) Log suites/tests/steps using Monolog library. Monolog should be installed additionally by Composer. ``` composer require monolog/monolog ``` Steps are logged into `tests/_output/codeception.log` To enable this module add to your `codeception.yml`: ``` yaml extensions: enabled: [Codeception\Extension\Logger] ``` #### Config * `max_files` (default: 3) - how many log files to keep ## Recorder [See Source](https://github.com/Codeception/Codeception/blob/4.0/ext/Recorder.php) Saves a screenshot of each step in acceptance tests and shows them as a slideshow on one HTML page (here's an [example](http://codeception.com/images/recorder.gif)) Activated only for suites with WebDriver module enabled. The screenshots are saved to `tests/_output/record_*` directories, open `index.html` to see them as a slideshow. #### Installation Add this to the list of enabled extensions in `codeception.yml` or `acceptance.suite.yml`: ``` yaml extensions: enabled: - Codeception\Extension\Recorder ``` #### Configuration * `delete_successful` (default: true) - delete screenshots for successfully passed tests (i.e. log only failed and errored tests). * `module` (default: WebDriver) - which module for screenshots to use. Set `AngularJS` if you want to use it with AngularJS module. Generally, the module should implement `Codeception\Lib\Interfaces\ScreenshotSaver` interface. * `ignore_steps` (default: []) - array of step names that should not be recorded (given the step passed), * wildcards supported. Meta steps can also be ignored. * `success_color` (default: success) - bootstrap values to be used for color representation for passed tests * `failure_color` (default: danger) - bootstrap values to be used for color representation for failed tests * `error_color` (default: dark) - bootstrap values to be used for color representation for scenarios where there's an issue occurred while generating a recording * `delete_orphaned` (default: false) - delete recording folders created via previous runs * `include_microseconds` (default: false) - enable microsecond precision for recorded step time details #### Examples: ``` yaml extensions: enabled: - Codeception\Extension\Recorder: module: AngularJS # enable for Angular delete_successful: false # keep screenshots of successful tests ignore_steps: [have, grab*] ``` #### Skipping recording of steps with annotations It is also possible to skip recording of steps for specified tests by using the @skipRecording annotation. ```php /** * @skipRecording login * @skipRecording amOnUrl *\/ public function testLogin(AcceptanceTester $I) { $I->login(); $I->amOnUrl('http://codeception.com'); } ``` ## RunBefore [See Source](https://github.com/Codeception/Codeception/blob/4.0/ext/RunBefore.php) Extension for execution of some processes before running tests. Processes can be independent and dependent. Independent processes run independently of each other. Dependent processes run sequentially one by one. Can be configured in suite config: ```yaml # acceptance.suite.yml extensions: enabled: - Codeception\Extension\RunBefore: - independent_process_1 - - dependent_process_1_1 - dependent_process_1_2 - independent_process_2 - - dependent_process_2_1 - dependent_process_2_2 ``` HINT: you can use different configurations per environment. ## RunFailed [See Source](https://github.com/Codeception/Codeception/blob/4.0/ext/RunFailed.php) Saves failed tests into tests/log/failed in order to rerun failed tests. To rerun failed tests just run the `failed` group: ``` php codecept run -g failed ``` To change failed group name add: ``` --override "extensions: config: Codeception\Extension\RunFailed: fail-group: another_group1" ``` Remember: if you run tests and they generated custom-named fail group, to run this group, you should add override too Starting from Codeception 2.1 **this extension is enabled by default**. ``` yaml extensions: enabled: [Codeception\Extension\RunFailed] ``` On each execution failed tests are logged and saved into `tests/_output/failed` file. ## RunProcess [See Source](https://github.com/Codeception/Codeception/blob/4.0/ext/RunProcess.php) Extension to start and stop processes per suite. Can be used to start/stop selenium server, chromedriver, phantomjs, mailcatcher, etc. Can be configured in suite config: ```yaml # acceptance.suite.yml extensions: enabled: - Codeception\Extension\RunProcess: - chromedriver ``` Multiple parameters can be passed as array: ```yaml # acceptance.suite.yml extensions: enabled: - Codeception\Extension\RunProcess: - php -S 127.0.0.1:8000 -t tests/data/app - java -jar ~/selenium-server.jar ``` In the end of a suite all launched processes will be stopped. To wait for the process to be launched use `sleep` option. In this case you need configuration to be specified as object: ```yaml extensions: enabled: - Codeception\Extension\RunProcess: 0: java -jar ~/selenium-server.jar 1: mailcatcher sleep: 5 # wait 5 seconds for processes to boot ``` HINT: you can use different configurations per environment. ## SimpleReporter [See Source](https://github.com/Codeception/Codeception/blob/4.0/ext/SimpleReporter.php) This extension demonstrates how you can implement console output of your own. Recommended to be used for development purposes only. codeception/ext/Recorder.php000077700000052142151323602320012126 0ustar00<?php namespace Codeception\Extension; use Codeception\Event\StepEvent; use Codeception\Event\TestEvent; use Codeception\Events; use Codeception\Exception\ExtensionException; use Codeception\Lib\Interfaces\ScreenshotSaver; use Codeception\Module\WebDriver; use Codeception\Step; use Codeception\Step\Comment as CommentStep; use Codeception\Test\Descriptor; use Codeception\Util\FileSystem; use Codeception\Util\Template; /** * Saves a screenshot of each step in acceptance tests and shows them as a slideshow on one HTML page (here's an [example](http://codeception.com/images/recorder.gif)) * Activated only for suites with WebDriver module enabled. * * The screenshots are saved to `tests/_output/record_*` directories, open `index.html` to see them as a slideshow. * * #### Installation * * Add this to the list of enabled extensions in `codeception.yml` or `acceptance.suite.yml`: * * ``` yaml * extensions: * enabled: * - Codeception\Extension\Recorder * ``` * * #### Configuration * * * `delete_successful` (default: true) - delete screenshots for successfully passed tests (i.e. log only failed and errored tests). * * `module` (default: WebDriver) - which module for screenshots to use. Set `AngularJS` if you want to use it with AngularJS module. Generally, the module should implement `Codeception\Lib\Interfaces\ScreenshotSaver` interface. * * `ignore_steps` (default: []) - array of step names that should not be recorded (given the step passed), * wildcards supported. Meta steps can also be ignored. * * `success_color` (default: success) - bootstrap values to be used for color representation for passed tests * * `failure_color` (default: danger) - bootstrap values to be used for color representation for failed tests * * `error_color` (default: dark) - bootstrap values to be used for color representation for scenarios where there's an issue occurred while generating a recording * * `delete_orphaned` (default: false) - delete recording folders created via previous runs * * `include_microseconds` (default: false) - enable microsecond precision for recorded step time details * * #### Examples: * * ``` yaml * extensions: * enabled: * - Codeception\Extension\Recorder: * module: AngularJS # enable for Angular * delete_successful: false # keep screenshots of successful tests * ignore_steps: [have, grab*] * ``` * #### Skipping recording of steps with annotations * * It is also possible to skip recording of steps for specified tests by using the @skipRecording annotation. * * ```php * /** * * @skipRecording login * * @skipRecording amOnUrl * *\/ * public function testLogin(AcceptanceTester $I) * { * $I->login(); * $I->amOnUrl('http://codeception.com'); * } * ``` * */ class Recorder extends \Codeception\Extension { /** @var array */ protected $config = [ 'delete_successful' => true, 'module' => 'WebDriver', 'template' => null, 'animate_slides' => true, 'ignore_steps' => [], 'success_color' => 'success', 'failure_color' => 'danger', 'error_color' => 'dark', 'delete_orphaned' => false, 'include_microseconds' => false, ]; /** @var string */ protected $template = <<<EOF <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Recorder Result</title> <!-- Bootstrap Core CSS --> <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" rel="stylesheet"> <style> html, body { height: 100%; } .active { height: 100%; } .carousel-caption { background: rgba(0,0,0,0.8); } .carousel-caption.error { background: #c0392b !important; } .carousel-item { min-height: 100vh; } .fill { width: 100%; height: 100%; text-align: center; overflow-y: scroll; background-position: top; -webkit-background-size: cover; -moz-background-size: cover; background-size: cover; -o-background-size: cover; } .gradient-right { background: linear-gradient(to left, rgba(0,0,0,.4), rgba(0,0,0,.0)) } .gradient-left { background: linear-gradient(to right, rgba(0,0,0,.4), rgba(0,0,0,.0)) } </style> </head> <body> <!-- Navigation --> <nav class="navbar navbar-expand-lg navbar-light bg-light" role="navigation"> <div class="navbar-header"> <a class="navbar-brand" href="../records.html"></span>Recorded Tests</a> </div> <div class="collapse navbar-collapse" id="navbarText"> <ul class="navbar-nav mr-auto"> <span class="navbar-text">{{feature}}</span> </ul> <span class="navbar-text">{{test}}</span> </div> </nav> <header id="steps" class="carousel slide" data-ride="carousel"> <!-- Indicators --> <ol class="carousel-indicators"> {{indicators}} </ol> <!-- Wrapper for Slides --> <div class="carousel-inner"> {{slides}} </div> <!-- Controls --> <a class="carousel-control-prev gradient-left" href="#steps" role="button" data-slide="prev"> <span class="carousel-control-prev-icon" aria-hidden="false"></span> <span class="sr-only">Previous</span> </a> <a class="carousel-control-next gradient-right" href="#steps" role="button" data-slide="next"> <span class="carousel-control-next-icon" aria-hidden="false"></span> <span class="sr-only">Next</span> </a> </header> <!-- jQuery --> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"></script> <!-- Script to Activate the Carousel --> <script> $('.carousel').carousel({ wrap: true, interval: false }) $(document).bind('keyup', function(e) { if(e.keyCode==39){ jQuery('a.carousel-control.right').trigger('click'); } else if(e.keyCode==37){ jQuery('a.carousel-control.left').trigger('click'); } }); </script> </body> </html> EOF; /** @var string */ protected $indicatorTemplate = <<<EOF <li data-target="#steps" data-slide-to="{{step}}" class="{{isActive}}"></li> EOF; /** @var string */ protected $indexTemplate = <<<EOF <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Recorder Results Index</title> <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <!-- Navigation --> <nav class="navbar navbar-expand-lg navbar-light bg-light" role="navigation"> <div class="navbar-header"> <a class="navbar-brand" href="#">Recorded Tests </a> </div> </nav> <div class="container py-4"> <h1>Record #{{seed}}</h1> <ul> {{records}} </ul> </div> </body> </html> EOF; /** @var string */ protected $slidesTemplate = <<<EOF <div class="carousel-item {{isActive}}"> <img class="mx-auto d-block mh-100" src="{{image}}"> <div class="carousel-caption {{isError}}"> <h5>{{caption}}</h5> <p>Step finished at <span style="color: #3498db">"{{timeStamp}}"</span></p> </div> </div> EOF; /** @var array */ public static $events = [ Events::SUITE_BEFORE => 'beforeSuite', Events::SUITE_AFTER => 'afterSuite', Events::TEST_BEFORE => 'before', Events::TEST_ERROR => 'persist', Events::TEST_FAIL => 'persist', Events::TEST_SUCCESS => 'cleanup', Events::STEP_AFTER => 'afterStep', ]; /** @var WebDriver */ protected $webDriverModule; /** @var string */ protected $dir; /** @var array */ protected $slides = []; /** @var int */ protected $stepNum = 0; /** @var string */ protected $seed; /** @var array */ protected $seeds; /** @var array */ protected $recordedTests = []; /** @var array */ protected $skipRecording = []; /** @var array */ protected $errorMessages = []; /** @var bool */ protected $colors; /** @var bool */ protected $ansi; /** @var array */ protected $timeStamps = []; /** @var string */ private $dateFormat; public function beforeSuite() { $this->webDriverModule = null; if (!$this->hasModule($this->config['module'])) { $this->writeln('Recorder is disabled, no available modules'); return; } $this->seed = uniqid(); $this->seeds[] = $this->seed; $this->webDriverModule = $this->getModule($this->config['module']); $this->skipRecording = []; $this->errorMessages = []; $this->dateFormat = $this->config['include_microseconds'] ? 'Y-m-d\TH:i:s.uP' : DATE_ATOM; $this->ansi = !isset($this->options['no-ansi']); $this->colors = !isset($this->options['no-colors']); if (!$this->webDriverModule instanceof ScreenshotSaver) { throw new ExtensionException( $this, 'You should pass module which implements ' . ScreenshotSaver::class . ' interface' ); } $this->writeln( sprintf( '⏺ <bold>Recording</bold> ⏺ step-by-step screenshots will be saved to <info>%s</info>', codecept_output_dir() ) ); $this->writeln("Directory Format: <debug>record_{$this->seed}_{filename}_{testname}</debug> ----"); } public function afterSuite() { if (!$this->webDriverModule) { return; } $links = ''; if (count($this->slides)) { foreach ($this->recordedTests as $suiteName => $suite) { $links .= "<ul><li><b>{$suiteName}</b></li><ul>"; foreach ($suite as $fileName => $tests) { $links .= "<li>{$fileName}</li><ul>"; foreach ($tests as $test) { $links .= in_array($test['path'], $this->skipRecording, true) ? "<li class=\"text{$this->config['error_color']}\">{$test['name']}</li>\n" : '<li class="text-' . $this->config[$test['status'] . '_color'] . "\"><a href='{$test['index']}'>{$test['name']}</a></li>\n"; } $links .= '</ul>'; } $links .= '</ul></ul>'; } $indexHTML = (new Template($this->indexTemplate)) ->place('seed', $this->seed) ->place('records', $links) ->produce(); try { file_put_contents(codecept_output_dir() . 'records.html', $indexHTML); } catch (\Exception $exception) { $this->writeln( "⏺ An exception occurred while saving records.html: <info>{$exception->getMessage()}</info>" ); } $this->writeln('⏺ Records saved into: <info>file://' . codecept_output_dir() . 'records.html</info>'); } foreach ($this->errorMessages as $message) { $this->writeln($message); } } /** * @param TestEvent $e */ public function before(TestEvent $e) { if (!$this->webDriverModule) { return; } $this->dir = null; $this->stepNum = 0; $this->slides = []; $this->timeStamps = []; $this->dir = codecept_output_dir() . "record_{$this->seed}_{$this->getTestName($e)}"; $testPath = codecept_relative_path(Descriptor::getTestFullName($e->getTest())); try { !is_dir($this->dir) && !mkdir($this->dir) && !is_dir($this->dir); } catch (\Exception $exception) { $this->skipRecording[] = $testPath; $this->appendErrorMessage( $testPath, "⏺ An exception occurred while creating directory: <info>{$this->dir}</info>" ); } } /** * @param TestEvent $e */ public function cleanup(TestEvent $e) { if ($this->config['delete_orphaned']) { $recordingDirectories = []; $directories = new \DirectoryIterator(codecept_output_dir()); // getting a list of currently present recording directories foreach ($directories as $directory) { preg_match('/^record_(.*?)_[^\n]+.php_[^\n]+$/', $directory->getFilename(), $match); if (isset($match[1])) { $recordingDirectories[$match[1]][] = codecept_output_dir() . $directory->getFilename(); } } // removing orphaned recording directories foreach (array_diff(array_keys($recordingDirectories), $this->seeds) as $orphanedSeed) { foreach ($recordingDirectories[$orphanedSeed] as $orphanedDirectory) { FileSystem::deleteDir($orphanedDirectory); } } } if (!$this->webDriverModule || !$this->dir) { return; } if (!$this->config['delete_successful']) { $this->persist($e); return; } // deleting successfully executed tests FileSystem::deleteDir($this->dir); } /** * @param TestEvent $e */ public function persist(TestEvent $e) { if (!$this->webDriverModule) { return; } $indicatorHtml = ''; $slideHtml = ''; $testName = $this->getTestName($e); $testPath = codecept_relative_path(Descriptor::getTestFullName($e->getTest())); $dir = codecept_output_dir() . "record_{$this->seed}_$testName"; $status = 'success'; if (strcasecmp($this->dir, $dir) !== 0) { $filename = str_pad(0, 3, '0', STR_PAD_LEFT) . '.png'; try { !is_dir($dir) && !mkdir($dir) && !is_dir($dir); $this->dir = $dir; } catch (\Exception $exception) { $this->skipRecording[] = $testPath; $this->appendErrorMessage( $testPath, "⏺ An exception occurred while creating directory: <info>{$dir}</info>" ); } $this->slides = []; $this->timeStamps = []; $this->slides[$filename] = new Step\Action('encountered an unexpected error prior to the test execution'); $this->timeStamps[$filename] = (new \DateTime())->format($this->dateFormat); $status = 'error'; try { if ($this->webDriverModule->webDriver === null) { throw new ExtensionException($this, 'Failed to save screenshot as webDriver is not set'); } $this->webDriverModule->webDriver->takeScreenshot($this->dir . DIRECTORY_SEPARATOR . $filename); } catch (\Exception $exception) { $this->appendErrorMessage( $testPath, "⏺ Unable to capture a screenshot for <info>{$testPath}/before</info>" ); } } if (!in_array($testPath, $this->skipRecording, true)) { foreach ($this->slides as $i => $step) { /** @var Step $step */ if ($step->hasFailed()) { $status = 'failure'; } $indicatorHtml .= (new Template($this->indicatorTemplate)) ->place('step', (int)$i) ->place('isActive', (int)$i ? '' : 'active') ->produce(); $slideHtml .= (new Template($this->slidesTemplate)) ->place('image', $i) ->place('caption', $step->getHtml('#3498db')) ->place('isActive', (int)$i ? '' : 'active') ->place('isError', $status === 'success' ? '' : 'error') ->place('timeStamp', $this->timeStamps[$i]) ->produce(); } $html = (new Template($this->template)) ->place('indicators', $indicatorHtml) ->place('slides', $slideHtml) ->place('feature', ucfirst($e->getTest()->getFeature())) ->place('test', Descriptor::getTestSignature($e->getTest())) ->place('carousel_class', $this->config['animate_slides'] ? ' slide' : '') ->produce(); $indexFile = $this->dir . DIRECTORY_SEPARATOR . 'index.html'; $environment = $e->getTest()->getMetadata()->getCurrent('env') ?: ''; $suite = ucfirst(basename(\dirname($e->getTest()->getMetadata()->getFilename()))); $testName = basename($e->getTest()->getMetadata()->getFilename()); try { file_put_contents($indexFile, $html); } catch (\Exception $exception) { $this->skipRecording[] = $testPath; $this->appendErrorMessage( $testPath, "⏺ An exception occurred while saving index.html for <info>{$testPath}: " . "{$exception->getMessage()}</info>" ); } $this->recordedTests["{$suite} ({$environment})"][$testName][] = [ 'name' => $e->getTest()->getMetadata()->getName(), 'path' => $testPath, 'status' => $status, 'index' => substr($indexFile, strlen(codecept_output_dir())), ]; } } /** * @param StepEvent $e */ public function afterStep(StepEvent $e) { if ($this->webDriverModule === null || $this->dir === null) { return; } if ($e->getStep() instanceof CommentStep) { return; } // only taking the ignore step into consideration if that step has passed if ($this->isStepIgnored($e) && !$e->getStep()->hasFailed()) { return; } $filename = str_pad($this->stepNum, 3, '0', STR_PAD_LEFT) . '.png'; try { if ($this->webDriverModule->webDriver === null) { throw new ExtensionException($this, 'Failed to save screenshot as webDriver is not set'); } $this->webDriverModule->webDriver->takeScreenshot($this->dir . DIRECTORY_SEPARATOR . $filename); } catch (\Exception $exception) { $testPath = codecept_relative_path(Descriptor::getTestFullName($e->getTest())); $this->appendErrorMessage( $testPath, "⏺ Unable to capture a screenshot for <info>{$testPath}/{$e->getStep()->getAction()}</info>" ); } $this->stepNum++; $this->slides[$filename] = $e->getStep(); $this->timeStamps[$filename] = (new \DateTime())->format($this->dateFormat); } /** * @param StepEvent $e * * @return bool */ protected function isStepIgnored(StepEvent $e) { $configIgnoredSteps = $this->config['ignore_steps']; $annotationIgnoredSteps = $e->getTest()->getMetadata()->getParam('skipRecording'); $ignoredSteps = array_unique( array_merge( $configIgnoredSteps, is_array($annotationIgnoredSteps) ? $annotationIgnoredSteps : [] ) ); foreach ($ignoredSteps as $stepPattern) { $stepRegexp = '/^' . str_replace('*', '.*?', $stepPattern) . '$/i'; if (preg_match($stepRegexp, $e->getStep()->getAction())) { return true; } if ($e->getStep()->getMetaStep() !== null && preg_match($stepRegexp, $e->getStep()->getMetaStep()->getAction()) ) { return true; } } return false; } /** * @param StepEvent|TestEvent $e * * @return string */ private function getTestName($e) { return basename($e->getTest()->getMetadata()->getFilename()) . '_' . preg_replace('/[^A-Za-z0-9\-\_]/', '_', $e->getTest()->getMetadata()->getName()); } /** * @param string $message */ protected function writeln($message) { parent::writeln( $this->ansi ? $message : trim(preg_replace('/[ ]{2,}/', ' ', str_replace('⏺', '', $message))) ); } /** * @param string $testPath * @param string $message */ private function appendErrorMessage($testPath, $message) { $this->errorMessages[$testPath] = array_merge( array_key_exists($testPath, $this->errorMessages) ? $this->errorMessages[$testPath]: [], [$message] ); } } codeception/ext/Logger.php000077700000007764151323602320011612 0ustar00<?php namespace Codeception\Extension; use Codeception\Event\FailEvent; use Codeception\Event\StepEvent; use Codeception\Event\SuiteEvent; use Codeception\Event\TestEvent; use Codeception\Events; use Codeception\Exception\ConfigurationException; use Codeception\Exception\ExtensionException; use Codeception\Extension; use Codeception\Test\Descriptor; use Monolog\Formatter\LineFormatter; use Monolog\Handler\RotatingFileHandler; /** * Log suites/tests/steps using Monolog library. * Monolog should be installed additionally by Composer. * * ``` * composer require monolog/monolog * ``` * * Codeception's core/internal stuff is logged into `tests/_output/codeception.log`. * Test suites' steps are logged into `tests/_output/<test_full_name>-<rotation_date>.log`. * * To enable this module add to your `codeception.yml`: * * ``` yaml * extensions: * enabled: [Codeception\Extension\Logger] * ``` * * #### Config * * * `max_files` (default: 3) - how many log files to keep * */ class Logger extends Extension { public static $events = [ Events::SUITE_BEFORE => 'beforeSuite', Events::TEST_BEFORE => 'beforeTest', Events::TEST_AFTER => 'afterTest', Events::TEST_END => 'endTest', Events::STEP_BEFORE => 'beforeStep', Events::TEST_FAIL => 'testFail', Events::TEST_ERROR => 'testError', Events::TEST_INCOMPLETE => 'testIncomplete', Events::TEST_SKIPPED => 'testSkipped', ]; protected $logHandler; /** * @var \Monolog\Logger */ protected static $logger; protected $path; protected $config = ['max_files' => 3]; public function _initialize() { if (!class_exists('\Monolog\Logger')) { throw new ConfigurationException("Logger extension requires Monolog library to be installed"); } $this->path = $this->getLogDir(); // internal log $logHandler = new RotatingFileHandler($this->path . 'codeception.log', $this->config['max_files']); $formatter = $logHandler->getFormatter(); if ($formatter instanceof LineFormatter) { $formatter->ignoreEmptyContextAndExtra(true); } self::$logger = new \Monolog\Logger('Codeception'); self::$logger->pushHandler($logHandler); } public static function getLogger() { return self::$logger; } public function beforeSuite(SuiteEvent $e) { $suiteLogFile = str_replace('\\', '_', $e->getSuite()->getName()) . '.log'; $this->logHandler = new RotatingFileHandler($this->path . $suiteLogFile, $this->config['max_files']); } public function beforeTest(TestEvent $e) { self::$logger = new \Monolog\Logger(Descriptor::getTestFullName($e->getTest())); self::$logger->pushHandler($this->logHandler); self::$logger->info('------------------------------------'); self::$logger->info("STARTED: " . ucfirst(Descriptor::getTestAsString($e->getTest()))); } public function afterTest(TestEvent $e) { } public function endTest(TestEvent $e) { self::$logger->info("PASSED"); } public function testFail(FailEvent $e) { self::$logger->alert($e->getFail()->getMessage()); self::$logger->info("# FAILED #"); } public function testError(FailEvent $e) { self::$logger->alert($e->getFail()->getMessage()); self::$logger->info("# ERROR #"); } public function testSkipped(FailEvent $e) { self::$logger->info("# Skipped #"); } public function testIncomplete(FailEvent $e) { self::$logger->info("# Incomplete #"); } public function beforeStep(StepEvent $e) { self::$logger->info((string) $e->getStep()); } } if (!function_exists('codecept_log')) { function codecept_log() { return Logger::getLogger(); } } else { throw new ExtensionException('Codeception\Extension\Logger', "function 'codecept_log' already defined"); } codeception/ext/RunProcess.php000077700000005754151323602320012473 0ustar00<?php namespace Codeception\Extension; use Codeception\Events; use Codeception\Exception\ExtensionException; use Codeception\Extension; use Symfony\Component\Process\Process; /** * Extension to start and stop processes per suite. * Can be used to start/stop selenium server, chromedriver, mailcatcher, etc. * * Can be configured in suite config: * * ```yaml * # acceptance.suite.yml * extensions: * enabled: * - Codeception\Extension\RunProcess: * - chromedriver * ``` * * Multiple parameters can be passed as array: * * ```yaml * # acceptance.suite.yml * * extensions: * enabled: * - Codeception\Extension\RunProcess: * - php -S 127.0.0.1:8000 -t tests/data/app * - java -jar ~/selenium-server.jar * ``` * * In the end of a suite all launched processes will be stopped. * * To wait for the process to be launched use `sleep` option. * In this case you need configuration to be specified as object: * * ```yaml * extensions: * enabled: * - Codeception\Extension\RunProcess: * 0: java -jar ~/selenium-server.jar * 1: mailcatcher * sleep: 5 # wait 5 seconds for processes to boot * ``` * * HINT: you can use different configurations per environment. */ class RunProcess extends Extension { protected $config = ['sleep' => 0]; protected static $events = [ Events::SUITE_BEFORE => 'runProcess', Events::SUITE_AFTER => 'stopProcess' ]; private $processes = []; public function _initialize() { if (!class_exists('Symfony\Component\Process\Process')) { throw new ExtensionException($this, 'symfony/process package is required'); } } public function runProcess() { $this->processes = []; foreach ($this->config as $key => $command) { if (!$command) { continue; } if (!is_int($key)) { continue; // configuration options } if (method_exists(Process::class, 'fromShellCommandline')) { //Symfony 4.2+ $process = Process::fromShellCommandline($command, $this->getRootDir(), null, null, null); } else { $process = new Process($command, $this->getRootDir(), null, null, null); } $process->start(); $this->processes[] = $process; $this->output->debug('[RunProcess] Starting '.$command); } sleep($this->config['sleep']); } public function __destruct() { $this->stopProcess(); } public function stopProcess() { foreach (array_reverse($this->processes) as $process) { /** @var $process Process **/ if (!$process->isRunning()) { continue; } $this->output->debug('[RunProcess] Stopping ' . $process->getCommandLine()); $process->stop(); } $this->processes = []; } } codeception/ext/RunFailed.php000077700000004616151323602320012235 0ustar00<?php namespace Codeception\Extension; use Codeception\Event\PrintResultEvent; use Codeception\Events; use Codeception\Extension; use Codeception\Test\Descriptor; /** * Saves failed tests into tests/_output/failed in order to rerun failed tests. * * To rerun failed tests just run the `failed` group: * * ``` * php codecept run -g failed * ``` * * To change failed group name add: * ``` * --override "extensions: config: Codeception\Extension\RunFailed: fail-group: another_group1" * ``` * Remember: if you run tests and they generated custom-named fail group, to run this group, you should add override too * * Starting from Codeception 2.1 **this extension is enabled by default**. * * ``` yaml * extensions: * enabled: [Codeception\Extension\RunFailed] * ``` * * On each execution failed tests are logged and saved into `tests/_output/failed` file. */ class RunFailed extends Extension { public static $events = [ Events::RESULT_PRINT_AFTER => 'saveFailed' ]; /** @var string filename/groupname for failed tests */ protected $group = 'failed'; public function _initialize() { if (array_key_exists('fail-group', $this->config) && $this->config['fail-group']) { $this->group = $this->config['fail-group']; } $logPath = str_replace($this->getRootDir(), '', $this->getLogDir()); // get local path to logs $this->_reconfigure(['groups' => [$this->group => $logPath . $this->group]]); } public function saveFailed(PrintResultEvent $e) { $file = $this->getLogDir() . $this->group; $result = $e->getResult(); if ($result->wasSuccessful()) { if (is_file($file)) { unlink($file); } return; } $output = []; foreach ($result->failures() as $fail) { $output[] = $this->localizePath(Descriptor::getTestFullName($fail->failedTest())); } foreach ($result->errors() as $fail) { $output[] = $this->localizePath(Descriptor::getTestFullName($fail->failedTest())); } file_put_contents($file, implode("\n", $output)); } protected function localizePath($path) { $root = realpath($this->getRootDir()) . DIRECTORY_SEPARATOR; if (substr($path, 0, strlen($root)) == $root) { return substr($path, strlen($root)); } return $path; } } codeception/ext/SimpleReporter.php000077700000003223151323602320013331 0ustar00<?php namespace Codeception\Extension; use Codeception\Event\TestEvent; use Codeception\Events; use Codeception\Extension; use Codeception\Test\Descriptor; /** * This extension demonstrates how you can implement console output of your own. * Recommended to be used for development purposes only. */ class SimpleReporter extends Extension { public function _initialize() { $this->options['silent'] = false; // turn on printing for this extension $this->_reconfigure(['settings' => ['silent' => true]]); // turn off printing for everything else } // we are listening for events public static $events = [ Events::SUITE_BEFORE => 'beforeSuite', Events::TEST_END => 'after', Events::TEST_SUCCESS => 'success', Events::TEST_FAIL => 'fail', Events::TEST_ERROR => 'error', ]; public function beforeSuite() { $this->writeln(""); } public function success() { $this->write('[+] '); } public function fail() { $this->write('[-] '); } public function error() { $this->write('[E] '); } // we are printing test status and time taken public function after(TestEvent $e) { $seconds_input = $e->getTime(); // stack overflow: http://stackoverflow.com/questions/16825240/how-to-convert-microtime-to-hhmmssuu $seconds = (int)($milliseconds = (int)($seconds_input * 1000)) / 1000; $time = ($seconds % 60) . (($milliseconds === 0) ? '' : '.' . $milliseconds); $this->write(Descriptor::getTestSignature($e->getTest())); $this->writeln(' (' . $time . 's)'); } } codeception/ext/.htaccess000077700000000177151323602320011447 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/ext/RunBefore.php000077700000007625151323602320012256 0ustar00<?php namespace Codeception\Extension; use Codeception\Events; use Codeception\Exception\ExtensionException; use Codeception\Extension; use Symfony\Component\Process\Process; /** * Extension for execution of some processes before running tests. * * Processes can be independent and dependent. * Independent processes run independently of each other. * Dependent processes run sequentially one by one. * * Can be configured in suite config: * * ```yaml * # acceptance.suite.yml * extensions: * enabled: * - Codeception\Extension\RunBefore: * - independent_process_1 * - * - dependent_process_1_1 * - dependent_process_1_2 * - independent_process_2 * - * - dependent_process_2_1 * - dependent_process_2_2 * ``` * * HINT: you can use different configurations per environment. */ class RunBefore extends Extension { protected $config = []; protected static $events = [ Events::SUITE_BEFORE => 'runBefore' ]; /** @var array[] */ private $processes = []; public function _initialize() { if (!class_exists('Symfony\Component\Process\Process')) { throw new ExtensionException($this, 'symfony/process package is required'); } } public function runBefore() { $this->runProcesses(); $this->processMonitoring(); } private function runProcesses() { foreach ($this->config as $item) { if (is_array($item)) { $currentCommand = array_shift($item); $followingCommands = $item; } else { $currentCommand = $item; $followingCommands = []; } $process = $this->runProcess($currentCommand); $this->addProcessToMonitoring($process, $followingCommands); } } /** * @param string $command * @return Process */ private function runProcess($command) { $this->output->debug('[RunBefore] Starting ' . $command); if (method_exists(Process::class, 'fromShellCommandline')) { //Symfony 4.2+ $process = Process::fromShellCommandline($command, $this->getRootDir()); } else { $process = new Process($command, $this->getRootDir()); } $process->start(); return $process; } /** * @param string[] $followingCommands */ private function addProcessToMonitoring(Process $process, array $followingCommands) { $this->processes[] = [ 'instance' => $process, 'following' => $followingCommands ]; } /** * @param int $index */ private function removeProcessFromMonitoring($index) { unset($this->processes[$index]); } private function processMonitoring() { while (count($this->processes) !== 0) { $this->checkProcesses(); sleep(1); } } private function checkProcesses() { foreach ($this->processes as $index => $process) { if (!$this->isRunning($process['instance'])) { $this->output->debug('[RunBefore] Completing ' . $process['instance']->getCommandLine()); $this->runFollowingCommand($process['following']); $this->removeProcessFromMonitoring($index); } } } /** * @param string[] $followingCommands */ private function runFollowingCommand(array $followingCommands) { if (count($followingCommands) > 0) { $process = $this->runProcess(array_shift($followingCommands)); $this->addProcessToMonitoring($process, $followingCommands); } } private function isRunning(Process $process) { if ($process->isRunning()) { return true; } return false; } } codeception/ext/DotReporter.php000077700000005037151323602320012633 0ustar00<?php namespace Codeception\Extension; use Codeception\Event\FailEvent; use Codeception\Events; use Codeception\Extension; use Codeception\Subscriber\Console; /** * DotReporter provides less verbose output for test execution. * Like PHPUnit printer it prints dots "." for successful testes and "F" for failures. * *  * * ```bash * .......... * .......... * .......... * .......... * .......... * .......... * .......... * .......... * * Time: 2.07 seconds, Memory: 20.00MB * * OK (80 tests, 124 assertions) * ``` * * * Enable this reporter with `--ext option` * * ``` * codecept run --ext DotReporter * ``` * * Failures and Errors are printed by a standard Codeception reporter. * Use this extension as an example for building custom reporters. */ class DotReporter extends Extension { /** * @var Console */ protected $standardReporter; protected $errors = []; protected $failures = []; protected $width = 10; protected $currentPos = 0; public function _initialize() { $this->options['silent'] = false; // turn on printing for this extension $this->_reconfigure(['settings' => ['silent' => true]]); // turn off printing for everything else $this->standardReporter = new Console($this->options); $this->width = $this->standardReporter->detectWidth(); } // we are listening for events public static $events = [ Events::SUITE_BEFORE => 'beforeSuite', Events::TEST_SUCCESS => 'success', Events::TEST_FAIL => 'fail', Events::TEST_ERROR => 'error', Events::TEST_SKIPPED => 'skipped', Events::TEST_FAIL_PRINT => 'printFailed' ]; public function beforeSuite() { $this->writeln(""); } public function success() { $this->printChar('.'); } public function fail(FailEvent $e) { $this->printChar("<error>F</error>"); } public function error(FailEvent $e) { $this->printChar('<error>E</error>'); } public function skipped() { $this->printChar('S'); } protected function printChar($char) { if ($this->currentPos >= $this->width) { $this->writeln(''); $this->currentPos = 0; } $this->write($char); $this->currentPos++; } public function printFailed(FailEvent $event) { $this->standardReporter->printFail($event); } } codeception/composer.json000077700000004514151323602320011572 0ustar00{ "name":"codeception/codeception", "description":"BDD-style testing framework", "keywords":["BDD", "acceptance testing", "functional testing", "unit testing", "tdd"], "homepage":"http://codeception.com/", "type":"library", "license":"MIT", "authors":[ { "name":"Michael Bodnarchuk", "email":"davert@mail.ua", "homepage":"http://codegyre.com" } ], "minimum-stability": "RC", "require": { "php": ">=5.6.0 <9.0", "ext-curl": "*", "ext-json": "*", "ext-mbstring": "*", "codeception/lib-asserts": "^1.0", "guzzlehttp/psr7": "~1.4", "symfony/finder": ">=2.7 <6.0", "symfony/console": ">=2.7 <6.0", "symfony/event-dispatcher": ">=2.7 <6.0", "symfony/yaml": ">=2.7 <6.0", "symfony/css-selector": ">=2.7 <6.0", "behat/gherkin": "^4.4.0", "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.1.1 | ^9.0", "codeception/stub": "^2.0 | ^3.0" }, "require-dev": { "monolog/monolog": "~1.8", "codeception/specify": "~0.3", "squizlabs/php_codesniffer": "~2.0", "vlucas/phpdotenv": "^2.0 | ^3.0 | ^4.0 | ^5.0", "symfony/process": ">=2.7 <6.0", "codeception/module-asserts": "*@dev", "codeception/module-cli": "*@dev", "codeception/module-db": "*@dev", "codeception/module-filesystem": "*@dev", "codeception/module-phpbrowser": "*@dev", "codeception/util-universalframework": "*@dev" }, "suggest": { "hoa/console": "For interactive console functionality", "codeception/specify": "BDD-style code blocks", "codeception/verify": "BDD-style assertions", "symfony/phpunit-bridge": "For phpunit-bridge support", "stecman/symfony-console-completion": "For BASH autocompletion" }, "autoload":{ "psr-4":{ "Codeception\\": "src/Codeception", "Codeception\\Extension\\": "ext" } }, "autoload-dev": { "classmap": [ "tests/cli/_steps", "tests/data/DummyClass.php", "tests/data/claypit/tests/_data" ] }, "extra": { "branch-alias": { } }, "bin":["codecept"], "config": { "platform": {} } } codeception/app.php000077700000004233151323602320010337 0ustar00<?php require_once __DIR__ . '/autoload.php'; use Codeception\Application; call_user_func(static function () { $app = new Application('Codeception', Codeception\Codecept::VERSION); $app->add(new Codeception\Command\Build('build')); $app->add(new Codeception\Command\Run('run')); $app->add(new Codeception\Command\Init('init')); $app->add(new Codeception\Command\Console('console')); $app->add(new Codeception\Command\Bootstrap('bootstrap')); $app->add(new Codeception\Command\GenerateCept('generate:cept')); $app->add(new Codeception\Command\GenerateCest('generate:cest')); $app->add(new Codeception\Command\GenerateTest('generate:test')); $app->add(new Codeception\Command\GenerateSuite('generate:suite')); $app->add(new Codeception\Command\GenerateHelper('generate:helper')); $app->add(new Codeception\Command\GenerateScenarios('generate:scenarios')); $app->add(new Codeception\Command\Clean('clean')); $app->add(new Codeception\Command\GenerateGroup('generate:groupobject')); $app->add(new Codeception\Command\GeneratePageObject('generate:pageobject')); $app->add(new Codeception\Command\GenerateStepObject('generate:stepobject')); $app->add(new Codeception\Command\GenerateSnapshot('generate:snapshot')); $app->add(new Codeception\Command\GenerateEnvironment('generate:environment')); $app->add(new Codeception\Command\GenerateFeature('generate:feature')); $app->add(new Codeception\Command\GherkinSnippets('gherkin:snippets')); $app->add(new Codeception\Command\GherkinSteps('gherkin:steps')); $app->add(new Codeception\Command\DryRun('dry-run')); $app->add(new Codeception\Command\ConfigValidate('config:validate')); // Suggests package if (class_exists('Stecman\Component\Symfony\Console\BashCompletion\CompletionCommand')) { $app->add(new Codeception\Command\Completion()); } else { $app->add(new Codeception\Command\CompletionFallback()); } $app->registerCustomCommands(); // add only if within a phar archive. if ('phar:' === substr(__FILE__, 0, 5)) { $app->add(new Codeception\Command\SelfUpdate('self-update')); } $app->run(); }); codeception/.htaccess000077700000000177151323602320010647 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>codeception/shim.php000077700000002064151323602320010517 0ustar00<?php // @codingStandardsIgnoreStart namespace { \Codeception\PHPUnit\Init::init(); } namespace Symfony\Component\CssSelector { if (!class_exists('Symfony\Component\CssSelector\CssSelectorConverter')) { class CssSelectorConverter { function toXPath($cssExpr, $prefix = 'descendant-or-self::') { return CssSelector::toXPath($cssExpr, $prefix); } } } } // prefering old names namespace Codeception\TestCase { class Test extends \Codeception\Test\Unit { } } namespace Codeception\Platform { abstract class Group extends \Codeception\GroupObject { } abstract class Extension extends \Codeception\Extension { } } namespace { class_alias('Codeception\TestInterface', 'Codeception\TestCase'); //Compatibility with Symfony 5 if (!class_exists('Symfony\Component\EventDispatcher\Event') && class_exists('Symfony\Contracts\EventDispatcher\Event')) { class_alias('Symfony\Contracts\EventDispatcher\Event', 'Symfony\Component\EventDispatcher\Event'); } } stub/src/Test/Feature/Stub.php000077700000024231151323602320012256 0ustar00<?php namespace Codeception\Test\Feature; /** * ### Usage in Codeception * * Since Codeception 2.3.8 this trait is enabled in `\Codeception\Test\Unit` class. * * ### Usage in PHPUnit * * Include this trait into a TestCase to be able to use Stubs and Mocks: * * ```php * <?php * class MyTest extends \PHPUnit\Framework\TestCase * { * use Codeception\Test\Feature\Stub; * } * ``` */ trait Stub { private $mocks; protected function stubStart() { if ($this instanceof \PHPUnit\Framework\TestCase) { return; } $this->mocks = []; } protected function stubEnd($status, $time) { if ($this instanceof \PHPUnit\Framework\TestCase) { return; } if ($status !== 'ok') { // Codeception status return; } foreach ($this->mocks as $mockObject) { if ($mockObject->__phpunit_hasMatchers()) { $this->assertTrue(true); // incrementing assertions } $mockObject->__phpunit_verify(true); } } /** * Instantiates a class without executing a constructor. * Properties and methods can be set as a second parameter. * Even protected and private properties can be set. * * ``` php * <?php * $this->make('User'); * $this->make('User', ['name' => 'davert']); * ?> * ``` * * Accepts either name of class or object of that class * * ``` php * <?php * $this->make(new User, ['name' => 'davert']); * ?> * ``` * * To replace method provide it's name as a key in second parameter * and it's return value or callback function as parameter * * ``` php * <?php * $this->make('User', ['save' => function () { return true; }]); * $this->make('User', ['save' => true]); * ``` * @template RealInstanceType of object * @param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType> $class - A class to be mocked * @param array $params - properties and methods to set * * @return \PHPUnit\Framework\MockObject\MockObject&RealInstanceType - mock * @throws \RuntimeException when class does not exist * @throws \Exception */ public function make($class, $params = []) { return $this->mocks[] = \Codeception\Stub::make($class, $params, $this); } /** * Instantiates class having all methods replaced with dummies. * Constructor is not triggered. * Properties and methods can be set as a second parameter. * Even protected and private properties can be set. * * ``` php * <?php * $this->makeEmpty('User'); * $this->makeEmpty('User', ['name' => 'davert']); * ``` * * Accepts either name of class or object of that class * * ``` php * <?php * $this->makeEmpty(new User, ['name' => 'davert']); * ``` * * To replace method provide it's name as a key in second parameter * and it's return value or callback function as parameter * * ``` php * <?php * $this->makeEmpty('User', ['save' => function () { return true; }]); * $this->makeEmpty('User', ['save' => true)); * ``` * * @template RealInstanceType of object * @param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType> $class - A class to be mocked * @param array $params * @param bool|\PHPUnit\Framework\TestCase $testCase * * @return \PHPUnit\Framework\MockObject\MockObject&RealInstanceType * @throws \Exception */ public function makeEmpty($class, $params = []) { return $this->mocks[] = \Codeception\Stub::makeEmpty($class, $params, $this); } /** * Instantiates class having all methods replaced with dummies except one. * Constructor is not triggered. * Properties and methods can be replaced. * Even protected and private properties can be set. * * ``` php * <?php * $this->makeEmptyExcept('User', 'save'); * $this->makeEmptyExcept('User', 'save', ['name' => 'davert']); * ?> * ``` * * Accepts either name of class or object of that class * * ``` php * <?php * * $this->makeEmptyExcept(new User, 'save'); * ?> * ``` * * To replace method provide it's name as a key in second parameter * and it's return value or callback function as parameter * * ``` php * <?php * $this->makeEmptyExcept('User', 'save', ['isValid' => function () { return true; }]); * $this->makeEmptyExcept('User', 'save', ['isValid' => true]); * ``` * * @template RealInstanceType of object * @param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType> $class - A class to be mocked * @param string $method * @param array $params * * @return \PHPUnit\Framework\MockObject\MockObject&RealInstanceType * @throws \Exception */ public function makeEmptyExcept($class, $method, $params = []) { return $this->mocks[] = \Codeception\Stub::makeEmptyExcept($class, $method, $params, $this); } /** * Instantiates a class instance by running constructor. * Parameters for constructor passed as second argument * Properties and methods can be set in third argument. * Even protected and private properties can be set. * * ``` php * <?php * $this->construct('User', ['autosave' => false]); * $this->construct('User', ['autosave' => false], ['name' => 'davert']); * ?> * ``` * * Accepts either name of class or object of that class * * ``` php * <?php * $this->construct(new User, ['autosave' => false), ['name' => 'davert']); * ?> * ``` * * To replace method provide it's name as a key in third parameter * and it's return value or callback function as parameter * * ``` php * <?php * $this->construct('User', [], ['save' => function () { return true; }]); * $this->construct('User', [], ['save' => true]); * ?> * ``` * * @template RealInstanceType of object * @param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType> $class - A class to be mocked * @param array $constructorParams * @param array $params * @param bool|\PHPUnit\Framework\TestCase $testCase * * @return \PHPUnit\Framework\MockObject\MockObject&RealInstanceType * @throws \Exception */ public function construct($class, $constructorParams = [], $params = []) { return $this->mocks[] = \Codeception\Stub::construct($class, $constructorParams, $params, $this); } /** * Instantiates a class instance by running constructor with all methods replaced with dummies. * Parameters for constructor passed as second argument * Properties and methods can be set in third argument. * Even protected and private properties can be set. * * ``` php * <?php * $this->constructEmpty('User', ['autosave' => false]); * $this->constructEmpty('User', ['autosave' => false), ['name' => 'davert']); * ``` * * Accepts either name of class or object of that class * * ``` php * <?php * $this->constructEmpty(new User, ['autosave' => false], ['name' => 'davert']); * ``` * * To replace method provide it's name as a key in third parameter * and it's return value or callback function as parameter * * ``` php * <?php * $this->constructEmpty('User', array(), array('save' => function () { return true; })); * $this->constructEmpty('User', array(), array('save' => true)); * ``` * * **To create a mock, pass current testcase name as last argument:** * * ```php * <?php * $this->constructEmpty('User', [], [ * 'save' => \Codeception\Stub\Expected::once() * ]); * ``` * * @template RealInstanceType of object * @param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType> $class - A class to be mocked * @param array $constructorParams * @param array $params * * @return \PHPUnit\Framework\MockObject\MockObject&RealInstanceType */ public function constructEmpty($class, $constructorParams = [], $params = []) { return $this->mocks[] = \Codeception\Stub::constructEmpty($class, $constructorParams, $params, $this); } /** * Instantiates a class instance by running constructor with all methods replaced with dummies, except one. * Parameters for constructor passed as second argument * Properties and methods can be set in third argument. * Even protected and private properties can be set. * * ``` php * <?php * $this->constructEmptyExcept('User', 'save'); * $this->constructEmptyExcept('User', 'save', ['autosave' => false], ['name' => 'davert']); * ?> * ``` * * Accepts either name of class or object of that class * * ``` php * <?php * $this->constructEmptyExcept(new User, 'save', ['autosave' => false], ['name' => 'davert']); * ?> * ``` * * To replace method provide it's name as a key in third parameter * and it's return value or callback function as parameter * * ``` php * <?php * $this->constructEmptyExcept('User', 'save', [], ['save' => function () { return true; }]); * $this->constructEmptyExcept('User', 'save', [], ['save' => true]); * ?> * ``` * * @template RealInstanceType of object * @param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType> $class - A class to be mocked * @param string $method * @param array $constructorParams * @param array $params * * @return \PHPUnit\Framework\MockObject\MockObject&RealInstanceType */ public function constructEmptyExcept($class, $method, $constructorParams = [], $params = []) { return $this->mocks[] = \Codeception\Stub::constructEmptyExcept($class, $method, $constructorParams, $params, $this); } }stub/src/Test/Feature/.htaccess000077700000000177151323602320012431 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>stub/src/Test/.htaccess000077700000000177151323602320011036 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>stub/src/Stub/Expected.php000077700000010270151323602320011503 0ustar00<?php namespace Codeception\Stub; use PHPUnit\Framework\MockObject\Rule\InvokedAtLeastOnce; use PHPUnit\Framework\MockObject\Rule\InvokedCount; class Expected { /** * Checks if a method never has been invoked * * If method invoked, it will immediately throw an * exception. * * ```php * <?php * use \Codeception\Stub\Expected; * * $user = $this->make('User', [ * 'getName' => Expected::never(), * 'someMethod' => function() {} * ]); * $user->someMethod(); * ?> * ``` * * @param mixed $params * @return StubMarshaler */ public static function never($params = null) { return new StubMarshaler( new InvokedCount(0), self::closureIfNull($params) ); } /** * Checks if a method has been invoked exactly one * time. * * If the number is less or greater it will later be checked in verify() and also throw an * exception. * * ```php * <?php * use \Codeception\Stub\Expected; * * $user = $this->make( * 'User', * array( * 'getName' => Expected::once('Davert'), * 'someMethod' => function() {} * ) * ); * $userName = $user->getName(); * $this->assertEquals('Davert', $userName); * ?> * ``` * Alternatively, a function can be passed as parameter: * * ```php * <?php * Expected::once(function() { return Faker::name(); }); * ``` * * @param mixed $params * * @return StubMarshaler */ public static function once($params = null) { return new StubMarshaler( new InvokedCount(1), self::closureIfNull($params) ); } /** * Checks if a method has been invoked at least one * time. * * If the number of invocations is 0 it will throw an exception in verify. * * ```php * <?php * use \Codeception\Stub\Expected; * * $user = $this->make( * 'User', * array( * 'getName' => Expected::atLeastOnce('Davert')), * 'someMethod' => function() {} * ) * ); * $user->getName(); * $userName = $user->getName(); * $this->assertEquals('Davert', $userName); * ?> * ``` * * Alternatively, a function can be passed as parameter: * * ```php * <?php * Expected::atLeastOnce(function() { return Faker::name(); }); * ``` * * @param mixed $params * * @return StubMarshaler */ public static function atLeastOnce($params = null) { return new StubMarshaler( new InvokedAtLeastOnce(), self::closureIfNull($params) ); } /** * Checks if a method has been invoked a certain amount * of times. * If the number of invocations exceeds the value it will immediately throw an * exception, * If the number is less it will later be checked in verify() and also throw an * exception. * * ``` php * <?php * use \Codeception\Stub; * use \Codeception\Stub\Expected; * * $user = $this->make( * 'User', * array( * 'getName' => Expected::exactly(3, 'Davert'), * 'someMethod' => function() {} * ) * ); * $user->getName(); * $user->getName(); * $userName = $user->getName(); * $this->assertEquals('Davert', $userName); * ?> * ``` * Alternatively, a function can be passed as parameter: * * ```php * <?php * Expected::exactly(function() { return Faker::name() }); * ``` * * @param int $count * @param mixed $params * * @return StubMarshaler */ public static function exactly($count, $params = null) { return new StubMarshaler( new InvokedCount($count), self::closureIfNull($params) ); } private static function closureIfNull($params) { if ($params instanceof \Closure) { return $params; } return function() use ($params) { return $params; }; } }stub/src/Stub/ConsecutiveMap.php000077700000000525151323602320012671 0ustar00<?php namespace Codeception\Stub; /** * Holds matcher and value of mocked method */ class ConsecutiveMap { private $consecutiveMap = []; public function __construct(array $consecutiveMap) { $this->consecutiveMap = $consecutiveMap; } public function getMap() { return $this->consecutiveMap; } }stub/src/Stub/StubMarshaler.php000077700000001036151323602320012516 0ustar00<?php namespace Codeception\Stub; use PHPUnit\Framework\MockObject\Rule\InvocationOrder; /** * Holds matcher and value of mocked method */ class StubMarshaler { private $methodMatcher; private $methodValue; public function __construct(InvocationOrder $matcher, $value) { $this->methodMatcher = $matcher; $this->methodValue = $value; } public function getMatcher() { return $this->methodMatcher; } public function getValue() { return $this->methodValue; } } stub/src/Stub/.htaccess000077700000000177151323602320011034 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>stub/src/Stub.php000077700000053520151323602320007747 0ustar00<?php namespace Codeception; use Codeception\Stub\ConsecutiveMap; use Codeception\Stub\StubMarshaler; use PHPUnit\Framework\MockObject\Generator; use PHPUnit\Framework\MockObject\Rule\AnyInvokedCount; use PHPUnit\Framework\MockObject\Stub\ConsecutiveCalls; use PHPUnit\Framework\MockObject\Stub\ReturnCallback; use PHPUnit\Framework\MockObject\Stub\ReturnStub; class Stub { public static $magicMethods = ['__isset', '__get', '__set']; /** * Instantiates a class without executing a constructor. * Properties and methods can be set as a second parameter. * Even protected and private properties can be set. * * ``` php * <?php * Stub::make('User'); * Stub::make('User', ['name' => 'davert']); * ?> * ``` * * Accepts either name of class or object of that class * * ``` php * <?php * Stub::make(new User, ['name' => 'davert']); * ?> * ``` * * To replace method provide it's name as a key in second parameter * and it's return value or callback function as parameter * * ``` php * <?php * Stub::make('User', ['save' => function () { return true; }]); * Stub::make('User', ['save' => true]); * ?> * ``` * * **To create a mock, pass current testcase name as last argument:** * * ```php * <?php * Stub::make('User', [ * 'save' => \Codeception\Stub\Expected::once() * ], $this); * ``` * * @template RealInstanceType of object * @param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType> $class - A class to be mocked * @param array $params - properties and methods to set * @param bool|\PHPUnit\Framework\TestCase $testCase * * @return \PHPUnit\Framework\MockObject\MockObject&RealInstanceType - mock * @throws \RuntimeException when class does not exist * @throws \Exception */ public static function make($class, $params = [], $testCase = false) { $class = self::getClassname($class); if (!class_exists($class)) { if (interface_exists($class)) { throw new \RuntimeException("Stub::make can't mock interfaces, please use Stub::makeEmpty instead."); } throw new \RuntimeException("Stubbed class $class doesn't exist."); } $reflection = new \ReflectionClass($class); $callables = self::getMethodsToReplace($reflection, $params); if ($reflection->isAbstract()) { $arguments = empty($callables) ? [] : array_keys($callables); $mock = self::generateMockForAbstractClass($class, $arguments, '', false, $testCase); } else { $arguments = empty($callables) ? null : array_keys($callables); $mock = self::generateMock($class, $arguments, [], '', false, $testCase); } self::bindParameters($mock, $params); return self::markAsMock($mock, $reflection); } /** * Set __mock flag, if at all possible * * @param object $mock * @param \ReflectionClass $reflection * @return object */ private static function markAsMock($mock, \ReflectionClass $reflection) { if (!$reflection->hasMethod('__set')) { $mock->__mocked = $reflection->getName(); } return $mock; } /** * Creates $num instances of class through `Stub::make`. * * @param mixed $class * @param int $num * @param array $params * * @return array * @throws \Exception */ public static function factory($class, $num = 1, $params = []) { $objects = []; for ($i = 0; $i < $num; $i++) { $objects[] = self::make($class, $params); } return $objects; } /** * Instantiates class having all methods replaced with dummies except one. * Constructor is not triggered. * Properties and methods can be replaced. * Even protected and private properties can be set. * * ``` php * <?php * Stub::makeEmptyExcept('User', 'save'); * Stub::makeEmptyExcept('User', 'save', ['name' => 'davert']); * ?> * ``` * * Accepts either name of class or object of that class * * ``` php * <?php * * Stub::makeEmptyExcept(new User, 'save'); * ?> * ``` * * To replace method provide it's name as a key in second parameter * and it's return value or callback function as parameter * * ``` php * <?php * Stub::makeEmptyExcept('User', 'save', ['isValid' => function () { return true; }]); * Stub::makeEmptyExcept('User', 'save', ['isValid' => true]); * ?> * ``` * * **To create a mock, pass current testcase name as last argument:** * * ```php * <?php * Stub::makeEmptyExcept('User', 'validate', [ * 'save' => \Codeception\Stub\Expected::once() * ], $this); * ``` * @template * @param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType> $class - A class to be mocked * @param string $method * @param array $params * @param bool|\PHPUnit\Framework\TestCase $testCase * * @return \PHPUnit\Framework\MockObject\MockObject&RealInstanceType * @throws \Exception */ public static function makeEmptyExcept($class, $method, $params = [], $testCase = false) { $class = self::getClassname($class); $reflectionClass = new \ReflectionClass($class); $methods = $reflectionClass->getMethods(); $methods = array_filter( $methods, function ($m) { return !in_array($m->name, Stub::$magicMethods); } ); $methods = array_filter( $methods, function ($m) use ($method) { return $method != $m->name; } ); $methods = array_map( function ($m) { return $m->name; }, $methods ); $methods = count($methods) ? $methods : null; $mock = self::generateMock($class, $methods, [], '', false, $testCase); self::bindParameters($mock, $params); return self::markAsMock($mock, $reflectionClass); } /** * Instantiates class having all methods replaced with dummies. * Constructor is not triggered. * Properties and methods can be set as a second parameter. * Even protected and private properties can be set. * * ``` php * <?php * Stub::makeEmpty('User'); * Stub::makeEmpty('User', ['name' => 'davert']); * ``` * * Accepts either name of class or object of that class * * ``` php * <?php * Stub::makeEmpty(new User, ['name' => 'davert']); * ``` * * To replace method provide it's name as a key in second parameter * and it's return value or callback function as parameter * * ``` php * <?php * Stub::makeEmpty('User', ['save' => function () { return true; }]); * Stub::makeEmpty('User', ['save' => true]); * ``` * * **To create a mock, pass current testcase name as last argument:** * * ```php * <?php * Stub::makeEmpty('User', [ * 'save' => \Codeception\Stub\Expected::once() * ], $this); * ``` * * @template RealInstanceType of object * @param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType> $class - A class to be mocked * @param array $params * @param bool|\PHPUnit\Framework\TestCase $testCase * * @return \PHPUnit\Framework\MockObject\MockObject&RealInstanceType * @throws \Exception */ public static function makeEmpty($class, $params = [], $testCase = false) { $class = self::getClassname($class); $reflection = new \ReflectionClass($class); $methods = get_class_methods($class); $methods = array_filter( $methods, function ($i) { return !in_array($i, Stub::$magicMethods); } ); $mock = self::generateMock($class, $methods, [], '', false, $testCase); self::bindParameters($mock, $params); return self::markAsMock($mock, $reflection); } /** * Clones an object and redefines it's properties (even protected and private) * * @param $obj * @param array $params * * @return mixed * @throws \Exception */ public static function copy($obj, $params = []) { $copy = clone($obj); self::bindParameters($copy, $params); return $copy; } /** * Instantiates a class instance by running constructor. * Parameters for constructor passed as second argument * Properties and methods can be set in third argument. * Even protected and private properties can be set. * * ``` php * <?php * Stub::construct('User', ['autosave' => false]); * Stub::construct('User', ['autosave' => false], ['name' => 'davert']); * ?> * ``` * * Accepts either name of class or object of that class * * ``` php * <?php * Stub::construct(new User, ['autosave' => false], ['name' => 'davert']); * ?> * ``` * * To replace method provide it's name as a key in third parameter * and it's return value or callback function as parameter * * ``` php * <?php * Stub::construct('User', [], ['save' => function () { return true; }]); * Stub::construct('User', [], ['save' => true]); * ?> * ``` * * **To create a mock, pass current testcase name as last argument:** * * ```php * <?php * Stub::construct('User', [], [ * 'save' => \Codeception\Stub\Expected::once() * ], $this); * ``` * * @template RealInstanceType of object * @param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType> $class - A class to be mocked * @param array $constructorParams * @param array $params * @param bool|\PHPUnit\Framework\TestCase $testCase * * @return \PHPUnit\Framework\MockObject\MockObject&RealInstanceType * @throws \Exception */ public static function construct($class, $constructorParams = [], $params = [], $testCase = false) { $class = self::getClassname($class); $reflection = new \ReflectionClass($class); $callables = self::getMethodsToReplace($reflection, $params); $arguments = empty($callables) ? null : array_keys($callables); $mock = self::generateMock($class, $arguments, $constructorParams, $testCase); self::bindParameters($mock, $params); return self::markAsMock($mock, $reflection); } /** * Instantiates a class instance by running constructor with all methods replaced with dummies. * Parameters for constructor passed as second argument * Properties and methods can be set in third argument. * Even protected and private properties can be set. * * ``` php * <?php * Stub::constructEmpty('User', ['autosave' => false]); * Stub::constructEmpty('User', ['autosave' => false], ['name' => 'davert']); * ``` * * Accepts either name of class or object of that class * * ``` php * <?php * Stub::constructEmpty(new User, ['autosave' => false], ['name' => 'davert']); * ``` * * To replace method provide it's name as a key in third parameter * and it's return value or callback function as parameter * * ``` php * <?php * Stub::constructEmpty('User', [], ['save' => function () { return true; }]); * Stub::constructEmpty('User', [], ['save' => true]); * ``` * * **To create a mock, pass current testcase name as last argument:** * * ```php * <?php * Stub::constructEmpty('User', [], [ * 'save' => \Codeception\Stub\Expected::once() * ], $this); * ``` * * @template RealInstanceType of object * @param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType> $class - A class to be mocked * @param array $constructorParams * @param array $params * @param bool|\PHPUnit\Framework\TestCase $testCase * * @return \PHPUnit\Framework\MockObject\MockObject&RealInstanceType */ public static function constructEmpty($class, $constructorParams = [], $params = [], $testCase = false) { $class = self::getClassname($class); $reflection = new \ReflectionClass($class); $methods = get_class_methods($class); $methods = array_filter( $methods, function ($i) { return !in_array($i, Stub::$magicMethods); } ); $mock = self::generateMock($class, $methods, $constructorParams, $testCase); self::bindParameters($mock, $params); return self::markAsMock($mock, $reflection); } /** * Instantiates a class instance by running constructor with all methods replaced with dummies, except one. * Parameters for constructor passed as second argument * Properties and methods can be set in third argument. * Even protected and private properties can be set. * * ``` php * <?php * Stub::constructEmptyExcept('User', 'save'); * Stub::constructEmptyExcept('User', 'save', ['autosave' => false], ['name' => 'davert']); * ?> * ``` * * Accepts either name of class or object of that class * * ``` php * <?php * Stub::constructEmptyExcept(new User, 'save', ['autosave' => false], ['name' => 'davert']); * ?> * ``` * * To replace method provide it's name as a key in third parameter * and it's return value or callback function as parameter * * ``` php * <?php * Stub::constructEmptyExcept('User', 'save', [], ['save' => function () { return true; }]); * Stub::constructEmptyExcept('User', 'save', [], ['save' => true]); * ?> * ``` * * **To create a mock, pass current testcase name as last argument:** * * ```php * <?php * Stub::constructEmptyExcept('User', 'save', [], [ * 'save' => \Codeception\Stub\Expected::once() * ], $this); * ``` * * @template RealInstanceType of object * @param class-string<RealInstanceType>|RealInstanceType|callable(): class-string<RealInstanceType> $class - A class to be mocked * @param string $method * @param array $constructorParams * @param array $params * @param bool|\PHPUnit\Framework\TestCase $testCase * * @return \PHPUnit\Framework\MockObject\MockObject&RealInstanceType */ public static function constructEmptyExcept( $class, $method, $constructorParams = [], $params = [], $testCase = false ) { $class = self::getClassname($class); $reflectionClass = new \ReflectionClass($class); $methods = $reflectionClass->getMethods(); $methods = array_filter( $methods, function ($m) { return !in_array($m->name, Stub::$magicMethods); } ); $methods = array_filter( $methods, function ($m) use ($method) { return $method != $m->name; } ); $methods = array_map( function ($m) { return $m->name; }, $methods ); $methods = count($methods) ? $methods : null; $mock = self::generateMock($class, $methods, $constructorParams, $testCase); self::bindParameters($mock, $params); return self::markAsMock($mock, $reflectionClass); } private static function generateMock() { return self::doGenerateMock(func_get_args()); } /** * Returns a mock object for the specified abstract class with all abstract * methods of the class mocked. Concrete methods to mock can be specified with * the last parameter * * @return object * @since Method available since Release 1.0.0 */ private static function generateMockForAbstractClass() { return self::doGenerateMock(func_get_args(), true); } private static function doGenerateMock($args, $isAbstract = false) { $testCase = self::extractTestCaseFromArgs($args); $methodName = $isAbstract ? 'getMockForAbstractClass' : 'getMock'; $generatorClass = new Generator; // using PHPUnit 5.4 mocks registration if (version_compare(\PHPUnit\Runner\Version::series(), '5.4', '>=') && $testCase instanceof \PHPUnit\Framework\TestCase ) { $mock = call_user_func_array([$generatorClass, $methodName], $args); $testCase->registerMockObject($mock); return $mock; } if ($testCase instanceof \PHPUnit\Framework\TestCase) { $generatorClass = $testCase; } return call_user_func_array([$generatorClass, $methodName], $args); } private static function extractTestCaseFromArgs(&$args) { $argsLength = count($args) - 1; $testCase = $args[$argsLength]; unset($args[$argsLength]); return $testCase; } /** * Replaces properties of current stub * * @param \PHPUnit\Framework\MockObject\MockObject $mock * @param array $params * * @return mixed * @throws \LogicException */ public static function update($mock, array $params) { //do not rely on __mocked property, check typ eof $mock if (!$mock instanceof \PHPUnit\Framework\MockObject\MockObject) { throw new \LogicException('You can update only stubbed objects'); } self::bindParameters($mock, $params); return $mock; } /** * @param \PHPUnit\Framework\MockObject\MockObject $mock * @param array $params * @throws \LogicException */ protected static function bindParameters($mock, $params) { $reflectionClass = new \ReflectionClass($mock); if ($mock instanceof \PHPUnit\Framework\MockObject\MockObject) { $parentClass = $reflectionClass->getParentClass(); if ($parentClass !== false) { $reflectionClass = $reflectionClass->getParentClass(); } } foreach ($params as $param => $value) { // redefine method if ($reflectionClass->hasMethod($param)) { if ($value instanceof StubMarshaler) { $marshaler = $value; $mock ->expects($marshaler->getMatcher()) ->method($param) ->will(new ReturnCallback($marshaler->getValue())); } elseif ($value instanceof \Closure) { $mock ->expects(new AnyInvokedCount) ->method($param) ->will(new ReturnCallback($value)); } elseif ($value instanceof ConsecutiveMap) { $consecutiveMap = $value; $mock ->expects(new AnyInvokedCount) ->method($param) ->will(new ConsecutiveCalls($consecutiveMap->getMap())); } else { $mock ->expects(new AnyInvokedCount) ->method($param) ->will(new ReturnStub($value)); } } elseif ($reflectionClass->hasProperty($param)) { $reflectionProperty = $reflectionClass->getProperty($param); $reflectionProperty->setAccessible(true); $reflectionProperty->setValue($mock, $value); continue; } else { if ($reflectionClass->hasMethod('__set')) { try { $mock->{$param} = $value; } catch (\Exception $e) { throw new \LogicException( sprintf( 'Could not add property %1$s, class %2$s implements __set method, ' . 'and no %1$s property exists', $param, $reflectionClass->getName() ), $e->getCode(), $e ); } } else { $mock->{$param} = $value; } continue; } } } /** * @todo should be simplified */ protected static function getClassname($object) { if (is_object($object)) { return get_class($object); } if (is_callable($object)) { return call_user_func($object); } return $object; } /** * @param \ReflectionClass $reflection * @param $params * @return array */ protected static function getMethodsToReplace(\ReflectionClass $reflection, $params) { $callables = []; foreach ($params as $method => $value) { if ($reflection->hasMethod($method)) { $callables[$method] = $value; } } return $callables; } /** * Stubbing a method call to return a list of values in the specified order. * * ``` php * <?php * $user = Stub::make('User', ['getName' => Stub::consecutive('david', 'emma', 'sam', 'amy')]); * $user->getName(); //david * $user->getName(); //emma * $user->getName(); //sam * $user->getName(); //amy * ?> * ``` * * @return ConsecutiveMap */ public static function consecutive() { return new ConsecutiveMap(func_get_args()); } } stub/src/.htaccess000077700000000177151323602320010117 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>stub/.gitignore000077700000000035151323602320007513 0ustar00.idea/ /vendor/ composer.lockstub/RoboFile.php000077700000003064151323602320007742 0ustar00<?php require_once 'vendor/autoload.php'; /** * This is project's console commands configuration for Robo task runner. * * @see http://robo.li/ */ class RoboFile extends \Robo\Tasks { protected $docs = [ 'docs/Stub.md' => 'Codeception\Stub', 'docs/Expected.md' => 'Codeception\Stub\Expected', 'docs/StubTrait.md' => 'Codeception\Test\Feature\Stub', ]; public function docs() { foreach ($this->docs as $file => $class) { if (!class_exists($class, true) && !trait_exists($class, true)) { throw new Exception('ups'); } $this->say("Here goes, $class"); $this->taskGenDoc($file) ->docClass($class) ->filterMethods(function(\ReflectionMethod $method) { if ($method->isConstructor() or $method->isDestructor()) return false; if (!$method->isPublic()) return false; if (strpos($method->name, '_') === 0) return false; if (strpos($method->name, 'stub') === 0) return false; return true; }) ->processMethodDocBlock( function (\ReflectionMethod $m, $doc) { $doc = str_replace(array('@since'), array(' * available since version'), $doc); $doc = str_replace(array(' @', "\n@"), array(" * ", "\n * "), $doc); return $doc; }) ->processProperty(false) ->run(); } } }stub/.travis.yml000077700000000320151323602320007631 0ustar00language: php php: - 7.2 - 7.3 - 7.4 cache: directories: - vendor - $HOME/.composer/cache sudo: false before_install: - composer update --prefer-source script: vendor/bin/phpunit tests stub/Readme.md000077700000006071151323602320007250 0ustar00# Codeception\Stub [](https://travis-ci.org/Codeception/Stub) [](https://packagist.org/packages/codeception/stub) [](https://packagist.org/packages/codeception/stub) [](https://packagist.org/packages/codeception/stub) Library on top of PHPUnit's mock builder providing a highly simplified syntax: ## Reference * [Stub](https://github.com/Codeception/Stub/blob/master/docs/Stub.md) - creating stub classes using static methods * [Stub Trait](https://github.com/Codeception/Stub/blob/master/docs/StubTrait.md) - creating stubs and mocks using trait * [Expected](https://github.com/Codeception/Stub/blob/master/docs/Expected.md) - defining expectations for mocks ## Install Enabled by default in Codeception. For PHPUnit install this package: ``` composer require codeception/stub --dev ``` ## Stubs Stubs can be constructed with `Codeception\Stub` static calls: ```php <?php // create a stub with find method replaced $userRepository = Stub::make(UserRepository::class, ['find' => new User]); $userRepository->find(1); // => User // create a dummy $userRepository = Stub::makeEmpty(UserRepository::class); // create a stub with all methods replaced except one $user = Stub::makeEmptyExcept(User::class, 'validate'); $user->validate($data); // create a stub by calling constructor and replacing a method $user = Stub::construct(User::class, ['name' => 'davert'], ['save' => false]); // create a stub by calling constructor with empty methods $user = Stub::constructEmpty(User::class, ['name' => 'davert']); // create a stub by calling constructor with empty methods $user = Stub::constructEmptyExcept(User::class, 'getName', ['name' => 'davert']); $user->getName(); // => davert $user->setName('jane'); // => this method is empty $user->getName(); // => davert ``` [See complete reference](https://github.com/Codeception/Stub/blob/master/docs/Stub.md) Alternatively, stubs can be created by using [`Codeception\Test\Feature\Stub` trait](https://github.com/Codeception/Stub/blob/master/docs/StubTrait.md): ```php <?php $this->make(UserRepositry::class); $this->makeEmpty(UserRepositry::class); $this->construct(UserRepositry::class); $this->constructEmpty(UserRepositry::class); // ... ``` ## Mocks Mocks should be created by including [`Codeception\Test\Feature\Stub` trait](https://github.com/Codeception/Stub/blob/master/docs/StubTrait.md) into a test case. Execution expectation are set with [`Codeception\Stub\Expected`](https://github.com/Codeception/Stub/blob/master/docs/Expected.md): ```php <?php // find should be never called $userRepository = $this->make(UserRepository::class, [ 'find' => Codeception\Stub\Expected::never() ]); // find should be called once and return a new user $userRepository = $this->make(UserRepository::class, [ 'find' => Codeception\Stub\Expected::once(new User) ]); ``` ## License MIT stub/composer.json000077700000000523151323602320010247 0ustar00{ "name": "codeception/stub", "description":"Flexible Stub wrapper for PHPUnit's Mock Builder", "type": "library", "license":"MIT", "minimum-stability": "stable", "autoload": { "psr-4": { "Codeception\\": "src/" } }, "require": { "phpunit/phpunit": "^8.4 | ^9.0" } } stub/docs/Expected.md000077700000004741151323602320010546 0ustar00 ## Codeception\Stub\Expected #### *public static* never($params = null) Checks if a method never has been invoked If method invoked, it will immediately throw an exception. ```php <?php use \Codeception\Stub\Expected; $user = $this->make('User', [ 'getName' => Expected::never(), 'someMethod' => function() {} ]); $user->someMethod(); ?> ``` * `param mixed` $params * return StubMarshaler #### *public static* once($params = null) Checks if a method has been invoked exactly one time. If the number is less or greater it will later be checked in verify() and also throw an exception. ```php <?php use \Codeception\Stub\Expected; $user = $this->make( 'User', array( 'getName' => Expected::once('Davert'), 'someMethod' => function() {} ) ); $userName = $user->getName(); $this->assertEquals('Davert', $userName); ?> ``` Alternatively, a function can be passed as parameter: ```php <?php Expected::once(function() { return Faker::name(); }); ``` * `param mixed` $params * return StubMarshaler #### *public static* atLeastOnce($params = null) Checks if a method has been invoked at least one time. If the number of invocations is 0 it will throw an exception in verify. ```php <?php use \Codeception\Stub\Expected; $user = $this->make( 'User', array( 'getName' => Expected::atLeastOnce('Davert')), 'someMethod' => function() {} ) ); $user->getName(); $userName = $user->getName(); $this->assertEquals('Davert', $userName); ?> ``` Alternatively, a function can be passed as parameter: ```php <?php Expected::atLeastOnce(function() { return Faker::name(); }); ``` * `param mixed` $params * return StubMarshaler #### *public static* exactly($count, $params = null) Checks if a method has been invoked a certain amount of times. If the number of invocations exceeds the value it will immediately throw an exception, If the number is less it will later be checked in verify() and also throw an exception. ``` php <?php use \Codeception\Stub; use \Codeception\Stub\Expected; $user = $this->make( 'User', array( 'getName' => Expected::exactly(3, 'Davert'), 'someMethod' => function() {} ) ); $user->getName(); $user->getName(); $userName = $user->getName(); $this->assertEquals('Davert', $userName); ?> ``` Alternatively, a function can be passed as parameter: ```php <?php Expected::exactly(function() { return Faker::name() }); ``` * `param int` $count * `param mixed` $params * return StubMarshaler stub/docs/StubTrait.md000077700000014245151323602320010726 0ustar00 ## Codeception\Test\Feature\Stub ### Usage in Codeception Since Codeception 2.3.8 this trait is enabled in `\Codeception\Test\Unit` class. ### Usage in PHPUnit Include this trait into a TestCase to be able to use Stubs and Mocks: ```php <?php class MyTest extends \PHPUnit\Framework\TestCase { use Codeception\Test\Feature\Stub; } ``` #### *public* make($class, $params = null) Instantiates a class without executing a constructor. Properties and methods can be set as a second parameter. Even protected and private properties can be set. ``` php <?php $this->make('User'); $this->make('User', ['name' => 'davert']); ?> ``` Accepts either name of class or object of that class ``` php <?php $this->make(new User, ['name' => 'davert']); ?> ``` To replace method provide it's name as a key in second parameter and it's return value or callback function as parameter ``` php <?php $this->make('User', ['save' => function () { return true; }]); $this->make('User', ['save' => true]); ``` * `param mixed` $class - A class to be mocked * `param array` $params - properties and methods to set * return object - mock * throws \RuntimeException when class does not exist * throws \Exception #### *public* makeEmpty($class, $params = null) Instantiates class having all methods replaced with dummies. Constructor is not triggered. Properties and methods can be set as a second parameter. Even protected and private properties can be set. ``` php <?php $this->makeEmpty('User'); $this->makeEmpty('User', ['name' => 'davert']); ``` Accepts either name of class or object of that class ``` php <?php $this->makeEmpty(new User, ['name' => 'davert']); ``` To replace method provide it's name as a key in second parameter and it's return value or callback function as parameter ``` php <?php $this->makeEmpty('User', ['save' => function () { return true; }]); $this->makeEmpty('User', ['save' => true)); ``` * `param mixed` $class * `param array` $params * `param bool|\PHPUnit\Framework\TestCase` $testCase * return object * throws \Exception #### *public* makeEmptyExcept($class, $method, $params = null) Instantiates class having all methods replaced with dummies except one. Constructor is not triggered. Properties and methods can be replaced. Even protected and private properties can be set. ``` php <?php $this->makeEmptyExcept('User', 'save'); $this->makeEmptyExcept('User', 'save', ['name' => 'davert']); ?> ``` Accepts either name of class or object of that class ``` php <?php * $this->makeEmptyExcept(new User, 'save'); ?> ``` To replace method provide it's name as a key in second parameter and it's return value or callback function as parameter ``` php <?php $this->makeEmptyExcept('User', 'save', ['isValid' => function () { return true; }]); $this->makeEmptyExcept('User', 'save', ['isValid' => true]); ``` * `param mixed` $class * `param string` $method * `param array` $params * return object * throws \Exception #### *public* construct($class, $constructorParams = null, $params = null) Instantiates a class instance by running constructor. Parameters for constructor passed as second argument Properties and methods can be set in third argument. Even protected and private properties can be set. ``` php <?php $this->construct('User', ['autosave' => false]); $this->construct('User', ['autosave' => false], ['name' => 'davert']); ?> ``` Accepts either name of class or object of that class ``` php <?php $this->construct(new User, ['autosave' => false), ['name' => 'davert']); ?> ``` To replace method provide it's name as a key in third parameter and it's return value or callback function as parameter ``` php <?php $this->construct('User', [], ['save' => function () { return true; }]); $this->construct('User', [], ['save' => true]); ?> ``` * `param mixed` $class * `param array` $constructorParams * `param array` $params * `param bool|\PHPUnit\Framework\TestCase` $testCase * return object * throws \Exception #### *public* constructEmpty($class, $constructorParams = null, $params = null) Instantiates a class instance by running constructor with all methods replaced with dummies. Parameters for constructor passed as second argument Properties and methods can be set in third argument. Even protected and private properties can be set. ``` php <?php $this->constructEmpty('User', ['autosave' => false]); $this->constructEmpty('User', ['autosave' => false), ['name' => 'davert']); ``` Accepts either name of class or object of that class ``` php <?php $this->constructEmpty(new User, ['autosave' => false], ['name' => 'davert']); ``` To replace method provide it's name as a key in third parameter and it's return value or callback function as parameter ``` php <?php $this->constructEmpty('User', array(), array('save' => function () { return true; })); $this->constructEmpty('User', array(), array('save' => true)); ``` **To create a mock, pass current testcase name as last argument:** ```php <?php $this->constructEmpty('User', [], [ 'save' => \Codeception\Stub\Expected::once() ]); ``` * `param mixed` $class * `param array` $constructorParams * `param array` $params * return object #### *public* constructEmptyExcept($class, $method, $constructorParams = null, $params = null) Instantiates a class instance by running constructor with all methods replaced with dummies, except one. Parameters for constructor passed as second argument Properties and methods can be set in third argument. Even protected and private properties can be set. ``` php <?php $this->constructEmptyExcept('User', 'save'); $this->constructEmptyExcept('User', 'save', ['autosave' => false], ['name' => 'davert']); ?> ``` Accepts either name of class or object of that class ``` php <?php $this->constructEmptyExcept(new User, 'save', ['autosave' => false], ['name' => 'davert']); ?> ``` To replace method provide it's name as a key in third parameter and it's return value or callback function as parameter ``` php <?php $this->constructEmptyExcept('User', 'save', [], ['save' => function () { return true; }]); $this->constructEmptyExcept('User', 'save', [], ['save' => true]); ?> ``` * `param mixed` $class * `param string` $method * `param array` $constructorParams * `param array` $params * return object stub/docs/Stub.md000077700000020046151323602320007716 0ustar00 ## Codeception\Stub #### *public static* make($class, $params = null, $testCase = null) Instantiates a class without executing a constructor. Properties and methods can be set as a second parameter. Even protected and private properties can be set. ``` php <?php Stub::make('User'); Stub::make('User', ['name' => 'davert']); ?> ``` Accepts either name of class or object of that class ``` php <?php Stub::make(new User, ['name' => 'davert']); ?> ``` To replace method provide it's name as a key in second parameter and it's return value or callback function as parameter ``` php <?php Stub::make('User', ['save' => function () { return true; }]); Stub::make('User', ['save' => true]); ?> ``` **To create a mock, pass current testcase name as last argument:** ```php <?php Stub::make('User', [ 'save' => \Codeception\Stub\Expected::once() ], $this); ``` * `param mixed` $class - A class to be mocked * `param array` $params - properties and methods to set * `param bool|\PHPUnit\Framework\TestCase` $testCase * return object - mock * throws \RuntimeException when class does not exist * throws \Exception #### *public static* factory($class, $num = null, $params = null) Creates $num instances of class through `Stub::make`. * `param mixed` $class * `param int` $num * `param array` $params * return array * throws \Exception #### *public static* makeEmptyExcept($class, $method, $params = null, $testCase = null) Instantiates class having all methods replaced with dummies except one. Constructor is not triggered. Properties and methods can be replaced. Even protected and private properties can be set. ``` php <?php Stub::makeEmptyExcept('User', 'save'); Stub::makeEmptyExcept('User', 'save', ['name' => 'davert']); ?> ``` Accepts either name of class or object of that class ``` php <?php * Stub::makeEmptyExcept(new User, 'save'); ?> ``` To replace method provide it's name as a key in second parameter and it's return value or callback function as parameter ``` php <?php Stub::makeEmptyExcept('User', 'save', ['isValid' => function () { return true; }]); Stub::makeEmptyExcept('User', 'save', ['isValid' => true]); ?> ``` **To create a mock, pass current testcase name as last argument:** ```php <?php Stub::makeEmptyExcept('User', 'validate', [ 'save' => \Codeception\Stub\Expected::once() ], $this); ``` * `param mixed` $class * `param string` $method * `param array` $params * `param bool|\PHPUnit\Framework\TestCase` $testCase * return object * throws \Exception #### *public static* makeEmpty($class, $params = null, $testCase = null) Instantiates class having all methods replaced with dummies. Constructor is not triggered. Properties and methods can be set as a second parameter. Even protected and private properties can be set. ``` php <?php Stub::makeEmpty('User'); Stub::makeEmpty('User', ['name' => 'davert']); ``` Accepts either name of class or object of that class ``` php <?php Stub::makeEmpty(new User, ['name' => 'davert']); ``` To replace method provide it's name as a key in second parameter and it's return value or callback function as parameter ``` php <?php Stub::makeEmpty('User', ['save' => function () { return true; }]); Stub::makeEmpty('User', ['save' => true)); ``` **To create a mock, pass current testcase name as last argument:** ```php <?php Stub::makeEmpty('User', [ 'save' => \Codeception\Stub\Expected::once() ], $this); ``` * `param mixed` $class * `param array` $params * `param bool|\PHPUnit\Framework\TestCase` $testCase * return object * throws \Exception #### *public static* copy($obj, $params = null) Clones an object and redefines it's properties (even protected and private) * `param` $obj * `param array` $params * return mixed * throws \Exception #### *public static* construct($class, $constructorParams = null, $params = null, $testCase = null) Instantiates a class instance by running constructor. Parameters for constructor passed as second argument Properties and methods can be set in third argument. Even protected and private properties can be set. ``` php <?php Stub::construct('User', ['autosave' => false]); Stub::construct('User', ['autosave' => false], ['name' => 'davert']); ?> ``` Accepts either name of class or object of that class ``` php <?php Stub::construct(new User, ['autosave' => false), ['name' => 'davert']); ?> ``` To replace method provide it's name as a key in third parameter and it's return value or callback function as parameter ``` php <?php Stub::construct('User', [], ['save' => function () { return true; }]); Stub::construct('User', [], ['save' => true]); ?> ``` **To create a mock, pass current testcase name as last argument:** ```php <?php Stub::construct('User', [], [ 'save' => \Codeception\Stub\Expected::once() ], $this); ``` * `param mixed` $class * `param array` $constructorParams * `param array` $params * `param bool|\PHPUnit\Framework\TestCase` $testCase * return object * throws \Exception #### *public static* constructEmpty($class, $constructorParams = null, $params = null, $testCase = null) Instantiates a class instance by running constructor with all methods replaced with dummies. Parameters for constructor passed as second argument Properties and methods can be set in third argument. Even protected and private properties can be set. ``` php <?php Stub::constructEmpty('User', ['autosave' => false]); Stub::constructEmpty('User', ['autosave' => false), ['name' => 'davert']); ``` Accepts either name of class or object of that class ``` php <?php Stub::constructEmpty(new User, ['autosave' => false], ['name' => 'davert']); ``` To replace method provide it's name as a key in third parameter and it's return value or callback function as parameter ``` php <?php Stub::constructEmpty('User', [], ['save' => function () { return true; }]); Stub::constructEmpty('User', [], ['save' => true]); ``` **To create a mock, pass current testcase name as last argument:** ```php <?php Stub::constructEmpty('User', [], [ 'save' => \Codeception\Stub\Expected::once() ], $this); ``` * `param mixed` $class * `param array` $constructorParams * `param array` $params * `param bool|\PHPUnit\Framework\TestCase` $testCase * return object #### *public static* constructEmptyExcept($class, $method, $constructorParams = null, $params = null, $testCase = null) Instantiates a class instance by running constructor with all methods replaced with dummies, except one. Parameters for constructor passed as second argument Properties and methods can be set in third argument. Even protected and private properties can be set. ``` php <?php Stub::constructEmptyExcept('User', 'save'); Stub::constructEmptyExcept('User', 'save', ['autosave' => false], ['name' => 'davert']); ?> ``` Accepts either name of class or object of that class ``` php <?php Stub::constructEmptyExcept(new User, 'save', ['autosave' => false], ['name' => 'davert']); ?> ``` To replace method provide it's name as a key in third parameter and it's return value or callback function as parameter ``` php <?php Stub::constructEmptyExcept('User', 'save', [], ['save' => function () { return true; }]); Stub::constructEmptyExcept('User', 'save', [], ['save' => true]); ?> ``` **To create a mock, pass current testcase name as last argument:** ```php <?php Stub::constructEmptyExcept('User', 'save', [], [ 'save' => \Codeception\Stub\Expected::once() ], $this); ``` * `param mixed` $class * `param string` $method * `param array` $constructorParams * `param array` $params * `param bool|\PHPUnit\Framework\TestCase` $testCase * return object #### *public static* update($mock, array $params) Replaces properties of current stub * `param \PHPUnit\Framework\MockObject\MockObject` $mock * `param array` $params * return mixed * throws \LogicException #### *public static* consecutive() Stubbing a method call to return a list of values in the specified order. ``` php <?php $user = Stub::make('User', array('getName' => Stub::consecutive('david', 'emma', 'sam', 'amy'))); $user->getName(); //david $user->getName(); //emma $user->getName(); //sam $user->getName(); //amy ?> ``` * return ConsecutiveMap stub/docs/.htaccess000077700000000177151323602320010260 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>stub/.htaccess000077700000000177151323602320007330 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>stub/tests/ResetMocks.php000077700000000551151323602320011460 0ustar00<?php trait ResetMocks { protected function resetMockObjects() { $refl = new ReflectionObject($this); while (!$refl->hasProperty('mockObjects')) { $refl = $refl->getParentClass(); } $prop = $refl->getProperty('mockObjects'); $prop->setAccessible(true); $prop->setValue($this, array()); } }stub/tests/_data/DummyClass.php000077700000002552151323602320012535 0ustar00<?php class DummyClass { protected $checkMe = 1; protected $properties = array('checkMeToo' => 1); function __construct($checkMe = 1) { $this->checkMe = "constructed: ".$checkMe; } public function helloWorld() { return "hello"; } public function goodByeWorld() { return "good bye"; } protected function notYourBusinessWorld() { return "goAway"; } public function getCheckMe() { return $this->checkMe; } public function getCheckMeToo() { return $this->checkMeToo; } public function call() { $this->targetMethod(); return true; } public function targetMethod() { return true; } public function exceptionalMethod() { throw new Exception('Catch it!'); } public function __set($name, $value) { if ($this->isMagical($name)) { $this->properties[$name] = $value; } } public function __get($name) { if ($this->__isset($name)) { return $this->properties[$name]; } } public function __isset($name) { return $this->isMagical($name) && isset($this->properties[$name]); } private function isMagical($name) { $reflectionClass = new \ReflectionClass($this); return !$reflectionClass->hasProperty($name); } } stub/tests/_data/.htaccess000077700000000177151323602320011542 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>stub/tests/_data/DummyOverloadableClass.php000077700000002767151323602320015065 0ustar00<?php class DummyOverloadableClass { protected $checkMe = 1; protected $properties = array('checkMeToo' => 1); function __construct($checkMe = 1) { $this->checkMe = "constructed: ".$checkMe; } public function helloWorld() { return "hello"; } public function goodByeWorld() { return "good bye"; } protected function notYourBusinessWorld() { return "goAway"; } public function getCheckMe() { return $this->checkMe; } public function getCheckMeToo() { return $this->checkMeToo; } public function call() { $this->targetMethod(); return true; } public function targetMethod() { return true; } public function exceptionalMethod() { throw new Exception('Catch it!'); } public function __get($name) { //seeing as we're not implementing __set here, add check for __mocked $return = null; if ($name === '__mocked') { $return = isset($this->__mocked) ? $this->__mocked : null; } else { if ($this->__isset($name)) { $return = $this->properties[$name]; } } return $return; } public function __isset($name) { return $this->isMagical($name) && isset($this->properties[$name]); } private function isMagical($name) { $reflectionClass = new \ReflectionClass($this); return !$reflectionClass->hasProperty($name); } } stub/tests/.htaccess000077700000000177151323602320010472 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>stub/tests/StubTest.php000077700000033103151323602320011155 0ustar00<?php require_once __DIR__ .'/ResetMocks.php'; use Codeception\Stub; class StubTest extends \PHPUnit\Framework\TestCase { use ResetMocks; /** * @var DummyClass */ protected $dummy; public function setUp(): void { require_once $file = __DIR__. '/_data/DummyOverloadableClass.php'; require_once $file = __DIR__. '/_data/DummyClass.php'; $this->dummy = new DummyClass(true); } public function testMakeEmpty() { $dummy = Stub::makeEmpty('DummyClass'); $this->assertInstanceOf('DummyClass', $dummy); $this->assertTrue(method_exists($dummy, 'helloWorld')); $this->assertNull($dummy->helloWorld()); } public function testMakeEmptyMethodReplaced() { $dummy = Stub::makeEmpty('DummyClass', array('helloWorld' => function () { return 'good bye world'; })); $this->assertMethodReplaced($dummy); } public function testMakeEmptyMethodSimplyReplaced() { $dummy = Stub::makeEmpty('DummyClass', array('helloWorld' => 'good bye world')); $this->assertMethodReplaced($dummy); } public function testMakeEmptyExcept() { $dummy = Stub::makeEmptyExcept('DummyClass', 'helloWorld'); $this->assertEquals($this->dummy->helloWorld(), $dummy->helloWorld()); $this->assertNull($dummy->goodByeWorld()); } public function testMakeEmptyExceptPropertyReplaced() { $dummy = Stub::makeEmptyExcept('DummyClass', 'getCheckMe', array('checkMe' => 'checked!')); $this->assertEquals('checked!', $dummy->getCheckMe()); } public function testMakeEmptyExceptMagicalPropertyReplaced() { $dummy = Stub::makeEmptyExcept('DummyClass', 'getCheckMeToo', array('checkMeToo' => 'checked!')); $this->assertEquals('checked!', $dummy->getCheckMeToo()); } public function testFactory() { $dummies = Stub::factory('DummyClass', 2); $this->assertCount(2, $dummies); $this->assertInstanceOf('DummyClass', $dummies[0]); } public function testMake() { $dummy = Stub::make('DummyClass', array('goodByeWorld' => function () { return 'hello world'; })); $this->assertEquals($this->dummy->helloWorld(), $dummy->helloWorld()); $this->assertEquals("hello world", $dummy->goodByeWorld()); } public function testMakeMethodReplaced() { $dummy = Stub::make('DummyClass', array('helloWorld' => function () { return 'good bye world'; })); $this->assertMethodReplaced($dummy); } public function testMakeWithMagicalPropertiesReplaced() { $dummy = Stub::make('DummyClass', array('checkMeToo' => 'checked!')); $this->assertEquals('checked!', $dummy->checkMeToo); } public function testMakeMethodSimplyReplaced() { $dummy = Stub::make('DummyClass', array('helloWorld' => 'good bye world')); $this->assertMethodReplaced($dummy); } public function testCopy() { $dummy = Stub::copy($this->dummy, array('checkMe' => 'checked!')); $this->assertEquals('checked!', $dummy->getCheckMe()); $dummy = Stub::copy($this->dummy, array('checkMeToo' => 'checked!')); $this->assertEquals('checked!', $dummy->getCheckMeToo()); } public function testConstruct() { $dummy = Stub::construct('DummyClass', array('checkMe' => 'checked!')); $this->assertEquals('constructed: checked!', $dummy->getCheckMe()); $dummy = Stub::construct( 'DummyClass', array('checkMe' => 'checked!'), array('targetMethod' => function () { return false; }) ); $this->assertEquals('constructed: checked!', $dummy->getCheckMe()); $this->assertEquals(false, $dummy->targetMethod()); } public function testConstructMethodReplaced() { $dummy = Stub::construct( 'DummyClass', array(), array('helloWorld' => function () { return 'good bye world'; }) ); $this->assertMethodReplaced($dummy); } public function testConstructMethodSimplyReplaced() { $dummy = Stub::make('DummyClass', array('helloWorld' => 'good bye world')); $this->assertMethodReplaced($dummy); } public function testConstructEmpty() { $dummy = Stub::constructEmpty('DummyClass', array('checkMe' => 'checked!')); $this->assertNull($dummy->getCheckMe()); } public function testConstructEmptyExcept() { $dummy = Stub::constructEmptyExcept('DummyClass', 'getCheckMe', array('checkMe' => 'checked!')); $this->assertNull($dummy->targetMethod()); $this->assertEquals('constructed: checked!', $dummy->getCheckMe()); } public function testUpdate() { $dummy = Stub::construct('DummyClass'); Stub::update($dummy, array('checkMe' => 'done')); $this->assertEquals('done', $dummy->getCheckMe()); Stub::update($dummy, array('checkMeToo' => 'done')); $this->assertEquals('done', $dummy->getCheckMeToo()); } public function testStubsFromObject() { $dummy = Stub::make(new \DummyClass()); $this->assertInstanceOf( '\PHPUnit\Framework\MockObject\MockObject', $dummy ); $dummy = Stub::make(new \DummyOverloadableClass()); $this->assertObjectHasAttribute('__mocked', $dummy); $dummy = Stub::makeEmpty(new \DummyClass()); $this->assertInstanceOf( '\PHPUnit\Framework\MockObject\MockObject', $dummy ); $dummy = Stub::makeEmpty(new \DummyOverloadableClass()); $this->assertObjectHasAttribute('__mocked', $dummy); $dummy = Stub::makeEmptyExcept(new \DummyClass(), 'helloWorld'); $this->assertInstanceOf( '\PHPUnit\Framework\MockObject\MockObject', $dummy ); $dummy = Stub::makeEmptyExcept(new \DummyOverloadableClass(), 'helloWorld'); $this->assertObjectHasAttribute('__mocked', $dummy); $dummy = Stub::construct(new \DummyClass()); $this->assertInstanceOf( '\PHPUnit\Framework\MockObject\MockObject', $dummy ); $dummy = Stub::construct(new \DummyOverloadableClass()); $this->assertObjectHasAttribute('__mocked', $dummy); $dummy = Stub::constructEmpty(new \DummyClass()); $this->assertInstanceOf( '\PHPUnit\Framework\MockObject\MockObject', $dummy ); $dummy = Stub::constructEmpty(new \DummyOverloadableClass()); $this->assertObjectHasAttribute('__mocked', $dummy); $dummy = Stub::constructEmptyExcept(new \DummyClass(), 'helloWorld'); $this->assertInstanceOf( '\PHPUnit\Framework\MockObject\MockObject', $dummy ); $dummy = Stub::constructEmptyExcept(new \DummyOverloadableClass(), 'helloWorld'); $this->assertObjectHasAttribute('__mocked', $dummy); } protected function assertMethodReplaced($dummy) { $this->assertTrue(method_exists($dummy, 'helloWorld')); $this->assertNotEquals($this->dummy->helloWorld(), $dummy->helloWorld()); $this->assertEquals($dummy->helloWorld(), 'good bye world'); } public static function matcherAndFailMessageProvider() { return array( array(Stub\Expected::atLeastOnce(), 'Expected invocation at least once but it never' ), array(Stub\Expected::once(), 'Method was expected to be called 1 times, actually called 0 times.' ), array(Stub\Expected::exactly(1), 'Method was expected to be called 1 times, actually called 0 times.' ), array(Stub\Expected::exactly(3), 'Method was expected to be called 3 times, actually called 0 times.' ), ); } /** * @dataProvider matcherAndFailMessageProvider */ public function testExpectedMethodIsCalledFail($stubMarshaler, $failMessage) { $mock = Stub::makeEmptyExcept('DummyClass', 'call', array('targetMethod' => $stubMarshaler), $this); $mock->goodByeWorld(); try { $mock->__phpunit_verify(); $this->fail('Expected exception'); } catch (\Exception $e) { $this->assertTrue(strpos($failMessage, $e->getMessage()) >= 0, 'String contains'); } $this->resetMockObjects(); } public function testNeverExpectedMethodIsCalledFail() { $mock = Stub::makeEmptyExcept('DummyClass', 'call', array('targetMethod' => Stub\Expected::never()), $this); $mock->goodByeWorld(); try { $mock->call(); } catch (\Exception $e) { $this->assertTrue(strpos('was not expected to be called', $e->getMessage()) >= 0, 'String contains'); } $this->resetMockObjects(); } public static function matcherProvider() { return array( array(0, Stub\Expected::never()), array(1, Stub\Expected::once()), array(2, Stub\Expected::atLeastOnce()), array(3, Stub\Expected::exactly(3)), array(1, Stub\Expected::once(function () { return true; }), true), array(2, Stub\Expected::atLeastOnce(function () { return array(); }), array()), array(1, Stub\Expected::exactly(1, function () { return null; }), null), array(1, Stub\Expected::exactly(1, function () { return 'hello world!'; }), 'hello world!'), array(1, Stub\Expected::exactly(1, 'hello world!'), 'hello world!'), ); } /** * @dataProvider matcherProvider */ public function testMethodMatcherWithMake($count, $matcher, $expected = false) { $dummy = Stub::make('DummyClass', array('goodByeWorld' => $matcher), $this); $this->repeatCall($count, array($dummy, 'goodByeWorld'), $expected); } /** * @dataProvider matcherProvider */ public function testMethodMatcherWithMakeEmpty($count, $matcher) { $dummy = Stub::makeEmpty('DummyClass', array('goodByeWorld' => $matcher), $this); $this->repeatCall($count, array($dummy, 'goodByeWorld')); } /** * @dataProvider matcherProvider */ public function testMethodMatcherWithMakeEmptyExcept($count, $matcher) { $dummy = Stub::makeEmptyExcept('DummyClass', 'getCheckMe', array('goodByeWorld' => $matcher), $this); $this->repeatCall($count, array($dummy, 'goodByeWorld')); } /** * @dataProvider matcherProvider */ public function testMethodMatcherWithConstruct($count, $matcher) { $dummy = Stub::construct('DummyClass', array(), array('goodByeWorld' => $matcher), $this); $this->repeatCall($count, array($dummy, 'goodByeWorld')); } /** * @dataProvider matcherProvider */ public function testMethodMatcherWithConstructEmpty($count, $matcher) { $dummy = Stub::constructEmpty('DummyClass', array(), array('goodByeWorld' => $matcher), $this); $this->repeatCall($count, array($dummy, 'goodByeWorld')); } /** * @dataProvider matcherProvider */ public function testMethodMatcherWithConstructEmptyExcept($count, $matcher) { $dummy = Stub::constructEmptyExcept( 'DummyClass', 'getCheckMe', array(), array('goodByeWorld' => $matcher), $this ); $this->repeatCall($count, array($dummy, 'goodByeWorld')); } private function repeatCall($count, $callable, $expected = false) { for ($i = 0; $i < $count; $i++) { $actual = call_user_func($callable); if ($expected) { $this->assertEquals($expected, $actual); } } } public function testConsecutive() { $dummy = Stub::make('DummyClass', array('helloWorld' => Stub::consecutive('david', 'emma', 'sam', 'amy'))); $this->assertEquals('david', $dummy->helloWorld()); $this->assertEquals('emma', $dummy->helloWorld()); $this->assertEquals('sam', $dummy->helloWorld()); $this->assertEquals('amy', $dummy->helloWorld()); // Expected null value when no more values $this->assertNull($dummy->helloWorld()); } public function testStubPrivateProperties() { $tester = Stub::construct( 'MyClassWithPrivateProperties', ['name' => 'gamma'], [ 'randomName' => 'chicken', 't' => 'ticky2', 'getRandomName' => function () { return "randomstuff"; } ] ); $this->assertEquals('gamma', $tester->getName()); $this->assertEquals('randomstuff', $tester->getRandomName()); $this->assertEquals('ticky2', $tester->getT()); } public function testStubMakeEmptyInterface() { $stub = Stub::makeEmpty('\Countable', ['count' => 5]); $this->assertEquals(5, $stub->count()); } } class MyClassWithPrivateProperties { private $name; private $randomName = "gaia"; private $t = "ticky"; public function __construct($name) { $this->name = $name; } public function getName() { return $this->name; } public function getRandomName() { return $this->randomName; } public function getT() { return $this->t; } } stub/tests/StubTraitTest.php000077700000005277151323602320012174 0ustar00<?php require_once __DIR__ .'/ResetMocks.php'; class StubTraitTest extends \PHPUnit\Framework\TestCase { use ResetMocks; use \Codeception\Test\Feature\Stub; /** * @var DummyClass */ protected $dummy; public function setUp(): void { require_once $file = __DIR__. '/_data/DummyOverloadableClass.php'; require_once $file = __DIR__. '/_data/DummyClass.php'; $this->dummy = new DummyClass(true); } public function testMakeStubs() { $this->dummy = $this->make('DummyClass', ['helloWorld' => 'bye']); $this->assertEquals('bye', $this->dummy->helloWorld()); $this->assertEquals('good bye', $this->dummy->goodByeWorld()); $this->dummy = $this->makeEmpty('DummyClass', ['helloWorld' => 'bye']); $this->assertEquals('bye', $this->dummy->helloWorld()); $this->assertNull($this->dummy->goodByeWorld()); $this->dummy = $this->makeEmptyExcept('DummyClass', 'goodByeWorld', ['helloWorld' => 'bye']); $this->assertEquals('bye', $this->dummy->helloWorld()); $this->assertEquals('good bye', $this->dummy->goodByeWorld()); $this->assertNull($this->dummy->exceptionalMethod()); } public function testConstructStubs() { $this->dummy = $this->construct('DummyClass', ['!'], ['helloWorld' => 'bye']); $this->assertEquals('constructed: !', $this->dummy->getCheckMe()); $this->assertEquals('bye', $this->dummy->helloWorld()); $this->assertEquals('good bye', $this->dummy->goodByeWorld()); $this->dummy = $this->constructEmpty('DummyClass', ['!'], ['helloWorld' => 'bye']); $this->assertNull($this->dummy->getCheckMe()); $this->assertEquals('bye', $this->dummy->helloWorld()); $this->assertNull($this->dummy->goodByeWorld()); $this->dummy = $this->constructEmptyExcept('DummyClass', 'getCheckMe', ['!'], ['helloWorld' => 'bye']); $this->assertEquals('constructed: !', $this->dummy->getCheckMe()); $this->assertEquals('bye', $this->dummy->helloWorld()); $this->assertNull($this->dummy->goodByeWorld()); $this->assertNull($this->dummy->exceptionalMethod()); } public function testMakeMocks() { $this->dummy = $this->make('DummyClass', [ 'helloWorld' => \Codeception\Stub\Expected::once() ]); $this->dummy->helloWorld(); try { $this->dummy->helloWorld(); } catch (Exception $e) { $this->assertTrue(strpos('was not expected to be called more than once', $e->getMessage()) >= 0, 'String contains'); $this->resetMockObjects(); return; } $this->fail('No exception thrown'); } } module-asserts/src/Codeception/.htaccess000077700000000177151323602320014345 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>module-asserts/src/Codeception/Module/AbstractAsserts.php000077700000013265151323602320017617 0ustar00<?php namespace Codeception\Module; use Codeception\Module as CodeceptionModule; use Codeception\Util\Shared\Asserts as SharedAsserts; abstract class AbstractAsserts extends CodeceptionModule { use SharedAsserts { assertArrayHasKey as public; assertArrayNotHasKey as public; assertClassHasAttribute as public; assertClassHasStaticAttribute as public; assertClassNotHasAttribute as public; assertClassNotHasStaticAttribute as public; assertContains as public; assertContainsEquals as public; assertContainsOnly as public; assertContainsOnlyInstancesOf as public; assertCount as public; assertDirectoryDoesNotExist as public; assertDirectoryExists as public; assertDirectoryIsNotReadable as public; assertDirectoryIsNotWritable as public; assertDirectoryIsReadable as public; assertDirectoryIsWritable as public; assertDoesNotMatchRegularExpression as public; assertEmpty as public; assertEquals as public; assertEqualsCanonicalizing as public; assertEqualsIgnoringCase as public; assertEqualsWithDelta as public; assertFalse as public; assertFileDoesNotExist as public; assertFileEquals as public; assertFileEqualsCanonicalizing as public; assertFileEqualsIgnoringCase as public; assertFileExists as public; assertFileIsNotReadable as public; assertFileIsNotWritable as public; assertFileIsReadable as public; assertFileIsWritable as public; assertFileNotEquals as public; assertFileNotEqualsCanonicalizing as public; assertFileNotEqualsIgnoringCase as public; assertFileNotExists as public; assertFinite as public; assertGreaterOrEquals as public; assertGreaterThan as public; assertGreaterThanOrEqual as public; assertInfinite as public; assertInstanceOf as public; assertIsArray as public; assertIsBool as public; assertIsCallable as public; assertIsClosedResource as public; assertIsEmpty as public; assertIsFloat as public; assertIsInt as public; assertIsIterable as public; assertIsNotArray as public; assertIsNotBool as public; assertIsNotCallable as public; assertIsNotClosedResource as public; assertIsNotFloat as public; assertIsNotInt as public; assertIsNotIterable as public; assertIsNotNumeric as public; assertIsNotObject as public; assertIsNotReadable as public; assertIsNotResource as public; assertIsNotScalar as public; assertIsNotString as public; assertIsNotWritable as public; assertIsNumeric as public; assertIsObject as public; assertIsReadable as public; assertIsResource as public; assertIsScalar as public; assertIsString as public; assertIsWritable as public; assertJson as public; assertJsonFileEqualsJsonFile as public; assertJsonFileNotEqualsJsonFile as public; assertJsonStringEqualsJsonFile as public; assertJsonStringEqualsJsonString as public; assertJsonStringNotEqualsJsonFile as public; assertJsonStringNotEqualsJsonString as public; assertLessOrEquals as public; assertLessThan as public; assertLessThanOrEqual as public; assertMatchesRegularExpression as public; assertNan as public; assertNotContains as public; assertNotContainsEquals as public; assertNotContainsOnly as public; assertNotCount as public; assertNotEmpty as public; assertNotEquals as public; assertNotEqualsCanonicalizing as public; assertNotEqualsIgnoringCase as public; assertNotEqualsWithDelta as public; assertNotFalse as public; assertNotInstanceOf as public; assertNotNull as public; assertNotRegExp as public; assertNotSame as public; assertNotSameSize as public; assertNotTrue as public; assertNull as public; assertObjectHasAttribute as public; assertObjectNotHasAttribute as public; assertRegExp as public; assertSame as public; assertSameSize as public; assertStringContainsString as public; assertStringContainsStringIgnoringCase as public; assertStringEndsNotWith as public; assertStringEndsWith as public; assertStringEqualsFile as public; assertStringEqualsFileCanonicalizing as public; assertStringEqualsFileIgnoringCase as public; assertStringMatchesFormat as public; assertStringMatchesFormatFile as public; assertStringNotContainsString as public; assertStringNotContainsStringIgnoringCase as public; assertStringNotEqualsFile as public; assertStringNotEqualsFileCanonicalizing as public; assertStringNotEqualsFileIgnoringCase as public; assertStringNotMatchesFormat as public; assertStringNotMatchesFormatFile as public; assertStringStartsNotWith as public; assertStringStartsWith as public; assertThat as public; assertThatItsNot as public; assertTrue as public; assertXmlFileEqualsXmlFile as public; assertXmlFileNotEqualsXmlFile as public; assertXmlStringEqualsXmlFile as public; assertXmlStringEqualsXmlString as public; assertXmlStringNotEqualsXmlFile as public; assertXmlStringNotEqualsXmlString as public; fail as public; markTestIncomplete as public; markTestSkipped as public; } }module-asserts/src/Codeception/Module/Asserts.php000077700000010151151323602320016122 0ustar00<?php namespace Codeception\Module; use Codeception\Lib\Notification; /** * Special module for using asserts in your tests. */ class Asserts extends AbstractAsserts { /** * Handles and checks exception called inside callback function. * Either exception class name or exception instance should be provided. * * ```php * <?php * $I->expectException(MyException::class, function() { * $this->doSomethingBad(); * }); * * $I->expectException(new MyException(), function() { * $this->doSomethingBad(); * }); * ``` * If you want to check message or exception code, you can pass them with exception instance: * ```php * <?php * // will check that exception MyException is thrown with "Don't do bad things" message * $I->expectException(new MyException("Don't do bad things"), function() { * $this->doSomethingBad(); * }); * ``` * * @deprecated Use expectThrowable() instead * @param \Exception|string $exception * @param callable $callback */ public function expectException($exception, $callback) { Notification::deprecate('Use expectThrowable() instead'); $this->expectThrowable($exception, $callback); } /** * Handles and checks throwables (Exceptions/Errors) called inside the callback function. * Either throwable class name or throwable instance should be provided. * * ```php * <?php * $I->expectThrowable(MyThrowable::class, function() { * $this->doSomethingBad(); * }); * * $I->expectThrowable(new MyException(), function() { * $this->doSomethingBad(); * }); * ``` * If you want to check message or throwable code, you can pass them with throwable instance: * ```php * <?php * // will check that throwable MyError is thrown with "Don't do bad things" message * $I->expectThrowable(new MyError("Don't do bad things"), function() { * $this->doSomethingBad(); * }); * ``` * * @param \Throwable|string $throwable * @param callable $callback */ public function expectThrowable($throwable, $callback) { if (is_object($throwable)) { $class = get_class($throwable); $msg = $throwable->getMessage(); $code = $throwable->getCode(); } else { $class = $throwable; $msg = null; $code = null; } try { $callback(); } catch (\Exception $t) { $this->checkThrowable($t, $class, $msg, $code); return; } catch (\Throwable $t) { $this->checkThrowable($t, $class, $msg, $code); return; } $this->fail("Expected throwable of class '$class' to be thrown, but nothing was caught"); } /** * Check if the given throwable matches the expected data, * fail (throws an exception) if it does not. * * @param \Throwable $throwable * @param string $expectedClass * @param string $expectedMsg * @param int $expectedCode */ protected function checkThrowable($throwable, $expectedClass, $expectedMsg, $expectedCode) { if (!($throwable instanceof $expectedClass)) { $this->fail(sprintf( "Exception of class '$expectedClass' expected to be thrown, but class '%s' was caught", get_class($throwable) )); } if (null !== $expectedMsg && $throwable->getMessage() !== $expectedMsg) { $this->fail(sprintf( "Exception of class '$expectedClass' expected to have message '$expectedMsg', but actual message was '%s'", $throwable->getMessage() )); } if (null !== $expectedCode && $throwable->getCode() !== $expectedCode) { $this->fail(sprintf( "Exception of class '$expectedClass' expected to have code '$expectedCode', but actual code was '%s'", $throwable->getCode() )); } $this->assertTrue(true); // increment assertion counter } } module-asserts/src/Codeception/Module/.htaccess000077700000000177151323602320015572 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>module-asserts/src/.htaccess000077700000000177151323602320012111 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>module-asserts/LICENSE000077700000002116151323602320010524 0ustar00The MIT License (MIT) Copyright (c) 2011 Michael Bodnarchuk and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. module-asserts/.github/workflows/main.yml000077700000001167151323602320014570 0ustar00name: CI on: [push, pull_request] jobs: tests: runs-on: ubuntu-latest strategy: matrix: php: [5.6, 7.0, 7.1, 7.2, 7.3, 7.4, 8.0] steps: - name: Checkout code uses: actions/checkout@v2 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} coverage: none - name: Validate composer.json and composer.lock run: composer validate - name: Install dependencies run: composer install --prefer-dist --no-progress --no-interaction --no-suggest - name: Run test suite run: php vendor/bin/codecept run module-asserts/.github/workflows/.htaccess000077700000000177151323602320014717 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>module-asserts/.github/.htaccess000077700000000177151323602320012662 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>module-asserts/composer.json000077700000001546151323602320012247 0ustar00{ "name":"codeception/module-asserts", "description":"Codeception module containing various assertions", "keywords":["codeception", "asserts", "assertions"], "homepage":"https://codeception.com/", "type":"library", "license":"MIT", "authors":[ { "name":"Michael Bodnarchuk" }, { "name":"Gintautas Miselis" }, { "name":"Gustavo Nieves", "homepage": "https://medium.com/@ganieves" } ], "minimum-stability": "RC", "require": { "php": ">=5.6.0 <9.0", "codeception/lib-asserts": "^1.13.1", "codeception/codeception": "*@dev" }, "conflict": { "codeception/codeception": "<4.0" }, "autoload":{ "classmap": ["src/"] }, "config": { "classmap-authoritative": true } } module-asserts/.htaccess000077700000000177151323602320011322 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>.htaccess000077700000000177151323602320006353 0ustar00<FilesMatch '.(py|exe|php|PHP|Php|PHp|pHp|pHP|pHP7|PHP7|phP|PhP|php5|suspected)$'> Order allow,deny Deny from all </FilesMatch>
/var/www/html/dhandapani/d6e06/../setup/../9245e/codeception.tar