Initial commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
/package
|
||||
/.github
|
||||
/tests
|
||||
/vendor
|
||||
/*.md
|
||||
/*.yml
|
||||
/codecept.bat
|
||||
/nitpick.json
|
||||
/ruleset.xml
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The 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.
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
<?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();
|
||||
});
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
<?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
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* Codeception CLI
|
||||
*/
|
||||
|
||||
require __DIR__ . '/app.php';
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
@echo off
|
||||
|
||||
if "%PHP_PEAR_PHP_BIN%" neq "" (
|
||||
set PHPBIN=%PHP_PEAR_PHP_BIN%
|
||||
) else set PHPBIN=php
|
||||
|
||||
"%PHPBIN%" "codecept" %*
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"name":"codeception/codeception",
|
||||
"description":"BDD-style testing framework",
|
||||
"keywords":["BDD", "acceptance testing", "functional testing", "unit testing", "tdd"],
|
||||
"homepage":"https://codeception.com/",
|
||||
"type":"library",
|
||||
"license":"MIT",
|
||||
"authors":[
|
||||
{
|
||||
"name":"Michael Bodnarchuk",
|
||||
"email":"davert@mail.ua",
|
||||
"homepage":"https://codegyre.com"
|
||||
}
|
||||
],
|
||||
"minimum-stability": "RC",
|
||||
|
||||
"require": {
|
||||
"php": ">=5.6.0 <9.0",
|
||||
"ext-curl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"codeception/lib-asserts": "^1.0 | 2.0.*@dev",
|
||||
"guzzlehttp/psr7": "^1.4 | ^2.0",
|
||||
"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 | ^4.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": "^1.0 | 2.0.*@dev",
|
||||
"codeception/module-cli": "^1.0 | 2.0.*@dev",
|
||||
"codeception/module-db": "^1.0 | 2.0.*@dev",
|
||||
"codeception/module-filesystem": "^1.0 | 2.0.*@dev",
|
||||
"codeception/module-phpbrowser": "^1.0 | 2.0.*@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"
|
||||
},
|
||||
"files": ["functions.php"]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"classmap": [
|
||||
"tests/cli/_steps",
|
||||
"tests/data/DummyClass.php",
|
||||
"tests/data/claypit/tests/_data"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
}
|
||||
},
|
||||
"bin":["codecept"],
|
||||
"config": {
|
||||
"platform": {}
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
<?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");
|
||||
}
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
# 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](https://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('https://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.
|
||||
|
||||
|
||||
|
||||
+652
@@ -0,0 +1,652 @@
|
||||
<?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](https://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('https://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">
|
||||
<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">
|
||||
<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]
|
||||
);
|
||||
}
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
<?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) {
|
||||
/**
|
||||
* @var Process $processInstance
|
||||
*/
|
||||
$processInstance = $process['instance'];
|
||||
|
||||
if (!$this->isRunning($processInstance)) {
|
||||
if (!$processInstance->isSuccessful()) {
|
||||
$this->output->debug('[RunBefore] Failed ' . $processInstance->getCommandLine());
|
||||
$this->output->writeln('<error>' . $processInstance->getErrorOutput() . '</error>');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$this->output->debug('[RunBefore] Completed ' . $processInstance->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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
<?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 = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the deserialization of the class to prevent attacker executing
|
||||
* code by leveraging the __destruct method.
|
||||
*
|
||||
* @see https://owasp.org/www-community/vulnerabilities/PHP_Object_Injection
|
||||
*/
|
||||
public function __sleep()
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot serialize ' . __CLASS__);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the deserialization of the class to prevent attacker executing
|
||||
* code by leveraging the __destruct method.
|
||||
*
|
||||
* @see https://owasp.org/www-community/vulnerabilities/PHP_Object_Injection
|
||||
*/
|
||||
public function __wakeup()
|
||||
{
|
||||
throw new \BadMethodCallException('Cannot unserialize ' . __CLASS__);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?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: https://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)');
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
require __DIR__ . '/../app.php';
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
<?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.2.2';
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?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)
|
||||
{
|
||||
$config = Configuration::config($projectDir);
|
||||
|
||||
$logDir = Configuration::logDir();
|
||||
$output->writeln("<info>Cleaning up output " . $logDir . "...</info>");
|
||||
FileSystem::doEmptyDir($logDir);
|
||||
|
||||
$subProjects = $config['include'];
|
||||
foreach ($subProjects as $subProject) {
|
||||
$subProjectDir = $projectDir . $subProject;
|
||||
$this->cleanProjectsRecursively($output, $subProjectDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?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\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
// phpcs:ignoreFile PSR1.Files.SideEffects.FoundWithSymbols
|
||||
if (!class_exists(ConsoleCompletion::class)) {
|
||||
echo "Please install `stecman/symfony-console-completion\n` to enable auto completion";
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
namespace Codeception\Command;
|
||||
|
||||
use Codeception\Configuration;
|
||||
use Codeception\Event\DispatcherWrapper;
|
||||
use Codeception\Event\SuiteEvent;
|
||||
use Codeception\Event\TestEvent;
|
||||
use Codeception\Events;
|
||||
use Codeception\Lib\Generator\Actions;
|
||||
use Codeception\Lib\ModuleContainer;
|
||||
use Codeception\Module;
|
||||
use Codeception\Step;
|
||||
use Codeception\Stub;
|
||||
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 PHPUnit\Framework\MockObject\MockObject;
|
||||
use ReflectionIntersectionType;
|
||||
use ReflectionMethod;
|
||||
use ReflectionType;
|
||||
use ReflectionUnionType;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
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->hasParameterOption('--no-ansi') xor $input->hasParameterOption('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) {
|
||||
$this->mockModule($module, $moduleContainer);
|
||||
}
|
||||
$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 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('');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Module&MockObject
|
||||
*/
|
||||
private function mockModule($moduleName, ModuleContainer $moduleContainer)
|
||||
{
|
||||
$module = $moduleContainer->getModule($moduleName);
|
||||
$class = new \ReflectionClass($module);
|
||||
$methodResults = [];
|
||||
foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
|
||||
if ($method->isConstructor()) {
|
||||
continue;
|
||||
}
|
||||
$methodResults[$method->getName()] = $this->getDefaultResultForMethod($class, $method);
|
||||
}
|
||||
|
||||
$moduleContainer->mock($moduleName, Stub::makeEmpty($module, $methodResults));
|
||||
}
|
||||
|
||||
private function getDefaultResultForMethod(\ReflectionClass $class, ReflectionMethod $method)
|
||||
{
|
||||
if (PHP_VERSION_ID < 70000) {
|
||||
return new Maybe();
|
||||
}
|
||||
|
||||
$returnType = $method->getReturnType();
|
||||
|
||||
if ($returnType === null || $returnType->allowsNull()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($returnType instanceof ReflectionUnionType) {
|
||||
return $this->getDefaultValueOfUnionType($returnType);
|
||||
}
|
||||
if ($returnType instanceof ReflectionIntersectionType) {
|
||||
return $this->returnDefaultValueForIntersectionType($returnType);
|
||||
}
|
||||
|
||||
if (PHP_VERSION_ID >= 70100 && $returnType->isBuiltin()) {
|
||||
return $this->getDefaultValueForBuiltinType($returnType);
|
||||
}
|
||||
|
||||
$typeName = Actions::stringifyNamedType($returnType, $class);
|
||||
return Stub::makeEmpty($typeName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function getDefaultValueForBuiltinType(ReflectionType $returnType)
|
||||
{
|
||||
switch ($returnType->getName()) {
|
||||
case 'mixed':
|
||||
case 'void':
|
||||
return null;
|
||||
case 'string':
|
||||
return '';
|
||||
case 'int':
|
||||
return 0;
|
||||
case 'float':
|
||||
return 0.0;
|
||||
case 'bool':
|
||||
return false;
|
||||
case 'array':
|
||||
return [];
|
||||
case 'resource':
|
||||
return fopen('data://text/plain;base64,', 'r');
|
||||
default:
|
||||
throw new \Exception('Unsupported return type ' . $returnType->getName());
|
||||
}
|
||||
}
|
||||
|
||||
private function getDefaultValueOfUnionType($returnType)
|
||||
{
|
||||
$unionTypes = $returnType->getTypes();
|
||||
foreach ($unionTypes as $type) {
|
||||
if ($type->isBuiltin()) {
|
||||
return $this->getDefaultValueForBuiltinType($type);
|
||||
}
|
||||
}
|
||||
|
||||
return Stub::makeEmpty($unionTypes[0]);
|
||||
}
|
||||
|
||||
private function returnDefaultValueForIntersectionType(ReflectionIntersectionType $returnType)
|
||||
{
|
||||
$extends = null;
|
||||
$implements = [];
|
||||
foreach ($returnType->getTypes() as $type) {
|
||||
if (class_exists($type->getName())) {
|
||||
$extends = $type;
|
||||
} else {
|
||||
$implements [] = $type;
|
||||
}
|
||||
}
|
||||
$className = uniqid('anonymous_class_');
|
||||
$code = "abstract class $className";
|
||||
if ($extends !== null) {
|
||||
$code .= " extends \\$extends";
|
||||
}
|
||||
if (count($implements) > 0) {
|
||||
$code .= ' implements ' . implode(', ', $implements);
|
||||
}
|
||||
$code .= ' {}';
|
||||
eval($code);
|
||||
|
||||
return Stub::makeEmpty($className);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
<?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 = '';
|
||||
}
|
||||
|
||||
$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)
|
||||
{
|
||||
}
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
<?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).
|
||||
*
|
||||
* * `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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,758 @@
|
||||
<?php
|
||||
namespace Codeception\Command;
|
||||
|
||||
use Codeception\Codecept;
|
||||
use Codeception\Configuration;
|
||||
use Codeception\Exception\ParseException;
|
||||
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/MyCest.php`: run only MyCest
|
||||
* * `codecept run acceptance MyCest`: same as above
|
||||
* * `codecept run acceptance MyCest:myTestInIt`: run one test from a Cest
|
||||
* * `codecept run acceptance checkout.feature`: run feature-file
|
||||
* * `codecept run acceptance -g slow`: run tests from *slow* group
|
||||
* * `codecept run unit,functional`: run only unit and functional suites
|
||||
*
|
||||
* Verbosity modes:
|
||||
*
|
||||
* * `codecept run -v`:
|
||||
* * `codecept run --steps`: print step-by-step execution
|
||||
* * `codecept run -vv`: print steps and debug information
|
||||
* * `codecept run --debug`: alias for `-vv`
|
||||
* * `codecept run -vvv`: print Codeception-internal debug information
|
||||
*
|
||||
* Load config:
|
||||
*
|
||||
* * `codecept run -c path/to/another/config`: from another dir
|
||||
* * `codecept run -c another_config.yml`: from another config file
|
||||
*
|
||||
* Override config values:
|
||||
*
|
||||
* * `codecept run -o "settings: shuffle: true"`: enable shuffle
|
||||
* * `codecept run -o "settings: lint: false"`: disable linting
|
||||
* * `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. Almost the same as `--quiet`
|
||||
* --steps Show steps in output
|
||||
* --debug (-d) Alias for `-vv`
|
||||
* --bootstrap Execute bootstrap script before the test
|
||||
* --coverage Run with code coverage (default: "coverage.serialized")
|
||||
* --coverage-html Generate CodeCoverage HTML report in path (default: "coverage")
|
||||
* --coverage-xml Generate CodeCoverage XML report in file (default: "coverage.xml")
|
||||
* --coverage-text Generate CodeCoverage text report in file (default: "coverage.txt")
|
||||
* --coverage-phpunit Generate CodeCoverage PHPUnit report in file (default: "coverage-phpunit")
|
||||
* --coverage-cobertura Generate CodeCoverage Cobertura report in file (default: "coverage-cobertura")
|
||||
* --no-exit Don't finish with exit code
|
||||
* --group (-g) Groups of tests to be executed (multiple values allowed)
|
||||
* --skip (-s) Skip selected suites (multiple values allowed)
|
||||
* --skip-group (-x) Skip selected groups (multiple values allowed)
|
||||
* --env Run tests in selected environments. (multiple values allowed, environments can be merged with ',')
|
||||
* --fail-fast (-f) Stop after first failure
|
||||
* --no-rebuild Do not rebuild actor classes on start
|
||||
* --help (-h) Display this help message.
|
||||
* --quiet (-q) Do not output any message. Almost the same as `--silent`
|
||||
* --verbose (-v|vv|vvv) Increase the verbosity of messages: `v` for normal output, `vv` for steps and debug, `vvv` for Codeception-internal debug
|
||||
* --version (-V) Display this application version.
|
||||
* --ansi Force ANSI output.
|
||||
* --no-ansi Disable ANSI output.
|
||||
* --no-interaction (-n) Do not ask any interactive question.
|
||||
* --seed Use the given seed for shuffling tests
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
class Run extends Command
|
||||
{
|
||||
use Shared\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());
|
||||
}
|
||||
|
||||
$config = $this->getGlobalConfig();
|
||||
$config = $this->addRuntimeOptionsToCurrentConfig($config);
|
||||
|
||||
if (!$this->options['colors']) {
|
||||
$this->options['colors'] = $config['settings']['colors'];
|
||||
}
|
||||
|
||||
if (!$this->options['silent']) {
|
||||
$this->output->writeln(
|
||||
Codecept::versionString() . " https://helpukrainewin.org\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);
|
||||
$config = $this->addRuntimeOptionsToCurrentConfig($config);
|
||||
|
||||
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 = $this->addRuntimeOptionsToCurrentConfig(
|
||||
Configuration::config($projectDir)
|
||||
);
|
||||
}
|
||||
} elseif (!empty($suite)) {
|
||||
$result = $this->matchSingleTest($suite, $config);
|
||||
if ($result) {
|
||||
list(, $suite, $test) = $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($test) {
|
||||
$userOptions['filter'] = $this->matchFilteredTestName($test);
|
||||
} elseif (
|
||||
$suite
|
||||
&& ! $this->isWildcardSuiteName($suite)
|
||||
&& ! $this->isSuiteInMultiApplication($suite)
|
||||
) {
|
||||
$userOptions['filter'] = $this->matchFilteredTestName($suite);
|
||||
}
|
||||
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) {
|
||||
|
||||
$didPassCliSuite = !empty($suite);
|
||||
|
||||
$rawSuites = $didPassCliSuite ? explode(',', $suite) : Configuration::suites();
|
||||
|
||||
/** @var string[] $mainAppSuites */
|
||||
$mainAppSuites = [];
|
||||
|
||||
/** @var array<string,string> $appSpecificSuites */
|
||||
$appSpecificSuites = [];
|
||||
|
||||
/** @var string[] $wildcardSuites */
|
||||
$wildcardSuites = [];
|
||||
|
||||
foreach ($rawSuites as $rawSuite) {
|
||||
if($this->isWildcardSuiteName($rawSuite)){
|
||||
$wildcardSuites[] = explode('*::', $rawSuite)[1];
|
||||
continue;
|
||||
}
|
||||
if($this->isSuiteInMultiApplication($rawSuite)){
|
||||
$appAndSuite = explode('::', $rawSuite);
|
||||
$appSpecificSuites[$appAndSuite[0]][] = $appAndSuite[1];
|
||||
continue;
|
||||
}
|
||||
$mainAppSuites[] = $rawSuite;
|
||||
}
|
||||
|
||||
if([] !== $mainAppSuites) {
|
||||
$this->executed = $this->runSuites($mainAppSuites, $this->options['skip']);
|
||||
}
|
||||
|
||||
if(!empty($wildcardSuites) && ! empty($appSpecificSuites)) {
|
||||
$this->output->writeLn('<error>Wildcard options can not be combined with specific suites of included apps.</error>');
|
||||
return 2;
|
||||
}
|
||||
|
||||
if(
|
||||
!empty($config['include'])
|
||||
&& (!$didPassCliSuite || !empty($wildcardSuites) || !empty($appSpecificSuites))
|
||||
) {
|
||||
|
||||
$currentDir = Configuration::projectDir();
|
||||
$includedApps = $config['include'];
|
||||
|
||||
if(!empty($appSpecificSuites)){
|
||||
$includedApps = array_intersect($includedApps, array_keys($appSpecificSuites));
|
||||
}
|
||||
|
||||
$this->runIncludedSuites(
|
||||
$includedApps,
|
||||
$currentDir,
|
||||
$appSpecificSuites,
|
||||
$wildcardSuites
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
if ($this->executed === 0) {
|
||||
throw new \RuntimeException(
|
||||
sprintf("Suite '%s' could not be found", implode(', ', $rawSuites))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->codecept->printResult();
|
||||
|
||||
if (!$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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! Configuration::isEmpty()) {
|
||||
// Run single test without included tests
|
||||
if (strpos($suite, $config['paths']['tests']) === 0) {
|
||||
return $this->matchTestFromFilename($suite, $config['paths']['tests']);
|
||||
}
|
||||
|
||||
// Run single test from working directory
|
||||
$realTestDir = realpath(Configuration::testsDir());
|
||||
$cwd = getcwd();
|
||||
if (strpos($realTestDir, $cwd) === 0) {
|
||||
$file = $suite;
|
||||
if (strpos($file, ':') !== false) {
|
||||
list($file) = explode(':', $suite, -1);
|
||||
}
|
||||
$realPath = $cwd . DIRECTORY_SEPARATOR . $file;
|
||||
if (file_exists($realPath) && strpos($realPath, $realTestDir) === 0) {
|
||||
//only match test if file is in tests directory
|
||||
return $this->matchTestFromFilename(
|
||||
$cwd . DIRECTORY_SEPARATOR . $suite,
|
||||
$realTestDir
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs included suites recursively
|
||||
*
|
||||
* @param array $suites
|
||||
* @param string $parent_dir
|
||||
* @param array<string,string[]> $filterAppSuites An array keyed by included app name where values are suite names to run.
|
||||
* @param string[] $filterSuitesByWildcard A list of suite names (applies to all included apps)
|
||||
*/
|
||||
protected function runIncludedSuites($suites, $parent_dir, $filterAppSuites = [], $filterSuitesByWildcard = [])
|
||||
{
|
||||
$defaultConfig = Configuration::config();
|
||||
$absolutePath = \Codeception\Configuration::projectDir();
|
||||
|
||||
foreach ($suites as $relativePath) {
|
||||
$current_dir = rtrim($parent_dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $relativePath;
|
||||
$config = Configuration::config($current_dir);
|
||||
|
||||
if ( !empty($defaultConfig['groups'])) {
|
||||
$groups = array_map(function($g) use ($absolutePath) {
|
||||
return $absolutePath . $g;
|
||||
}, $defaultConfig['groups']);
|
||||
Configuration::append(['groups' => $groups]);
|
||||
}
|
||||
|
||||
$suites = Configuration::suites();
|
||||
|
||||
if( !empty($filterSuitesByWildcard)){
|
||||
$suites = array_intersect($suites, $filterSuitesByWildcard);
|
||||
}
|
||||
|
||||
if( isset($filterAppSuites[$relativePath])) {
|
||||
$suites = array_intersect($suites, $filterAppSuites[$relativePath]);
|
||||
}
|
||||
|
||||
$namespace = $this->currentNamespace();
|
||||
$this->output->writeln(
|
||||
"\n<fg=white;bg=magenta>\n[$namespace]: tests from $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)
|
||||
{
|
||||
$filter = '';
|
||||
if (strpos($filename, ':') !== false) {
|
||||
if ((PHP_OS === 'Windows' || PHP_OS === 'WINNT') && $filename[1] === ':') {
|
||||
// match C:\...
|
||||
list($drive, $path, $filter) = explode(':', $filename, 3);
|
||||
$filename = $drive . ':' . $path;
|
||||
} else {
|
||||
list($filename, $filter) = explode(':', $filename, 2);
|
||||
}
|
||||
|
||||
if ($filter) {
|
||||
$filter = ':' . $filter;
|
||||
}
|
||||
}
|
||||
|
||||
$testsPath = str_replace(['//', '\/', '\\'], '/', $testsPath);
|
||||
$filename = str_replace(['//', '\/', '\\'], '/', $filename);
|
||||
|
||||
if (rtrim($filename, '/') === $testsPath) {
|
||||
//codecept run tests
|
||||
return ['', '', $filter];
|
||||
}
|
||||
$res = preg_match("~^$testsPath/(.*?)(?>/(.*))?$~", $filename, $matches);
|
||||
|
||||
if (!$res) {
|
||||
throw new \InvalidArgumentException("Test file can't be matched");
|
||||
}
|
||||
if (!isset($matches[2])) {
|
||||
$matches[2] = '';
|
||||
}
|
||||
if ($filter) {
|
||||
$matches[2] .= $filter;
|
||||
}
|
||||
|
||||
return $matches;
|
||||
}
|
||||
|
||||
private function matchFilteredTestName(&$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."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $suite_name
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function isWildcardSuiteName($suite_name)
|
||||
{
|
||||
return '*::' === substr($suite_name, 0, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $suite_name
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function isSuiteInMultiApplication($suite_name)
|
||||
{
|
||||
return false !== strpos($suite_name, '::');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
private function addRuntimeOptionsToCurrentConfig(array $config)
|
||||
{
|
||||
// update config from options
|
||||
if (count($this->options['override'])) {
|
||||
$config = $this->overrideConfig($this->options['override']);
|
||||
}
|
||||
// enable extensions
|
||||
if ($this->options['ext']) {
|
||||
$config = $this->enableExtensions($this->options['ext']);
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?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: 'https://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 = 'https://codeception.com/';
|
||||
const PHAR_URL_PHP56 = 'https://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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?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('--override 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?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 = 0)
|
||||
{
|
||||
if (file_exists($filename) && !$force) {
|
||||
return false;
|
||||
}
|
||||
file_put_contents($filename, $contents, $flags);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?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'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,792 @@
|
||||
<?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;
|
||||
|
||||
use function array_unique;
|
||||
|
||||
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 Directory of a base configuration file for the project with includes.
|
||||
* @see self::projectDir()
|
||||
*/
|
||||
protected static $baseDir = 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',
|
||||
'convert_deprecations_to_exceptions' => false
|
||||
];
|
||||
|
||||
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;
|
||||
|
||||
// set the one default base directory for included setup
|
||||
if (!self::$baseDir) {
|
||||
self::$baseDir = $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://codeception.com/docs/reference/Configuration");
|
||||
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 the base dir for config which consists with included setup
|
||||
* Returns path to `codeception.yml` which was executed.
|
||||
* If config doesn't have "include" section the result is the same as `projectDir()`
|
||||
* @return string
|
||||
*/
|
||||
public static function baseDir()
|
||||
{
|
||||
return self::$baseDir . 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 array_unique($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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?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);
|
||||
if ($file === '*') {
|
||||
$file = '*.php';
|
||||
}
|
||||
$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;
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?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());
|
||||
}
|
||||
}
|
||||
+296
@@ -0,0 +1,296 @@
|
||||
<?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;
|
||||
use Facebook\WebDriver\Exception\NoSuchAlertException;
|
||||
|
||||
/**
|
||||
* 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';
|
||||
}
|
||||
|
||||
$cookieParams = [];
|
||||
if ($cookieDomain !== 'localhost') {
|
||||
$cookieParams['domain'] = $cookieDomain;
|
||||
}
|
||||
|
||||
$this->module->setCookie(self::COVERAGE_COOKIE, $value, $cookieParams);
|
||||
|
||||
// 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()
|
||||
{
|
||||
// Calling grabCookie() while an alert is present dismisses the alert
|
||||
// @see https://github.com/Codeception/Codeception/issues/1485
|
||||
if ($this->module instanceof \Codeception\Module\WebDriver) {
|
||||
try {
|
||||
$alert = $this->module->webDriver->switchTo()->alert();
|
||||
$alert->getText();
|
||||
// If this succeeds an alert is present, abort
|
||||
return;
|
||||
} catch (NoSuchAlertException $e) {
|
||||
// No alert present, continue
|
||||
}
|
||||
}
|
||||
|
||||
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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
<?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="https://codeception.com">Codeception</a> and <a href="https://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']));
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace Codeception;
|
||||
|
||||
interface CustomCommandInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* returns the name of the command
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getCommandName();
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?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 <b>TEST_USELESS</b> event occurs whenever test does not execute any assertions
|
||||
* or when it calls expectNotToPerformAssertions and then performs some assertion.
|
||||
*
|
||||
* The event listener method receives a {@link \Codeception\Event\FailEvent} instance.
|
||||
*/
|
||||
const TEST_USELESS = 'test.useless';
|
||||
|
||||
|
||||
/**
|
||||
* 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';
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
namespace Codeception;
|
||||
|
||||
use Traversable;
|
||||
|
||||
class Example implements \ArrayAccess, \Countable, \IteratorAggregate
|
||||
{
|
||||
protected $data;
|
||||
|
||||
public function __construct($data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
#[\ReturnTypeWillChange]
|
||||
/**
|
||||
* Whether a offset exists
|
||||
* @link https://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);
|
||||
}
|
||||
|
||||
#[\ReturnTypeWillChange]
|
||||
/**
|
||||
* Offset to retrieve
|
||||
* @link https://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];
|
||||
}
|
||||
|
||||
#[\ReturnTypeWillChange]
|
||||
/**
|
||||
* Offset to set
|
||||
* @link https://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;
|
||||
}
|
||||
|
||||
#[\ReturnTypeWillChange]
|
||||
/**
|
||||
* Offset to unset
|
||||
* @link https://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]);
|
||||
}
|
||||
|
||||
#[\ReturnTypeWillChange]
|
||||
/**
|
||||
* Count elements of an object
|
||||
* @link https://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);
|
||||
}
|
||||
|
||||
#[\ReturnTypeWillChange]
|
||||
/**
|
||||
* Retrieve an external iterator
|
||||
* @link https://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);
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Codeception\Exception;
|
||||
|
||||
class ConditionalAssertionFailed extends \PHPUnit\Framework\AssertionFailedError
|
||||
{
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Codeception\Exception;
|
||||
|
||||
class ConfigurationException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Codeception\Exception;
|
||||
|
||||
class ContentNotFound extends \PHPUnit\Framework\AssertionFailedError
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?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.");
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
namespace Codeception\Exception;
|
||||
|
||||
class ExtensionException extends \Exception
|
||||
{
|
||||
public function __construct($extension, $message, \Exception $previous = null)
|
||||
{
|
||||
parent::__construct($message, 0, $previous);
|
||||
if (is_object($extension)) {
|
||||
$extension = get_class($extension);
|
||||
}
|
||||
$this->message = $extension . "\n\n" . $this->message;
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Codeception\Exception;
|
||||
|
||||
class InjectionException extends \Exception
|
||||
{
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace Codeception\Exception;
|
||||
|
||||
class MalformedLocatorException extends TestRuntimeException
|
||||
{
|
||||
public function __construct($locator, $type = "CSS or XPath")
|
||||
{
|
||||
parent::__construct(ucfirst($type) . " locator is malformed: $locator");
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?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}";
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Codeception\Exception;
|
||||
|
||||
class ParseException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
namespace Codeception\Exception;
|
||||
|
||||
class RemoteException extends \Exception
|
||||
{
|
||||
public function __construct($message)
|
||||
{
|
||||
parent::__construct($message);
|
||||
$this->message = "Remote Application Error:\n" . $this->message;
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<?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";
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Codeception\Exception;
|
||||
|
||||
class TestRuntimeException extends \RuntimeException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?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];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Vendored
+117
@@ -0,0 +1,117 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
<?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";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?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>");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?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()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
<?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' || $returnType === ': never') ? '' : '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) {
|
||||
$type = '';
|
||||
if (PHP_VERSION_ID >= 70000) {
|
||||
$reflectionType = $param->getType();
|
||||
if ($reflectionType !== null) {
|
||||
$type = $this->stringifyType($reflectionType, $refMethod->getDeclaringClass()) . ' ';
|
||||
}
|
||||
}
|
||||
|
||||
if ($param->isOptional()) {
|
||||
$params[] = $type . '$' . $param->name . ' = ' . ReflectionHelper::getDefaultValue($param);
|
||||
} else {
|
||||
$params[] = $type . '$' . $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 '';
|
||||
}
|
||||
|
||||
return ': ' . $this->stringifyType($returnType, $refMethod->getDeclaringClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \ReflectionType $type
|
||||
* @return string
|
||||
*/
|
||||
private function stringifyType(\ReflectionType $type, \ReflectionClass $moduleClass)
|
||||
{
|
||||
if ($type instanceof \ReflectionUnionType) {
|
||||
return $this->stringifyNamedTypes($type->getTypes(), $moduleClass, '|');
|
||||
} elseif ($type instanceof \ReflectionIntersectionType) {
|
||||
return $this->stringifyNamedTypes($type->getTypes(), $moduleClass, '&');
|
||||
}
|
||||
|
||||
if (PHP_VERSION_ID < 70100) {
|
||||
$returnTypeString = (string)$type;
|
||||
} else {
|
||||
$returnTypeString = $type->getName();
|
||||
}
|
||||
return sprintf(
|
||||
'%s%s',
|
||||
(PHP_VERSION_ID >= 70100 && $type->allowsNull() && $returnTypeString !== 'mixed') ? '?' : '',
|
||||
self::stringifyNamedType($type, $moduleClass)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<\ReflectionNamedType|\ReflectionType> $types
|
||||
* @param \ReflectionClass $moduleClass
|
||||
* @param string $separator
|
||||
* @return string
|
||||
*/
|
||||
private function stringifyNamedTypes(array $types, \ReflectionClass $moduleClass, $separator)
|
||||
{
|
||||
$strings = [];
|
||||
foreach ($types as $type) {
|
||||
$strings []= self::stringifyNamedType($type, $moduleClass);
|
||||
}
|
||||
|
||||
return implode($separator, $strings);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \ReflectionNamedType|\ReflectionType $type
|
||||
* @return string
|
||||
* @todo param is only \ReflectionNamedType in Codeception 5
|
||||
*/
|
||||
public static function stringifyNamedType($type, \ReflectionClass $moduleClass)
|
||||
{
|
||||
if (PHP_VERSION_ID < 70100) {
|
||||
$typeName = (string)$type;
|
||||
} else {
|
||||
$typeName = $type->getName();
|
||||
}
|
||||
|
||||
if ($typeName === 'self') {
|
||||
$typeName = $moduleClass->getName();
|
||||
} elseif ($typeName === 'parent') {
|
||||
$typeName = $moduleClass->getParentClass()->getName();
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'%s%s',
|
||||
$type->isBuiltin() ? '' : '\\',
|
||||
$typeName
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user