This commit is contained in:
2020-02-01 16:47:12 +07:00
commit 4c619ad6e6
16739 changed files with 3329179 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
<?php
namespace Codeception;
use Codeception\Lib\Actor\Shared\Comment;
use Codeception\Lib\Actor\Shared\Friend;
use Codeception\Step\Executor;
abstract class Actor
{
use Comment;
use Friend;
/**
* @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(mb_strtolower($text, 'utf-8'));
}
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;
}
}

View File

@@ -0,0 +1,183 @@
<?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->renderException($e, new ConsoleOutput());
exit(1);
} catch (\Exception $e) {
$this->renderException($e, new ConsoleOutput());
exit(1);
}
}
/**
* 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') && empty($_SERVER['argv'])) {
//register_argc_argv is always off on HHVM, but it has no effect
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;
}
}
}

View File

@@ -0,0 +1,236 @@
<?php
namespace Codeception;
use Codeception\Exception\ConfigurationException;
use Codeception\Subscriber\ExtensionLoader;
use Symfony\Component\EventDispatcher\EventDispatcher;
class Codecept
{
const VERSION = "2.4.5";
/**
* @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,
'json' => false,
'tap' => false,
'report' => false,
'colors' => false,
'coverage' => false,
'coverage-xml' => false,
'coverage-html' => false,
'coverage-text' => false,
'coverage-crap4j' => 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();
$settings = Configuration::suiteSettings($suite, $config);
$selectedEnvironments = $this->options['env'];
$environments = Configuration::suiteEnvironments($suite);
if (!$selectedEnvironments or empty($environments)) {
$this->runSuite($settings, $suite, $test);
return;
}
foreach (array_unique($selectedEnvironments) as $envList) {
$envArray = explode(',', $envList);
$config = [];
foreach ($envArray as $env) {
if (isset($environments[$env])) {
$currentEnvironment = isset($config['current_environment']) ? [$config['current_environment']] : [];
$config = Configuration::mergeConfigs($config, $environments[$env]);
$currentEnvironment[] = $config['current_environment'];
$config['current_environment'] = implode(',', $currentEnvironment);
}
}
if (empty($config)) {
continue;
}
$suiteToRun = $suite;
if (!empty($envList)) {
$suiteToRun .= ' (' . implode(', ', $envArray) . ')';
}
$this->runSuite($config, $suiteToRun, $test);
}
}
public function runSuite($settings, $suite, $test = null)
{
$suiteManager = new SuiteManager($this->dispatcher, $suite, $settings);
$suiteManager->initialize();
$suiteManager->loadTests($test);
$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->dispatcher->dispatch(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);
}
}

View File

@@ -0,0 +1,57 @@
<?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',
'ns',
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();
}
}

View File

@@ -0,0 +1,108 @@
<?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();
}
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);
$this->output->writeln(
" -> {$settings['actor']}Actions.php generated successfully. "
. $actionsGenerator->getNumMethods() . " methods added"
);
$content = $actionsGenerator->produce();
$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);
}
}
}

View File

@@ -0,0 +1,45 @@
<?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");
}
private function cleanProjectsRecursively(OutputInterface $output, $projectDir)
{
$logDir = Configuration::logDir();
$output->writeln("<info>Cleaning up output " . $logDir . "...</info>");
FileSystem::doEmptyDir($logDir);
$config = Configuration::config($projectDir);
$subProjects = $config['include'];
foreach ($subProjects as $subProject) {
$subProjectDir = $projectDir . $subProject;
$this->cleanProjectsRecursively($output, $subProjectDir);
}
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Codeception\Command;
if (!class_exists('Stecman\Component\Symfony\Console\BashCompletion\Completion')) {
echo "Please install `stecman/symfony-console-completion\n` to enable auto completion";
return;
}
use Codeception\Configuration;
use Stecman\Component\Symfony\Console\BashCompletion\Completion as ConsoleCompletion;
use Stecman\Component\Symfony\Console\BashCompletion\CompletionCommand;
use Stecman\Component\Symfony\Console\BashCompletion\CompletionHandler;
use Stecman\Component\Symfony\Console\BashCompletion\Completion\ShellPathCompletion as ShellPathCompletion;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class Completion extends CompletionCommand
{
protected function configureCompletion(CompletionHandler $handler)
{
// Can't set for all commands, because it wouldn't work well with generate:suite
$suiteCommands = [
'run',
'config:validate',
'console',
'dry-run',
'generate:cept',
'generate:cest',
'generate:feature',
'generate:phpunit',
'generate:scenarios',
'generate:stepobject',
'generate:test',
'gherkin:snippets',
'gherkin:steps'
];
foreach ($suiteCommands as $suiteCommand) {
$handler->addHandler(new ConsoleCompletion(
$suiteCommand,
'suite',
ConsoleCompletion::TYPE_ARGUMENT,
Configuration::suites()
));
}
$handler->addHandlers([
new ShellPathCompletion(
ConsoleCompletion::ALL_COMMANDS,
'path',
ConsoleCompletion::TYPE_ARGUMENT
),
new ShellPathCompletion(
ConsoleCompletion::ALL_COMMANDS,
'test',
ConsoleCompletion::TYPE_ARGUMENT
),
]);
}
protected function execute(InputInterface $input, OutputInterface $output)
{
if ($input->getOption('generate-hook') && $input->getOption('use-vendor-bin')) {
global $argv;
$argv[0] = 'vendor/bin/' . basename($argv[0]);
}
parent::execute($input, $output);
}
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;
}
}

View File

@@ -0,0 +1,40 @@
<?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 as 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>");
}
}

View File

@@ -0,0 +1,105 @@
<?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, $input->getOption('config'));
$output->writeln("Ok");
$output->writeln("------------------------------\n");
$output->writeln("<info>$suite Suite Config</info>:\n");
$output->writeln($this->formatOutput($config));
return;
}
$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");
}
protected function formatOutput($config)
{
$output = print_r($config, true);
return preg_replace('~\[(.*?)\] =>~', "<fg=yellow>$1</fg=yellow> =>", $output);
}
}

View File

@@ -0,0 +1,159 @@
<?php
namespace Codeception\Command;
use Codeception\Codecept;
use Codeception\Configuration;
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
{
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>");
$output->writeln("<info>type 'exit' to leave console</info>");
$output->writeln("<info>type 'actions' to see all available actions for this suite</info>");
$suiteEvent = new SuiteEvent($this->suite, $this->codecept->getResult(), $settings);
$dispatcher->dispatch(Events::SUITE_BEFORE, $suiteEvent);
$dispatcher->dispatch(Events::TEST_PARSED, new TestEvent($this->test));
$dispatcher->dispatch(Events::TEST_BEFORE, new TestEvent($this->test));
$output->writeln("\n\n<comment>\$I</comment> = new {$settings['actor']}(\$scenario);");
$this->executeCommands($input, $output, $I, $settings['bootstrap']);
$dispatcher->dispatch(Events::TEST_AFTER, new TestEvent($this->test));
$dispatcher->dispatch(Events::SUITE_AFTER, new SuiteEvent($this->suite));
$output->writeln("<info>Bye-bye!</info>");
}
protected function executeCommands(InputInterface $input, OutputInterface $output, $I, $bootstrap)
{
$dialog = new QuestionHelper();
if (file_exists($bootstrap)) {
require $bootstrap;
}
do {
$question = new Question("<comment>\$I-></comment>");
$question->setAutocompleterValues($this->actions);
$command = $dialog->ask($input, $output, $question);
if ($command == 'actions') {
$output->writeln("<info>" . implode(' ', $this->actions));
continue;
}
if ($command == 'exit') {
return;
}
if ($command == '') {
continue;
}
try {
$value = eval("return \$I->$command;");
if ($value && !is_object($value)) {
codecept_debug($value);
}
} catch (\PHPUnit\Framework\AssertionFailedError $fail) {
$output->writeln("<error>fail</error> " . $fail->getMessage());
} catch (\Exception $e) {
$output->writeln("<error>error</error> " . $e->getMessage());
}
} while (true);
}
protected function listenToSignals()
{
if (function_exists('pcntl_signal')) {
declare (ticks = 1);
pcntl_signal(SIGINT, SIG_IGN);
pcntl_signal(SIGTERM, SIG_IGN);
}
}
}

View File

@@ -0,0 +1,134 @@
<?php
namespace Codeception\Command;
use Codeception\Configuration;
use Codeception\Event\SuiteEvent;
use Codeception\Event\TestEvent;
use Codeception\Events;
use Codeception\Subscriber\Bootstrap as BootstrapLoader;
use Codeception\Subscriber\Console as ConsolePrinter;
use Codeception\SuiteManager;
use Codeception\Test\Interfaces\ScenarioDriven;
use Codeception\Test\Test;
use Codeception\Util\Maybe;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;
/**
* Shows step by step execution process for scenario driven tests without actually running them.
*
* * `codecept dry-run acceptance`
* * `codecept dry-run acceptance MyCest`
* * `codecept dry-run acceptance checkout.feature`
* * `codecept dry-run tests/acceptance/MyCest.php`
*
*/
class DryRun extends Command
{
use 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();
if (! Configuration::isEmpty() && ! $test && strpos($suite, $config['paths']['tests']) === 0) {
list(, $suite, $test) = $this->matchTestFromFilename($suite, $config['paths']['tests']);
}
$settings = $this->getSuiteConfig($suite);
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new ConsolePrinter([
'colors' => !$input->getOption('no-ansi'),
'steps' => true,
'verbosity' => OutputInterface::VERBOSITY_VERBOSE,
]));
$dispatcher->addSubscriber(new BootstrapLoader());
$suiteManager = new SuiteManager($dispatcher, $suite, $settings);
$moduleContainer = $suiteManager->getModuleContainer();
foreach (Configuration::modules($settings) as $module) {
$moduleContainer->mock($module, new Maybe());
}
$suiteManager->loadTests($test);
$tests = $suiteManager->getSuite()->tests();
$dispatcher->dispatch(Events::SUITE_INIT, new SuiteEvent($suiteManager->getSuite(), null, $settings));
$dispatcher->dispatch(Events::SUITE_BEFORE, new SuiteEvent($suiteManager->getSuite(), null, $settings));
foreach ($tests as $test) {
if ($test instanceof \PHPUnit\Framework\TestSuite\DataProvider) {
foreach ($test as $t) {
if ($t instanceof Test) {
$this->dryRunTest($output, $dispatcher, $t);
}
}
}
if ($test instanceof Test and $test instanceof ScenarioDriven) {
$this->dryRunTest($output, $dispatcher, $test);
}
}
$dispatcher->dispatch(Events::SUITE_AFTER, new SuiteEvent($suiteManager->getSuite()));
}
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)
{
$dispatcher->dispatch(Events::TEST_START, new TestEvent($test));
$dispatcher->dispatch(Events::TEST_BEFORE, new TestEvent($test));
try {
$test->test();
} catch (\Exception $e) {
}
$dispatcher->dispatch(Events::TEST_AFTER, new TestEvent($test));
$dispatcher->dispatch(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('');
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Codeception\Command;
use Codeception\Lib\Generator\Cept;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Generates Cept (scenario-driven test) file:
*
* * `codecept generate:cept suite Login`
* * `codecept g:cept suite subdir/subdir/testnameCept.php`
* * `codecept g:cept suite LoginCept -c path/to/project`
*
*/
class GenerateCept extends Command
{
use Shared\FileSystem;
use Shared\Config;
protected function configure()
{
$this->setDefinition([
new InputArgument('suite', InputArgument::REQUIRED, 'suite to be tested'),
new InputArgument('test', InputArgument::REQUIRED, 'test to be run'),
]);
}
public function getDescription()
{
return 'Generates empty Cept file in suite';
}
public function execute(InputInterface $input, OutputInterface $output)
{
$suite = $input->getArgument('suite');
$filename = $input->getArgument('test');
$config = $this->getSuiteConfig($suite);
$this->createDirectoryFor($config['path'], $filename);
$filename = $this->completeSuffix($filename, 'Cept');
$gen = new Cept($config);
$full_path = rtrim($config['path'], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $filename;
$res = $this->createFile($full_path, $gen->produce());
if (!$res) {
$output->writeln("<error>Test $filename already exists</error>");
return;
}
$output->writeln("<info>Test was created in $full_path</info>");
}
}

View File

@@ -0,0 +1,62 @@
<?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;
}
$gen = new CestGenerator($class, $config);
$res = $this->createFile($filename, $gen->produce());
if (!$res) {
$output->writeln("<error>Test $filename already exists</error>");
return;
}
$output->writeln("<info>Test was created in $filename</info>");
}
}

View File

@@ -0,0 +1,58 @@
<?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>");
} else {
$output->writeln("<error>File $relativePath/$file already exists</error>");
}
}
}

View File

@@ -0,0 +1,58 @@
<?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;
}
$output->writeln("<info>Feature was created in $full_path</info>");
}
}

View File

@@ -0,0 +1,56 @@
<?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;
}
$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.'
);
}
}

View File

@@ -0,0 +1,50 @@
<?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>");
} else {
$output->writeln("<error>Error creating helper $filename</error>");
}
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Codeception\Command;
use Codeception\Configuration;
use Codeception\Lib\Generator\PageObject as PageObjectGenerator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Generates PageObject. Can be generated either globally, or just for one suite.
* If PageObject is generated globally it will act as UIMap, without any logic in it.
*
* * `codecept g:page Login`
* * `codecept g:page Registration`
* * `codecept g:page acceptance Login`
*/
class GeneratePageObject extends Command
{
use Shared\FileSystem;
use Shared\Config;
protected function configure()
{
$this->setDefinition([
new InputArgument('suite', InputArgument::REQUIRED, 'Either suite name or page object name)'),
new InputArgument('page', InputArgument::OPTIONAL, 'Page name of pageobject to represent'),
]);
parent::configure();
}
public function getDescription()
{
return 'Generates empty PageObject class';
}
public function execute(InputInterface $input, OutputInterface $output)
{
$suite = $input->getArgument('suite');
$class = $input->getArgument('page');
if (!$class) {
$class = $suite;
$suite = null;
}
$conf = $suite
? $this->getSuiteConfig($suite)
: $this->getGlobalConfig();
if ($suite) {
$suite = DIRECTORY_SEPARATOR . ucfirst($suite);
}
$path = $this->createDirectoryFor(Configuration::supportDir() . 'Page' . $suite, $class);
$filename = $path . $this->getShortClassName($class) . '.php';
$output->writeln($filename);
$gen = new PageObjectGenerator($conf, ucfirst($suite) . '\\' . $class);
$res = $this->createFile($filename, $gen->produce());
if (!$res) {
$output->writeln("<error>PageObject $filename already exists</error>");
exit;
}
$output->writeln("<info>PageObject was created in $filename</info>");
}
protected function pathToPageObject($class, $suite)
{
}
}

View File

@@ -0,0 +1,142 @@
<?php
namespace Codeception\Command;
use Codeception\Configuration;
use Codeception\Exception\ConfigurationException as ConfigurationException;
use Codeception\Test\Cest;
use Codeception\Test\Interfaces\ScenarioDriven;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\EventDispatcher\EventDispatcher;
/**
* Generates user-friendly text scenarios from scenario-driven tests (Cest, Cept).
*
* * `codecept g:scenarios acceptance` - for all acceptance tests
* * `codecept g:scenarios acceptance --format html` - in html format
* * `codecept g:scenarios acceptance --path doc` - generate scenarios to `doc` dir
*/
class GenerateScenarios extends Command
{
use Shared\FileSystem;
use Shared\Config;
protected function configure()
{
$this->setDefinition([
new InputArgument('suite', InputArgument::REQUIRED, 'suite from which texts should be generated'),
new InputOption('path', 'p', InputOption::VALUE_REQUIRED, 'Use specified path as destination instead of default'),
new InputOption('single-file', '', InputOption::VALUE_NONE, 'Render all scenarios to only one file'),
new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Specify output format: html or text (default)', 'text'),
]);
parent::configure();
}
public function getDescription()
{
return 'Generates text representation for all scenarios';
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$suite = $input->getArgument('suite');
$suiteConf = $this->getSuiteConfig($suite);
$path = $input->getOption('path')
? $input->getOption('path')
: Configuration::dataDir() . 'scenarios';
$format = $input->getOption('format');
@mkdir($path);
if (!is_writable($path)) {
throw new ConfigurationException(
"Path $path is not writable. Please, set valid permissions for folder to store scenarios."
);
}
$path = $path . DIRECTORY_SEPARATOR . $suite;
if (!$input->getOption('single-file')) {
@mkdir($path);
}
$suiteManager = new \Codeception\SuiteManager(new EventDispatcher(), $suite, $suiteConf);
if ($suiteConf['bootstrap']) {
if (file_exists($suiteConf['path'] . $suiteConf['bootstrap'])) {
require_once $suiteConf['path'] . $suiteConf['bootstrap'];
}
}
$tests = $this->getTests($suiteManager);
$scenarios = "";
foreach ($tests as $test) {
if (!($test instanceof ScenarioDriven)) {
continue;
}
$feature = $test->getScenarioText($format);
$name = $this->underscore(basename($test->getFileName(), '.php'));
// create separate file for each test in Cest
if ($test instanceof Cest && !$input->getOption('single-file')) {
$name .= '.' . $this->underscore($test->getTestMethod());
}
if ($input->getOption('single-file')) {
$scenarios .= $feature;
$output->writeln("* $name rendered");
} else {
$feature = $this->decorate($feature, $format);
$this->createFile($path . DIRECTORY_SEPARATOR . $name . $this->formatExtension($format), $feature, true);
$output->writeln("* $name generated");
}
}
if ($input->getOption('single-file')) {
$this->createFile($path . $this->formatExtension($format), $this->decorate($scenarios, $format), true);
}
}
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;
}
}

View File

@@ -0,0 +1,75 @@
<?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>");
exit;
}
$output->writeln("<info>StepObject was created in $filename</info>");
}
}

View File

@@ -0,0 +1,134 @@
<?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;
}
$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>");
}
private function containsInvalidCharacters($suite)
{
return preg_match('#[^A-Za-z0-9_]#', $suite) ? true : false;
}
}

View File

@@ -0,0 +1,60 @@
<?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;
}
$output->writeln("<info>Test was created in $filename</info>");
}
}

View File

@@ -0,0 +1,72 @@
<?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;
}
$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>");
}
}

View File

@@ -0,0 +1,71 @@
<?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>");
}
}
}

View File

@@ -0,0 +1,55 @@
<?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();
}
}

View File

@@ -0,0 +1,558 @@
<?php
namespace Codeception\Command;
use Codeception\Codecept;
use Codeception\Configuration;
use Codeception\Util\PathResolver;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Executes tests.
*
* Usage:
*
* * `codecept run acceptance`: run all acceptance tests
* * `codecept run tests/acceptance/MyCept.php`: run only MyCept
* * `codecept run acceptance MyCept`: same as above
* * `codecept run acceptance MyCest:myTestInIt`: run one test from a Cest
* * `codecept run acceptance checkout.feature`: run feature-file
* * `codecept run acceptance -g slow`: run tests from *slow* group
* * `codecept run unit,functional`: run only unit and functional suites
*
* Verbosity modes:
*
* * `codecept run -v`:
* * `codecept run --steps`: print step-by-step execution
* * `codecept run -vv`:
* * `codecept run --debug`: print steps and debug information
* * `codecept run -vvv`: print internal debug information
*
* Load config:
*
* * `codecept run -c path/to/another/config`: from another dir
* * `codecept run -c another_config.yml`: from another config file
*
* Override config values:
*
* * `codecept run -o "settings: shuffle: true"`: enable shuffle
* * `codecept run -o "settings: lint: false"`: disable linting
* * `codecept run -o "reporters: report: \Custom\Reporter" --report`: use custom reporter
*
* Run with specific extension
*
* * `codecept run --ext Recorder` run with Recorder extension enabled
* * `codecept run --ext DotReporter` run with DotReporter printer
* * `codecept run --ext "My\Custom\Extension"` run with an extension loaded by class name
*
* Full reference:
* ```
* Arguments:
* suite suite to be tested
* test test to be run
*
* Options:
* -o, --override=OVERRIDE Override config values (multiple values allowed)
* --config (-c) Use custom path for config
* --report Show output in compact style
* --html Generate html with results (default: "report.html")
* --xml Generate JUnit XML Log (default: "report.xml")
* --tap Generate Tap Log (default: "report.tap.log")
* --json Generate Json Log (default: "report.json")
* --colors Use colors in output
* --no-colors Force no colors in output (useful to override config file)
* --silent Only outputs suite names and final results
* --steps Show steps in output
* --debug (-d) Show debug and scenario output
* --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")
* --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
* --help (-h) Display this help message.
* --quiet (-q) Do not output any message.
* --verbose (-v|vv|vvv) Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
* --version (-V) Display this application version.
* --ansi Force ANSI output.
* --no-ansi Disable ANSI output.
* --no-interaction (-n) Do not ask any interactive question.
* ```
*
*/
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('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(
'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-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'),
]);
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->ensureCurlIsAvailable();
$this->options = $input->getOptions();
$this->output = $output;
// load config
$config = $this->getGlobalConfig();
// update config from options
if (count($this->options['override'])) {
$config = $this->overrideConfig($this->options['override']);
}
if ($this->options['ext']) {
$config = $this->enableExtensions($this->options['ext']);
}
if (!$this->options['colors']) {
$this->options['colors'] = $config['settings']['colors'];
}
if (!$this->options['silent']) {
$this->output->writeln(
Codecept::versionString() . "\nPowered by " . \PHPUnit\Runner\Version::getVersionString()
);
}
if ($this->options['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',
'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-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['no-colors'] || !$userOptions['ansi']) {
$userOptions['colors'] = false;
}
if ($this->options['group']) {
$userOptions['groups'] = $this->options['group'];
}
if ($this->options['skip-group']) {
$userOptions['excludeGroups'] = $this->options['skip-group'];
}
if ($this->options['report']) {
$userOptions['silent'] = true;
}
if ($this->options['coverage-xml'] or $this->options['coverage-html'] or $this->options['coverage-text'] or $this->options['coverage-crap4j'] or $this->options['coverage-phpunit']) {
$this->options['coverage'] = true;
}
if (!$userOptions['ansi'] && $input->getOption('colors')) {
$userOptions['colors'] = true; // turn on colors even in non-ansi mode if strictly passed
}
$suite = $input->getArgument('suite');
$test = $input->getArgument('test');
if ($this->options['group']) {
$this->output->writeln(sprintf("[Groups] <info>%s</info> ", implode(', ', $this->options['group'])));
}
if ($input->getArgument('test')) {
$this->options['steps'] = true;
}
if (!$test) {
// Check if suite is given and is in an included path
if (!empty($suite) && !empty($config['include'])) {
$isIncludeTest = false;
// Remember original projectDir
$projectDir = Configuration::projectDir();
foreach ($config['include'] as $include) {
// Find if the suite begins with an include path
if (strpos($suite, $include) === 0) {
// Use include config
$config = Configuration::config($projectDir.$include);
if (!isset($config['paths']['tests'])) {
throw new \RuntimeException(
sprintf("Included '%s' has no tests path configured", $include)
);
}
$testsPath = $include . DIRECTORY_SEPARATOR. $config['paths']['tests'];
try {
list(, $suite, $test) = $this->matchTestFromFilename($suite, $testsPath);
$isIncludeTest = true;
} catch (\InvalidArgumentException $e) {
// Incorrect include match, continue trying to find one
continue;
}
} else {
$result = $this->matchSingleTest($suite, $config);
if ($result) {
list(, $suite, $test) = $result;
}
}
}
// Restore main config
if (!$isIncludeTest) {
$config = Configuration::config($projectDir);
}
} elseif (!empty($suite)) {
$result = $this->matchSingleTest($suite, $config);
if ($result) {
list(, $suite, $test) = $result;
}
}
}
if ($test) {
$filter = $this->matchFilteredTestName($test);
$userOptions['filter'] = $filter;
}
$this->codecept = new Codecept($userOptions);
if ($suite and $test) {
$this->codecept->run($suite, $test, $config);
}
// Run all tests of given suite or all suites
if (!$test) {
$suites = $suite ? explode(',', $suite) : Configuration::suites();
$this->executed = $this->runSuites($suites, $this->options['skip']);
if (!empty($config['include']) and !$suite) {
$current_dir = Configuration::projectDir();
$suites += $config['include'];
$this->runIncludedSuites($config['include'], $current_dir);
}
if ($this->executed === 0) {
throw new \RuntimeException(
sprintf("Suite '%s' could not be found", implode(', ', $suites))
);
}
}
$this->codecept->printResult();
if (!$input->getOption('no-exit')) {
if (!$this->codecept->getResult()->wasSuccessful()) {
exit(1);
}
}
}
protected function matchSingleTest($suite, $config)
{
// Workaround when codeception.yml is inside tests directory and tests path is set to "."
// @see https://github.com/Codeception/Codeception/issues/4432
if (isset($config['paths']['tests']) && $config['paths']['tests'] === '.' && !preg_match('~^\.[/\\\]~', $suite)) {
$suite = './' . $suite;
}
// running a single test when suite has a configured path
if (isset($config['suites'])) {
foreach ($config['suites'] as $s => $suiteConfig) {
if (!isset($suiteConfig['path'])) {
continue;
}
$testsPath = $config['paths']['tests'] . DIRECTORY_SEPARATOR . $suiteConfig['path'];
if ($suiteConfig['path'] === '.') {
$testsPath = $config['paths']['tests'];
}
if (preg_match("~^$testsPath/(.*?)$~", $suite, $matches)) {
$matches[2] = $matches[1];
$matches[1] = $s;
return $matches;
}
}
}
// Run single test without included tests
if (! Configuration::isEmpty() && strpos($suite, $config['paths']['tests']) === 0) {
return $this->matchTestFromFilename($suite, $config['paths']['tests']);
}
}
/**
* Runs included suites recursively
*
* @param array $suites
* @param string $parent_dir
*/
protected function runIncludedSuites($suites, $parent_dir)
{
foreach ($suites as $relativePath) {
$current_dir = rtrim($parent_dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $relativePath;
$config = Configuration::config($current_dir);
$suites = Configuration::suites();
$namespace = $this->currentNamespace();
$this->output->writeln(
"\n<fg=white;bg=magenta>\n[$namespace]: tests from $current_dir\n</fg=white;bg=magenta>"
);
$this->executed += $this->runSuites($suites, $this->options['skip']);
if (!empty($config['include'])) {
$this->runIncludedSuites($config['include'], $current_dir);
}
}
}
protected function currentNamespace()
{
$config = Configuration::config();
if (!$config['namespace']) {
throw new \RuntimeException(
"Can't include into runner suite without a namespace;\n"
. "Please add `namespace` section into included codeception.yml file"
);
}
return $config['namespace'];
}
protected function runSuites($suites, $skippedSuites = [])
{
$executed = 0;
foreach ($suites as $suite) {
if (in_array($suite, $skippedSuites)) {
continue;
}
if (!in_array($suite, Configuration::suites())) {
continue;
}
$this->codecept->run($suite);
$executed++;
}
return $executed;
}
protected function matchTestFromFilename($filename, $testsPath)
{
$testsPath = str_replace(['//', '\/', '\\'], '/', $testsPath);
$filename = str_replace(['//', '\/', '\\'], '/', $filename);
$res = preg_match("~^$testsPath/(.*?)(?>/(.*))?$~", $filename, $matches);
if (!$res) {
throw new \InvalidArgumentException("Test file can't be matched");
}
if (!isset($matches[2])) {
$matches[2] = null;
}
return $matches;
}
private function matchFilteredTestName(&$path)
{
$test_parts = explode(':', $path, 2);
if (count($test_parts) > 1) {
list($path, $filter) = $test_parts;
// use carat to signify start of string like in normal regex
// phpunit --filter matches against the fully qualified method name, so tests actually begin with :
$carat_pos = strpos($filter, '^');
if ($carat_pos !== false) {
$filter = substr_replace($filter, ':', $carat_pos, 1);
}
return $filter;
}
return null;
}
protected function passedOptionKeys(InputInterface $input)
{
$options = [];
$request = (string)$input;
$tokens = explode(' ', $request);
foreach ($tokens as $token) {
$token = preg_replace('~=.*~', '', $token); // strip = from options
if (empty($token)) {
continue;
}
if ($token == '--') {
break; // there should be no options after ' -- ', only arguments
}
if (substr($token, 0, 2) === '--') {
$options[] = substr($token, 2);
} elseif ($token[0] === '-') {
$shortOption = substr($token, 1);
$options[] = $this->getDefinition()->getOptionForShortcut($shortOption)->getName();
}
}
return $options;
}
protected function booleanOptions(InputInterface $input, $options = [])
{
$values = [];
$request = (string)$input;
foreach ($options as $option => $defaultValue) {
if (strpos($request, "--$option")) {
$values[$option] = $input->getOption($option) ? $input->getOption($option) : $defaultValue;
} else {
$values[$option] = false;
}
}
return $values;
}
private function ensureCurlIsAvailable()
{
if (!extension_loaded('curl')) {
throw new \Exception(
"Codeception requires CURL extension installed to make tests run\n"
. "If you are not sure, how to install CURL, 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 CURL enabled."
);
}
}
}

View File

@@ -0,0 +1,311 @@
<?php
namespace Codeception\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Codeception\Codecept;
/**
* Auto-updates phar archive from official site: 'http://codeception.com/codecept.phar' .
*
* * `php codecept.phar self-update`
*
* @author Franck Cassedanne <franck@cassedanne.com>
*/
class SelfUpdate extends Command
{
/**
* Class constants
*/
const NAME = 'Codeception';
const GITHUB_REPO = 'Codeception/Codeception';
const PHAR_URL = 'http://codeception.com/releases/%s/codecept.phar';
const PHAR_URL_PHP54 = 'http://codeception.com/releases/%s/php54/codecept.phar';
/**
* Holds the current script filename.
* @var string
*/
protected $filename;
/**
* Holds the live version string.
* @var string
*/
protected $liveVersion;
/**
* {@inheritdoc}
*/
protected function configure()
{
if (isset($_SERVER['argv'], $_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
)
);
$output->writeln("\n<info>Checking for a new version...</info>\n");
try {
$latestVersion = $this->getLatestStableVersion();
if ($this->isOutOfDate($version, $latestVersion)) {
$output->writeln(
sprintf(
'A newer version is available: <comment>%s</comment>',
$latestVersion
)
);
if (!$input->getOption('no-interaction')) {
$dialog = $this->getHelperSet()->get('question');
$question = new ConfirmationQuestion("\n<question>Do you want to update?</question> ", false);
if (!$dialog->ask($input, $output, $question)) {
$output->writeln("\n<info>Bye-bye!</info>\n");
return;
}
}
$output->writeln("\n<info>Updating...</info>");
$this->retrievePharFile($latestVersion, $output);
} else {
$output->writeln('You are already using the latest version.');
}
} catch (\Exception $e) {
$output->writeln(
sprintf(
"<error>\n%s\n</error>",
$e->getMessage()
)
);
}
}
/**
* Checks whether the provided version is current.
*
* @param string $version The version number to check.
* @param string $latestVersion Latest stable version
* @return boolean Returns True if a new version is available.
*/
private function isOutOfDate($version, $latestVersion)
{
return -1 != version_compare($version, $latestVersion, '>=');
}
/**
* @return string
*/
private function getLatestStableVersion()
{
$stableVersions = $this->filterStableVersions(
$this->getGithubTags(self::GITHUB_REPO)
);
return array_reduce(
$stableVersions,
function ($a, $b) {
return version_compare($a, $b, '>') ? $a : $b;
}
);
}
/**
* @param array $tags
* @return array
*/
private function filterStableVersions($tags)
{
return array_filter($tags, function ($tag) {
return preg_match('/^[0-9]+\.[0-9]+\.[0-9]+$/', $tag);
});
}
/**
* Returns an array of tags from a github repo.
*
* @param string $repo The repository name to check upon.
* @return array
*/
protected function getGithubTags($repo)
{
$jsonTags = $this->retrieveContentFromUrl(
'https://api.github.com/repos/' . $repo . '/tags'
);
return array_map(
function ($tag) {
return $tag['name'];
},
json_decode($jsonTags, true)
);
}
/**
* Retrieves the body-content from the provided URL.
*
* @param string $url
* @return string
* @throws \Exception if status code is above 300
*/
private function retrieveContentFromUrl($url)
{
$ctx = $this->prepareContext($url);
$body = file_get_contents($url, 0, $ctx);
if (isset($http_response_header)) {
$code = substr($http_response_header[0], 9, 3);
if (floor($code / 100) > 3) {
throw new \Exception($http_response_header[0]);
}
} else {
throw new \Exception('Request failed.');
}
return $body;
}
/**
* Add proxy support to context if environment variable was set up
*
* @param array $opt context options
* @param string $url
*/
private function prepareProxy(&$opt, $url)
{
$scheme = parse_url($url)['scheme'];
if ($scheme === 'http' && (!empty($_SERVER['HTTP_PROXY']) || !empty($_SERVER['http_proxy']))) {
$proxy = !empty($_SERVER['http_proxy']) ? $_SERVER['http_proxy'] : $_SERVER['HTTP_PROXY'];
}
if ($scheme === 'https' && (!empty($_SERVER['HTTPS_PROXY']) || !empty($_SERVER['https_proxy']))) {
$proxy = !empty($_SERVER['https_proxy']) ? $_SERVER['https_proxy'] : $_SERVER['HTTPS_PROXY'];
}
if (!empty($proxy)) {
$proxy = str_replace(['http://', 'https://'], ['tcp://', 'ssl://'], $proxy);
$opt['http']['proxy'] = $proxy;
}
}
/**
* Preparing context for request
* @param $url
*
* @return resource
*/
private function prepareContext($url)
{
$opts = [
'http' => [
'follow_location' => 1,
'max_redirects' => 20,
'timeout' => 10,
'user_agent' => self::NAME
]
];
$this->prepareProxy($opts, $url);
return stream_context_create($opts);
}
/**
* Retrieves the latest phar file.
*
* @param string $version
* @param OutputInterface $output
* @throws \Exception
*/
protected function retrievePharFile($version, OutputInterface $output)
{
$temp = basename($this->filename, '.phar') . '-temp.phar';
try {
$sourceUrl = $this->getPharUrl($version);
if (@copy($sourceUrl, $temp)) {
chmod($temp, 0777 & ~umask());
// test the phar validity
$phar = new \Phar($temp);
// free the variable to unlock the file
unset($phar);
rename($temp, $this->filename);
} else {
throw new \Exception('Request failed.');
}
} catch (\Exception $e) {
if (!$e instanceof \UnexpectedValueException
&& !$e instanceof \PharException
) {
throw $e;
}
unlink($temp);
$output->writeln(
sprintf(
"<error>\nSomething went wrong (%s).\nPlease re-run this again.</error>\n",
$e->getMessage()
)
);
}
$output->writeln(
sprintf(
"\n<comment>%s</comment> has been updated.\n",
$this->filename
)
);
}
/**
* Returns Phar file URL for specified version
*
* @param string $version
* @return string
*/
protected function getPharUrl($version)
{
$sourceUrl = self::PHAR_URL;
if (version_compare(PHP_VERSION, '7.0.0', '<')) {
$sourceUrl = self::PHAR_URL_PHP54;
}
return sprintf($sourceUrl, $version);
}
}

View File

@@ -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('--config-option should have config passed as "key:value"');
}
$value = array_pop($keys);
$yaml = '';
for ($ind = 0; count($keys); $ind += 2) {
$yaml .= "\n" . str_repeat(' ', $ind) . array_shift($keys) . ': ';
}
$yaml .= $value;
try {
$config = Yaml::parse($yaml);
} catch (ParseException $e) {
throw new \Codeception\Exception\ParseException("Overridden config can't be parsed: \n$yaml\n" . $e->getParsedLine());
}
$updatedConfig = array_merge_recursive($updatedConfig, $config);
}
return Configuration::append($updatedConfig);
}
protected function enableExtensions($extensions)
{
$config = ['extensions' => ['enabled' => []]];
foreach ($extensions as $name) {
if (!class_exists($name)) {
$className = 'Codeception\\Extension\\' . ucfirst($name);
if (!class_exists($className)) {
throw new InvalidOptionException("Extension $name can't be loaded (tried by $name and $className)");
}
$config['extensions']['enabled'][] = $className;
continue;
}
$config['extensions']['enabled'][] = $name;
}
return Configuration::append($config);
}
}

View File

@@ -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 = null)
{
if (file_exists($filename) && !$force) {
return false;
}
file_put_contents($filename, $contents, $flags);
return true;
}
}

View File

@@ -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'));
}
}

View File

@@ -0,0 +1,767 @@
<?php
namespace Codeception;
use Codeception\Exception\ConfigurationException;
use Codeception\Lib\ParamsLoader;
use Codeception\Util\Autoload;
use Codeception\Util\Template;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
class Configuration
{
protected static $suites = [];
/**
* @var array Current configuration
*/
protected static $config = null;
/**
* @var array environmental files configuration cache
*/
protected static $envConfig = [];
/**
* @var string Directory containing main configuration file.
* @see self::projectDir()
*/
protected static $dir = null;
/**
* @var string Current project output directory.
*/
protected static $outputDir = null;
/**
* @var string Current project data directory. This directory is used to hold
* sql dumps and other things needed for current project tests.
*/
protected static $dataDir = null;
/**
* @var string Directory with test support files like Actors, Helpers, PageObjects, etc
*/
protected static $supportDir = null;
/**
* @var string Directory containing environment configuration files.
*/
protected static $envsDir = null;
/**
* @var string Directory containing tests and suites of the current project.
*/
protected static $testsDir = null;
public static $lock = false;
protected static $di;
/**
* @var array Default config
*/
public static $defaultConfig = [
'actor_suffix'=> 'Tester',
'namespace' => '',
'include' => [],
'paths' => [],
'extends' => null,
'suites' => [],
'modules' => [],
'extensions' => [
'enabled' => [],
'config' => [],
'commands' => [],
],
'reporters' => [
'xml' => 'Codeception\PHPUnit\Log\JUnit',
'html' => 'Codeception\PHPUnit\ResultPrinter\HTML',
'report' => 'Codeception\PHPUnit\ResultPrinter\Report',
'tap' => 'PHPUnit\Util\Log\TAP',
'json' => 'PHPUnit\Util\Log\JSON',
],
'groups' => [],
'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
],
'coverage' => [],
'params' => [],
'gherkin' => []
];
public static $defaultSuiteSettings = [
'actor' => null,
'class_name' => null, // Codeception <2.3 compatibility
'modules' => [
'enabled' => [],
'config' => [],
'depends' => []
],
'path' => null,
'extends' => null,
'namespace' => null,
'groups' => [],
'formats' => [],
'shuffle' => false,
'extensions' => [ // suite extensions
'enabled' => [],
'config' => [],
],
'error_level' => 'E_ALL & ~E_STRICT & ~E_DEPRECATED',
];
protected static $params;
/**
* Loads global config file which is `codeception.yml` by default.
* When config is already loaded - returns it.
*
* @param null $configFile
* @return array
* @throws Exception\ConfigurationException
*/
public static function config($configFile = null)
{
if (!$configFile && self::$config) {
return self::$config;
}
if (self::$config && self::$lock) {
return self::$config;
}
if ($configFile === null) {
$configFile = getcwd() . DIRECTORY_SEPARATOR . 'codeception.yml';
}
if (is_dir($configFile)) {
$configFile = $configFile . DIRECTORY_SEPARATOR . 'codeception.yml';
}
$dir = realpath(dirname($configFile));
self::$dir = $dir;
$configDistFile = $dir . DIRECTORY_SEPARATOR . 'codeception.dist.yml';
if (!(file_exists($configDistFile) || file_exists($configFile))) {
throw new ConfigurationException("Configuration file could not be found.\nRun `bootstrap` to initialize Codeception.", 404);
}
// Preload config to retrieve params such that they are applied to codeception config file below
$tempConfig = self::$defaultConfig;
$distConfigContents = "";
if (file_exists($configDistFile)) {
$distConfigContents = file_get_contents($configDistFile);
$tempConfig = self::mergeConfigs($tempConfig, self::getConfFromContents($distConfigContents, $configDistFile));
}
$configContents = "";
if (file_exists($configFile)) {
$configContents = file_get_contents($configFile);
$tempConfig = self::mergeConfigs($tempConfig, self::getConfFromContents($configContents, $configFile));
}
self::prepareParams($tempConfig);
// load config using params
$config = self::mergeConfigs(self::$defaultConfig, self::getConfFromContents($distConfigContents, $configDistFile));
$config = self::mergeConfigs($config, self::getConfFromContents($configContents, $configFile));
if ($config == self::$defaultConfig) {
throw new ConfigurationException("Configuration file is invalid");
}
// we check for the "extends" key in the yml file
if (isset($config['extends'])) {
// and now we search for the file
$presetFilePath = codecept_absolute_path($config['extends']);
if (file_exists($presetFilePath)) {
// and merge it with our configuration file
$config = self::mergeConfigs(self::getConfFromFile($presetFilePath), $config);
}
}
self::$config = $config;
// compatibility with suites created by Codeception < 2.3.0
if (!isset($config['paths']['output']) and isset($config['paths']['log'])) {
$config['paths']['output'] = $config['paths']['log'];
}
if (isset(self::$config['actor'])) {
self::$config['actor_suffix'] = self::$config['actor']; // old compatibility
}
if (!isset($config['paths']['support']) and isset($config['paths']['helpers'])) {
$config['paths']['support'] = $config['paths']['helpers'];
}
if (!isset($config['paths']['output'])) {
throw new ConfigurationException('Output path is not defined by key "paths: output"');
}
self::$outputDir = $config['paths']['output'];
// fill up includes with wildcard expansions
$config['include'] = self::expandWildcardedIncludes($config['include']);
// config without tests, for inclusion of other configs
if (count($config['include'])) {
self::$config = $config;
if (!isset($config['paths']['tests'])) {
return $config;
}
}
if (!isset($config['paths']['tests'])) {
throw new ConfigurationException(
'Tests directory is not defined in Codeception config by key "paths: tests:"'
);
}
if (!isset($config['paths']['data'])) {
throw new ConfigurationException('Data path is not defined Codeception config by key "paths: data"');
}
if (!isset($config['paths']['support'])) {
throw new ConfigurationException('Helpers path is not defined by key "paths: support"');
}
self::$dataDir = $config['paths']['data'];
self::$supportDir = $config['paths']['support'];
self::$testsDir = $config['paths']['tests'];
if (isset($config['paths']['envs'])) {
self::$envsDir = $config['paths']['envs'];
}
Autoload::addNamespace(self::$config['namespace'], self::supportDir());
self::loadBootstrap($config['settings']['bootstrap']);
self::loadSuites();
return $config;
}
protected static function loadBootstrap($bootstrap)
{
if (!$bootstrap) {
return;
}
$bootstrap = self::$dir . DIRECTORY_SEPARATOR . self::$testsDir . DIRECTORY_SEPARATOR . $bootstrap;
if (file_exists($bootstrap)) {
include_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
*/
protected static function getConfFromFile($filename, $nonExistentValue = [])
{
if (file_exists($filename)) {
$yaml = file_get_contents($filename);
return self::getConfFromContents($yaml, $filename);
}
return $nonExistentValue;
}
/**
* Returns all possible suite configurations according environment rules.
* Suite configurations will contain `current_environment` key which specifies what environment used.
*
* @param $suite
* @return array
*/
public static function suiteEnvironments($suite)
{
$settings = self::suiteSettings($suite, self::config());
if (!isset($settings['env']) || !is_array($settings['env'])) {
return [];
}
$environments = [];
foreach ($settings['env'] as $env => $envConfig) {
$environments[$env] = $envConfig ? self::mergeConfigs($settings, $envConfig) : $settings;
$environments[$env]['current_environment'] = $env;
}
return $environments;
}
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'], 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 (strcmp(self::$outputDir[0], "/") !== 0) {
$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."
);
}
return $dir;
}
/**
* Compatibility alias to `Configuration::logDir()`
* @return string
*/
public static function logDir()
{
return self::outputDir();
}
/**
* Returns path to the root of your project.
* Basically returns path to current `codeception.yml` loaded.
* Use this method instead of `__DIR__`, `getcwd()` or anything else.
* @return string
*/
public static function projectDir()
{
return self::$dir . DIRECTORY_SEPARATOR;
}
/**
* Returns path to tests directory
*
* @return string
*/
public static function testsDir()
{
return self::$dir . DIRECTORY_SEPARATOR . self::$testsDir . DIRECTORY_SEPARATOR;
}
/**
* Return current path to `_envs` dir.
* Use it to store environment specific configuration.
*
* @return string
*/
public static function envsDir()
{
if (!self::$envsDir) {
return null;
}
return self::$dir . DIRECTORY_SEPARATOR . self::$envsDir . DIRECTORY_SEPARATOR;
}
/**
* Is this a meta-configuration file that just points to other `codeception.yml`?
* If so, it may have no tests by itself.
*
* @return bool
*/
public static function isEmpty()
{
return !(bool)self::$testsDir;
}
/**
* Adds parameters to config
*
* @param array $config
* @return array
*/
public static function append(array $config = [])
{
self::$config = self::mergeConfigs(self::$config, $config);
if (isset(self::$config['paths']['output'])) {
self::$outputDir = self::$config['paths']['output'];
}
if (isset(self::$config['paths']['data'])) {
self::$dataDir = self::$config['paths']['data'];
}
if (isset(self::$config['paths']['support'])) {
self::$supportDir = self::$config['paths']['support'];
}
if (isset(self::$config['paths']['tests'])) {
self::$testsDir = self::$config['paths']['tests'];
}
return self::$config;
}
public static function mergeConfigs($a1, $a2)
{
if (!is_array($a1)) {
return $a2;
}
if (!is_array($a2)) {
return $a1;
}
$res = [];
// for sequential arrays
if (isset($a1[0], $a2[0])) {
return array_merge_recursive($a2, $a1);
}
// 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
*/
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
*/
protected static function expandWildcardedIncludes(array $includes)
{
if (empty($includes)) {
return $includes;
}
$expandedIncludes = [];
foreach ($includes as $include) {
$expandedIncludes = array_merge($expandedIncludes, self::expandWildcardsFor($include));
}
return $expandedIncludes;
}
/**
* Finds config files in given wildcarded include path.
* Returns the expanded paths or the original if not a wildcard.
*
* @param $include
* @return array
* @throws ConfigurationException
*/
protected static function expandWildcardsFor($include)
{
if (1 !== preg_match('/[\?\.\*]/', $include)) {
return [$include,];
}
try {
$configFiles = Finder::create()->files()
->name('/codeception(\.dist\.yml|\.yml)/')
->in(self::$dir . DIRECTORY_SEPARATOR . $include);
} catch (\InvalidArgumentException $e) {
throw new ConfigurationException(
"Configuration file(s) could not be found in \"$include\"."
);
}
$paths = [];
foreach ($configFiles as $file) {
$paths[] = codecept_relative_path($file->getPath());
}
return $paths;
}
private static function prepareParams($settings)
{
self::$params = [];
$paramsLoader = new ParamsLoader();
foreach ($settings['params'] as $paramStorage) {
static::$params = array_merge(self::$params, $paramsLoader->load($paramStorage));
}
}
}

View File

@@ -0,0 +1,175 @@
<?php
namespace Codeception\Coverage;
use Codeception\Configuration;
use Codeception\Exception\ConfigurationException;
use Codeception\Exception\ModuleException;
use Symfony\Component\Finder\Finder;
class Filter
{
/**
* @var \SebastianBergmann\CodeCoverage\CodeCoverage
*/
protected $phpCodeCoverage = null;
/**
* @var Filter
*/
protected static $c3;
/**
* @var \SebastianBergmann\CodeCoverage\Filter
*/
protected $filter = null;
public function __construct(\SebastianBergmann\CodeCoverage\CodeCoverage $phpCoverage)
{
$this->phpCodeCoverage = $phpCoverage
? $phpCoverage
: new \SebastianBergmann\CodeCoverage\CodeCoverage;
$this->filter = $this->phpCodeCoverage->filter();
}
/**
* @param \SebastianBergmann\CodeCoverage\CodeCoverage $phpCoverage
* @return Filter
*/
public static function setup(\SebastianBergmann\CodeCoverage\CodeCoverage $phpCoverage)
{
self::$c3 = new self($phpCoverage);
return self::$c3;
}
/**
* @return null|\SebastianBergmann\CodeCoverage\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) {
$filter->addFileToWhitelist($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) {
$finder = strpos($fileOrDir, '*') === false
? [Configuration::projectDir() . DIRECTORY_SEPARATOR . $fileOrDir]
: $this->matchWildcardPattern($fileOrDir);
foreach ($finder as $file) {
$filter->removeFileFromWhitelist($file);
}
}
}
return $this;
}
/**
* @param $config
* @return Filter
*/
public function blackList($config)
{
$filter = $this->filter;
if (!isset($config['coverage'])) {
return $this;
}
$coverage = $config['coverage'];
if (isset($coverage['blacklist'])) {
if (!method_exists($filter, 'addFileToBlacklist')) {
throw new ModuleException($this, 'The blacklist functionality has been removed from PHPUnit 5,'
. ' please remove blacklist section from configuration.');
}
if (isset($coverage['blacklist']['include'])) {
foreach ($coverage['blacklist']['include'] as $fileOrDir) {
$finder = strpos($fileOrDir, '*') === false
? [Configuration::projectDir() . DIRECTORY_SEPARATOR . $fileOrDir]
: $this->matchWildcardPattern($fileOrDir);
foreach ($finder as $file) {
$filter->addFileToBlacklist($file);
}
}
}
if (isset($coverage['blacklist']['exclude'])) {
foreach ($coverage['blacklist']['exclude'] as $fileOrDir) {
$finder = strpos($fileOrDir, '*') === false
? [Configuration::projectDir() . DIRECTORY_SEPARATOR . $fileOrDir]
: $this->matchWildcardPattern($fileOrDir);
foreach ($finder as $file) {
$filter->removeFileFromBlacklist($file);
}
}
}
}
return $this;
}
protected function matchWildcardPattern($pattern)
{
$finder = Finder::create();
$fileOrDir = str_replace('\\', '/', $pattern);
$parts = explode('/', $fileOrDir);
$file = array_pop($parts);
$finder->name($file);
if (count($parts)) {
$last_path = array_pop($parts);
if ($last_path === '*') {
$finder->in(Configuration::projectDir() . implode('/', $parts));
} else {
$finder->in(Configuration::projectDir() . implode('/', $parts) . '/' . $last_path);
}
}
$finder->ignoreVCS(true)->files();
return $finder;
}
/**
* @return \SebastianBergmann\CodeCoverage\Filter
*/
public function getFilter()
{
return $this->filter;
}
}

View File

@@ -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());
}
}

View File

@@ -0,0 +1,238 @@
<?php
namespace Codeception\Coverage\Subscriber;
use Codeception\Configuration;
use Codeception\Coverage\SuiteSubscriber;
use Codeception\Event\StepEvent;
use Codeception\Event\SuiteEvent;
use Codeception\Event\TestEvent;
use Codeception\Events;
use Codeception\Exception\ModuleException;
use Codeception\Exception\RemoteException;
/**
* When collecting code coverage data from local server HTTP requests are sent to c3.php file.
* Coverage Collection is started by sending cookies/headers.
* Result is taken from the local file and merged with local code coverage results.
*
* Class LocalServer
* @package Codeception\Coverage\Subscriber
*/
class LocalServer extends SuiteSubscriber
{
// headers
const COVERAGE_HEADER = 'X-Codeception-CodeCoverage';
const COVERAGE_HEADER_ERROR = 'X-Codeception-CodeCoverage-Error';
const COVERAGE_HEADER_CONFIG = 'X-Codeception-CodeCoverage-Config';
const COVERAGE_HEADER_SUITE = 'X-Codeception-CodeCoverage-Suite';
// cookie names
const COVERAGE_COOKIE = 'CODECEPTION_CODECOVERAGE';
const COVERAGE_COOKIE_ERROR = 'CODECEPTION_CODECOVERAGE_ERROR';
protected $suiteName;
protected $c3Access = [
'http' => [
'method' => "GET",
'header' => ''
]
];
/**
* @var \Codeception\Lib\Interfaces\Web
*/
protected $module;
public static $events = [
Events::SUITE_BEFORE => 'beforeSuite',
Events::TEST_BEFORE => 'beforeTest',
Events::STEP_AFTER => 'afterStep',
Events::SUITE_AFTER => 'afterSuite',
];
protected function isEnabled()
{
return $this->module && !$this->settings['remote'] && $this->settings['enabled'];
}
public function beforeSuite(SuiteEvent $e)
{
$this->module = $this->getServerConnectionModule($e->getSuite()->getModules());
$this->applySettings($e->getSettings());
if (!$this->isEnabled()) {
return;
}
$this->suiteName = $e->getSuite()->getBaseName();
if ($this->settings['remote_config']) {
$this->addC3AccessHeader(self::COVERAGE_HEADER_CONFIG, $this->settings['remote_config']);
}
$knock = $this->c3Request('clear');
if ($knock === false) {
throw new RemoteException(
'
CodeCoverage Error.
Check the file "c3.php" is included in your application.
We tried to access "/c3/report/clear" but this URI was not accessible.
You can review actual error messages in c3tmp dir.
'
);
}
}
public function beforeTest(TestEvent $e)
{
if (!$this->isEnabled()) {
return;
}
$this->startCoverageCollection($e->getTest()->getName());
}
public function afterStep(StepEvent $e)
{
if (!$this->isEnabled()) {
return;
}
$this->fetchErrors();
}
public function afterSuite(SuiteEvent $e)
{
if (!$this->isEnabled()) {
return;
}
$coverageFile = Configuration::outputDir() . 'c3tmp/codecoverage.serialized';
$retries = 5;
while (!file_exists($coverageFile) && --$retries >= 0) {
usleep(0.5 * 1000000); // 0.5 sec
}
if (!file_exists($coverageFile)) {
if (file_exists(Configuration::outputDir() . 'c3tmp/error.txt')) {
throw new \RuntimeException(file_get_contents(Configuration::outputDir() . 'c3tmp/error.txt'));
}
return;
}
$contents = file_get_contents($coverageFile);
$coverage = @unserialize($contents);
if ($coverage === false) {
return;
}
$this->mergeToPrint($coverage);
}
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('/');
}
$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.
$c3Host = isset($c3Url['host']) ? $c3Url['host'] : 'localhost';
$this->module->setCookie(self::COVERAGE_COOKIE, $value, ['domain' => $c3Host]);
// 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;
}
}
if (!$found) {
$cookies[] = [
'Name' => self::COVERAGE_COOKIE,
'Value' => $value
];
}
$this->module->_setConfig(['cookies' => $cookies]);
}
protected function fetchErrors()
{
try {
$error = $this->module->grabCookie(self::COVERAGE_COOKIE_ERROR);
} catch (ModuleException $e) {
// when a new session is started we can't get cookies because there is no
// current page, but there can be no code coverage error either
$error = null;
}
if (!empty($error)) {
$this->module->resetCookie(self::COVERAGE_COOKIE_ERROR);
throw new RemoteException($error);
}
}
protected function getRemoteError($headers)
{
foreach ($headers as $header) {
if (strpos($header, self::COVERAGE_HEADER_ERROR) === 0) {
throw new RemoteException($header);
}
}
}
protected function addC3AccessHeader($header, $value)
{
$headerString = "$header: $value\r\n";
if (strpos($this->c3Access['http']['header'], $headerString) === false) {
$this->c3Access['http']['header'] .= $headerString;
}
}
protected function applySettings($settings)
{
parent::applySettings($settings);
if (isset($settings['coverage']['remote_context_options'])) {
$this->c3Access = array_replace_recursive($this->c3Access, $settings['coverage']['remote_context_options']);
}
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace Codeception\Coverage\Subscriber;
use Codeception\Configuration;
use Codeception\Coverage\Filter;
use Codeception\Event\PrintResultEvent;
use Codeception\Events;
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
];
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 = new \SebastianBergmann\CodeCoverage\CodeCoverage();
// 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-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'],
false
);
$printer->write($writer->process(self::$coverage, $this->options['colors']));
}
protected function printHtml()
{
$writer = new \SebastianBergmann\CodeCoverage\Report\Html\Facade(
$this->settings['low_limit'],
$this->settings['high_limit'],
sprintf(
', <a href="http://codeception.com">Codeception</a> and <a href="http://phpunit.de/">PHPUnit %s</a>',
\PHPUnit\Runner\Version::id()
)
);
$writer->process(self::$coverage, $this->absolutePath($this->options['coverage-html']));
}
protected function printXml()
{
$writer = new \SebastianBergmann\CodeCoverage\Report\Clover();
$writer->process(self::$coverage, $this->absolutePath($this->options['coverage-xml']));
}
protected function printPHP()
{
$writer = new \SebastianBergmann\CodeCoverage\Report\PHP;
$writer->process(self::$coverage, $this->absolutePath($this->options['coverage']));
}
protected function printText()
{
$writer = new \SebastianBergmann\CodeCoverage\Report\Text(
$this->settings['low_limit'],
$this->settings['high_limit'],
$this->settings['show_uncovered'],
false
);
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 printPHPUnit()
{
$writer = new \SebastianBergmann\CodeCoverage\Report\Xml\Facade(\PHPUnit\Runner\Version::id());
$writer->process(self::$coverage, $this->absolutePath($this->options['coverage-phpunit']));
}
}

View File

@@ -0,0 +1,90 @@
<?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-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 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);
}
}

View File

@@ -0,0 +1,95 @@
<?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 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
];
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 = new \SebastianBergmann\CodeCoverage\CodeCoverage();
} 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];
}
}
$this->coverage->setProcessUncoveredFilesFromWhitelist($this->settings['show_uncovered']);
}
/**
* @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));
Filter::setup($this->coverage)
->whiteList($this->filters)
->blackList($this->filters);
$result->setCodeCoverage($this->coverage);
}
protected function mergeToPrint($coverage)
{
Printer::$coverage->merge($coverage);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Codeception;
interface CustomCommandInterface
{
/**
* returns the name of the command
*
* @return string
*/
public static function getCommandName();
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace Codeception;
/**
* Contains all events dispatched by Codeception.
*
* @author tiger-seo <tiger.seo@gmail.com>
*/
final class Events
{
/**
* Private constructor. This class cannot be instantiated.
*/
private function __construct()
{
}
/**
* The <b>MODULE_INIT</b> event occurs before modules are initialized.
*
* The event listener method receives a {@link Codeception\Event\SuiteEvent} instance.
*/
const MODULE_INIT = 'module.init';
/**
* The <b>SUITE_INIT</b> event occurs when suite is initialized.
* Modules are created and initialized, but Actor class is not loaded.
*
* The event listener method receives a {@link Codeception\Event\SuiteEvent} instance.
*/
const SUITE_INIT = 'suite.init';
/**
* The <b>SUITE_BEFORE</b> event occurs before suite is executed.
*
* The event listener method receives a {@link Codeception\Event\SuiteEvent} instance.
*/
const SUITE_BEFORE = 'suite.before';
/**
* The <b>SUITE_AFTER</b> event occurs after suite has been executed.
*
* The event listener method receives a {@link Codeception\Event\SuiteEvent} instance.
*/
const SUITE_AFTER = 'suite.after';
/**
* The event listener method receives a {@link Codeception\Event\TestEvent} instance.
*/
const TEST_START = 'test.start';
/**
* The event listener method receives a {@link Codeception\Event\TestEvent} instance.
*/
const TEST_BEFORE = 'test.before';
/**
* The event listener method receives a {@link Codeception\Event\StepEvent} instance.
*/
const STEP_BEFORE = 'step.before';
/**
* The event listener method receives a {@link Codeception\Event\StepEvent} instance.
*/
const STEP_AFTER = 'step.after';
/**
* The <b>TEST_FAIL</b> event occurs whenever test has failed.
*
* The event listener method receives a {@link Codeception\Event\FailEvent} instance.
*/
const TEST_FAIL = 'test.fail';
/**
* The <b>TEST_ERROR</b> event occurs whenever test got an error while being executed.
*
* The event listener method receives a {@link Codeception\Event\FailEvent} instance.
*/
const TEST_ERROR = 'test.error';
/**
* The event listener method receives a {@link Codeception\Event\TestEvent} instance.
*/
const TEST_PARSED = 'test.parsed';
/**
* The event listener method receives a {@link Codeception\Event\FailEvent} instance.
*/
const TEST_INCOMPLETE = 'test.incomplete';
/**
* The event listener method receives a {@link Codeception\Event\FailEvent} instance.
*/
const TEST_SKIPPED = 'test.skipped';
/**
* The event listener method receives a {@link Codeception\Event\FailEvent} instance.
*/
const TEST_WARNING = 'test.warning';
/**
* The event listener method receives a {@link Codeception\Event\TestEvent} instance.
*/
const TEST_SUCCESS = 'test.success';
/**
* The event listener method receives a {@link Codeception\Event\TestEvent} instance.
*/
const TEST_AFTER = 'test.after';
/**
* The event listener method receives a {@link Codeception\Event\TestEvent} instance.
*/
const TEST_END = 'test.end';
/**
* The event listener method receives a {@link Codeception\Event\FailEvent} instance.
*/
const TEST_FAIL_PRINT = 'test.fail.print';
/**
* The event listener method receives a {@link Codeception\Event\PrintResultEvent} instance.
*/
const RESULT_PRINT_AFTER = 'result.print.after';
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Codeception;
use Traversable;
class Example implements \ArrayAccess, \Countable, \IteratorAggregate
{
protected $data;
public function __construct($data)
{
$this->data = $data;
}
/**
* Whether a offset exists
* @link http://php.net/manual/en/arrayaccess.offsetexists.php
* @param mixed $offset <p>
* An offset to check for.
* </p>
* @return boolean true on success or false on failure.
* </p>
* <p>
* The return value will be casted to boolean if non-boolean was returned.
* @since 5.0.0
*/
public function offsetExists($offset)
{
return array_key_exists($offset, $this->data);
}
/**
* Offset to retrieve
* @link http://php.net/manual/en/arrayaccess.offsetget.php
* @param mixed $offset <p>
* The offset to retrieve.
* </p>
* @return mixed Can return all value types.
* @since 5.0.0
*/
public function offsetGet($offset)
{
if (!$this->offsetExists($offset)) {
throw new \PHPUnit\Framework\AssertionFailedError("Example $offset doesn't exist");
};
return $this->data[$offset];
}
/**
* Offset to set
* @link http://php.net/manual/en/arrayaccess.offsetset.php
* @param mixed $offset <p>
* The offset to assign the value to.
* </p>
* @param mixed $value <p>
* The value to set.
* </p>
* @return void
* @since 5.0.0
*/
public function offsetSet($offset, $value)
{
$this->data[$offset] = $value;
}
/**
* Offset to unset
* @link http://php.net/manual/en/arrayaccess.offsetunset.php
* @param mixed $offset <p>
* The offset to unset.
* </p>
* @return void
* @since 5.0.0
*/
public function offsetUnset($offset)
{
unset($this->data[$offset]);
}
/**
* Count elements of an object
* @link http://php.net/manual/en/countable.count.php
* @return int The custom count as an integer.
* </p>
* <p>
* The return value is cast to an integer.
* @since 5.1.0
*/
public function count()
{
return count($this->data);
}
/**
* Retrieve an external iterator
* @link http://php.net/manual/en/iteratoraggregate.getiterator.php
* @return Traversable An instance of an object implementing <b>Iterator</b> or
* <b>Traversable</b>
* @since 5.0.0
*/
public function getIterator()
{
return new \ArrayIterator($this->data);
}
}

View File

@@ -0,0 +1,6 @@
<?php
namespace Codeception\Exception;
class ConditionalAssertionFailed extends \PHPUnit\Framework\AssertionFailedError
{
}

View File

@@ -0,0 +1,6 @@
<?php
namespace Codeception\Exception;
class ConfigurationException extends \Exception
{
}

View File

@@ -0,0 +1,6 @@
<?php
namespace Codeception\Exception;
class ConnectionException extends \RuntimeException
{
}

View File

@@ -0,0 +1,6 @@
<?php
namespace Codeception\Exception;
class ContentNotFound extends \PHPUnit\Framework\AssertionFailedError
{
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Codeception\Exception;
use Codeception\Util\Locator;
class ElementNotFound extends \PHPUnit\Framework\AssertionFailedError
{
public function __construct($selector, $message = null)
{
$selector = Locator::humanReadableString($selector);
parent::__construct($message . " element with $selector was not found.");
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Codeception\Exception;
class ExtensionException extends \Exception
{
public function __construct($extension, $message, \Exception $previous = null)
{
parent::__construct($message, $previous);
if (is_object($extension)) {
$extension = get_class($extension);
}
$this->message = $extension . "\n\n" . $this->message;
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Codeception\Exception;
class ExternalUrlException extends \Exception
{
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Codeception\Exception;
class Fail extends \PHPUnit\Framework\AssertionFailedError
{
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Codeception\Exception;
class Incomplete extends \PHPUnit\Framework\IncompleteTestError
{
}

View File

@@ -0,0 +1,6 @@
<?php
namespace Codeception\Exception;
class InjectionException extends \Exception
{
}

View File

@@ -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");
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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}";
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,6 @@
<?php
namespace Codeception\Exception;
class ParseException extends \Exception
{
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Codeception\Exception;
class Skip extends \PHPUnit\Framework\SkippedTestError
{
}

View File

@@ -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";
}
}
}

View File

@@ -0,0 +1,6 @@
<?php
namespace Codeception\Exception;
class TestRuntimeException extends \RuntimeException
{
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,249 @@
<?php
namespace Codeception;
use Codeception\Command\Shared\FileSystem;
use Codeception\Command\Shared\Style;
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 $question
* @param null $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 $message
*/
protected function saySuccess($message)
{
$this->say("<notice> $message </notice>");
}
/**
* Print warning message
* @param $message
*/
protected function sayWarning($message)
{
$this->say("<warning> $message </warning>");
}
/**
* Print info message
* @param $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");
}
}

View File

@@ -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;
}
}

View File

@@ -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];
}
}

View File

@@ -0,0 +1,295 @@
<?php
namespace Codeception\Lib\Connector;
use Aws\Credentials\Credentials;
use Aws\Signature\SignatureV4;
use Codeception\Util\Uri;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Message\Response;
use GuzzleHttp\Post\PostFile;
use Symfony\Component\BrowserKit\Client;
use Symfony\Component\BrowserKit\Response as BrowserKitResponse;
use GuzzleHttp\Url;
use Symfony\Component\BrowserKit\Request as BrowserKitRequest;
class Guzzle extends Client
{
protected $baseUri;
protected $requestOptions = [
'allow_redirects' => false,
'headers' => [],
];
protected $refreshMaxInterval = 0;
protected $awsCredentials = null;
protected $awsSignature = null;
/** @var \GuzzleHttp\Client */
protected $client;
public function setBaseUri($uri)
{
$this->baseUri = $uri;
}
/**
* Sets the maximum allowable timeout interval for a meta tag refresh to
* automatically redirect a request.
*
* A meta tag detected with an interval equal to or greater than $seconds
* would not result in a redirect. A meta tag without a specified interval
* or one with a value less than $seconds would result in the client
* automatically redirecting to the specified URL
*
* @param int $seconds Number of seconds
*/
public function setRefreshMaxInterval($seconds)
{
$this->refreshMaxInterval = $seconds;
}
public function setClient(\GuzzleHttp\Client $client)
{
$this->client = $client;
}
/**
* Sets the request header to the passed value. The header will be
* sent along with the next request.
*
* Passing an empty value clears the header, which is the equivalent
* of calling deleteHeader.
*
* @param string $name the name of the header
* @param string $value the value of the header
*/
public function setHeader($name, $value)
{
if (strval($value) === '') {
$this->deleteHeader($name);
} else {
$this->requestOptions['headers'][$name] = $value;
}
}
/**
* Deletes the header with the passed name from the list of headers
* that will be sent with the request.
*
* @param string $name the name of the header to delete.
*/
public function deleteHeader($name)
{
unset($this->requestOptions['headers'][$name]);
}
/**
* @param string $username
* @param string $password
* @param string $type Default: 'basic'
*/
public function setAuth($username, $password, $type = 'basic')
{
if (!$username) {
unset($this->requestOptions['auth']);
return;
}
$this->requestOptions['auth'] = [$username, $password, $type];
}
/**
* Taken from Mink\BrowserKitDriver
*
* @param Response $response
*
* @return \Symfony\Component\BrowserKit\Response
*/
protected function createResponse(Response $response)
{
$contentType = $response->getHeader('Content-Type');
if (!$contentType) {
$contentType = 'text/html';
}
if (strpos($contentType, 'charset=') === false) {
$body = $response->getBody(true);
if (preg_match('/\<meta[^\>]+charset *= *["\']?([a-zA-Z\-0-9]+)/i', $body, $matches)) {
$contentType .= ';charset=' . $matches[1];
}
$response->setHeader('Content-Type', $contentType);
}
$headers = $response->getHeaders();
$status = $response->getStatusCode();
if ($status < 300 || $status >= 400) {
$matches = [];
$matchesMeta = preg_match(
'/\<meta[^\>]+http-equiv="refresh" content="\s*(\d*)\s*;\s*url=(.*?)"/i',
$response->getBody(true),
$matches
);
if (!$matchesMeta) {
// match by header
preg_match(
'/^\s*(\d*)\s*;\s*url=(.*)/i',
(string)$response->getHeader('Refresh'),
$matches
);
}
if ((!empty($matches)) && (empty($matches[1]) || $matches[1] < $this->refreshMaxInterval)) {
$uri = $this->getAbsoluteUri($matches[2]);
$partsUri = parse_url($uri);
$partsCur = parse_url($this->getHistory()->current()->getUri());
foreach ($partsCur as $key => $part) {
if ($key === 'fragment') {
continue;
}
if (!isset($partsUri[$key]) || $partsUri[$key] !== $part) {
$status = 302;
$headers['Location'] = $matchesMeta ? htmlspecialchars_decode($uri) : $uri;
break;
}
}
}
}
return new BrowserKitResponse($response->getBody(), $status, $headers);
}
public function getAbsoluteUri($uri)
{
$baseUri = $this->baseUri;
if (strpos($uri, '://') === false && strpos($uri, '//') !== 0) {
if (strpos($uri, '/') === 0) {
$baseUriPath = parse_url($baseUri, PHP_URL_PATH);
if (!empty($baseUriPath) && strpos($uri, $baseUriPath) === 0) {
$uri = substr($uri, strlen($baseUriPath));
}
return Uri::appendPath((string)$baseUri, $uri);
}
// relative url
if (!$this->getHistory()->isEmpty()) {
return Uri::mergeUrls((string)$this->getHistory()->current()->getUri(), $uri);
}
}
return Uri::mergeUrls($baseUri, $uri);
}
protected function doRequest($request)
{
/** @var $request BrowserKitRequest **/
$requestOptions = [
'body' => $this->extractBody($request),
'cookies' => $this->extractCookies($request),
'headers' => $this->extractHeaders($request)
];
$requestOptions = array_replace_recursive($requestOptions, $this->requestOptions);
$guzzleRequest = $this->client->createRequest(
$request->getMethod(),
$request->getUri(),
$requestOptions
);
foreach ($this->extractFiles($request) as $postFile) {
$guzzleRequest->getBody()->addFile($postFile);
}
// Let BrowserKit handle redirects
try {
if (null !== $this->awsCredentials) {
$response = $this->client->send($this->awsSignature->signRequest($guzzleRequest, $this->awsCredentials));
} else {
$response = $this->client->send($guzzleRequest);
}
} catch (RequestException $e) {
if ($e->hasResponse()) {
$response = $e->getResponse();
} else {
throw $e;
}
}
return $this->createResponse($response);
}
protected function extractHeaders(BrowserKitRequest $request)
{
$headers = [];
$server = $request->getServer();
$contentHeaders = ['Content-Length' => true, 'Content-Md5' => true, 'Content-Type' => true];
foreach ($server as $header => $val) {
$header = html_entity_decode(implode('-', array_map('ucfirst', explode('-', strtolower(str_replace('_', '-', $header))))), ENT_NOQUOTES);
if (strpos($header, 'Http-') === 0) {
$headers[substr($header, 5)] = $val;
} elseif (isset($contentHeaders[$header])) {
$headers[$header] = $val;
}
}
return $headers;
}
protected function extractBody(BrowserKitRequest $request)
{
if (in_array(strtoupper($request->getMethod()), ['GET', 'HEAD'])) {
return null;
}
if ($request->getContent() !== null) {
return $request->getContent();
}
return $request->getParameters();
}
protected function extractFiles(BrowserKitRequest $request)
{
if (!in_array(strtoupper($request->getMethod()), ['POST', 'PUT'])) {
return [];
}
return $this->mapFiles($request->getFiles());
}
protected function mapFiles($requestFiles, $arrayName = '')
{
$files = [];
foreach ($requestFiles as $name => $info) {
if (!empty($arrayName)) {
$name = $arrayName.'['.$name.']';
}
if (is_array($info)) {
if (isset($info['tmp_name'])) {
if ($info['tmp_name']) {
$handle = fopen($info['tmp_name'], 'r');
$filename = isset($info['name']) ? $info['name'] : null;
$files[] = new PostFile($name, $handle, $filename);
}
} else {
$files = array_merge($files, $this->mapFiles($info, $name));
}
} else {
$files[] = new PostFile($name, fopen($info, 'r'));
}
}
return $files;
}
protected function extractCookies(BrowserKitRequest $request)
{
return $this->getCookieJar()->allRawValues($request->getUri());
}
public function setAwsAuth($config)
{
$this->awsCredentials = new Credentials($config['key'], $config['secret']);
$this->awsSignature = new SignatureV4($config['service'], $config['region']);
}
}

View File

@@ -0,0 +1,356 @@
<?php
namespace Codeception\Lib\Connector;
use Aws\Credentials\Credentials;
use Aws\Signature\SignatureV4;
use Codeception\Util\Uri;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Cookie\SetCookie;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Handler\CurlHandler;
use GuzzleHttp\Handler\StreamHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Request as Psr7Request;
use GuzzleHttp\Psr7\Response as Psr7Response;
use GuzzleHttp\Psr7\Uri as Psr7Uri;
use Symfony\Component\BrowserKit\Client;
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\BrowserKit\Request as BrowserKitRequest;
use Symfony\Component\BrowserKit\Request;
use Symfony\Component\BrowserKit\Response as BrowserKitResponse;
class Guzzle6 extends Client
{
protected $requestOptions = [
'allow_redirects' => false,
'headers' => [],
];
protected $refreshMaxInterval = 0;
protected $awsCredentials = null;
protected $awsSignature = null;
/** @var \GuzzleHttp\Client */
protected $client;
/**
* Sets the maximum allowable timeout interval for a meta tag refresh to
* automatically redirect a request.
*
* A meta tag detected with an interval equal to or greater than $seconds
* would not result in a redirect. A meta tag without a specified interval
* or one with a value less than $seconds would result in the client
* automatically redirecting to the specified URL
*
* @param int $seconds Number of seconds
*/
public function setRefreshMaxInterval($seconds)
{
$this->refreshMaxInterval = $seconds;
}
public function setClient(GuzzleClient &$client)
{
$this->client = $client;
}
/**
* Sets the request header to the passed value. The header will be
* sent along with the next request.
*
* Passing an empty value clears the header, which is the equivalent
* of calling deleteHeader.
*
* @param string $name the name of the header
* @param string $value the value of the header
*/
public function setHeader($name, $value)
{
if (strval($value) === '') {
$this->deleteHeader($name);
} else {
$this->requestOptions['headers'][$name] = $value;
}
}
/**
* Deletes the header with the passed name from the list of headers
* that will be sent with the request.
*
* @param string $name the name of the header to delete.
*/
public function deleteHeader($name)
{
unset($this->requestOptions['headers'][$name]);
}
/**
* @param string $username
* @param string $password
* @param string $type Default: 'basic'
*/
public function setAuth($username, $password, $type = 'basic')
{
if (!$username) {
unset($this->requestOptions['auth']);
return;
}
$this->requestOptions['auth'] = [$username, $password, $type];
}
/**
* Taken from Mink\BrowserKitDriver
*
* @param Response $response
*
* @return \Symfony\Component\BrowserKit\Response
*/
protected function createResponse(Psr7Response $response)
{
$body = (string) $response->getBody();
$headers = $response->getHeaders();
$contentType = null;
if (isset($headers['Content-Type'])) {
$contentType = reset($headers['Content-Type']);
}
if (!$contentType) {
$contentType = 'text/html';
}
if (strpos($contentType, 'charset=') === false) {
if (preg_match('/\<meta[^\>]+charset *= *["\']?([a-zA-Z\-0-9]+)/i', $body, $matches)) {
$contentType .= ';charset=' . $matches[1];
}
$headers['Content-Type'] = [$contentType];
}
$status = $response->getStatusCode();
if ($status < 300 || $status >= 400) {
$matches = [];
$matchesMeta = preg_match(
'/\<meta[^\>]+http-equiv="refresh" content="\s*(\d*)\s*;\s*url=(.*?)"/i',
$body,
$matches
);
if (!$matchesMeta && isset($headers['Refresh'])) {
// match by header
preg_match(
'/^\s*(\d*)\s*;\s*url=(.*)/i',
(string) reset($headers['Refresh']),
$matches
);
}
if ((!empty($matches)) && (empty($matches[1]) || $matches[1] < $this->refreshMaxInterval)) {
$uri = new Psr7Uri($this->getAbsoluteUri($matches[2]));
$currentUri = new Psr7Uri($this->getHistory()->current()->getUri());
if ($uri->withFragment('') != $currentUri->withFragment('')) {
$status = 302;
$headers['Location'] = $matchesMeta ? htmlspecialchars_decode($uri) : (string)$uri;
}
}
}
return new BrowserKitResponse($body, $status, $headers);
}
public function getAbsoluteUri($uri)
{
$baseUri = $this->client->getConfig('base_uri');
if (strpos($uri, '://') === false && strpos($uri, '//') !== 0) {
if (strpos($uri, '/') === 0) {
$baseUriPath = $baseUri->getPath();
if (!empty($baseUriPath) && strpos($uri, $baseUriPath) === 0) {
$uri = substr($uri, strlen($baseUriPath));
}
return Uri::appendPath((string)$baseUri, $uri);
}
// relative url
if (!$this->getHistory()->isEmpty()) {
return Uri::mergeUrls((string)$this->getHistory()->current()->getUri(), $uri);
}
}
return Uri::mergeUrls($baseUri, $uri);
}
protected function doRequest($request)
{
/** @var $request BrowserKitRequest **/
$guzzleRequest = new Psr7Request(
$request->getMethod(),
$request->getUri(),
$this->extractHeaders($request),
$request->getContent()
);
$options = $this->requestOptions;
$options['cookies'] = $this->extractCookies($guzzleRequest->getUri()->getHost());
$multipartData = $this->extractMultipartFormData($request);
if (!empty($multipartData)) {
$options['multipart'] = $multipartData;
}
$formData = $this->extractFormData($request);
if (empty($multipartData) and $formData) {
$options['form_params'] = $formData;
}
try {
if (null !== $this->awsCredentials) {
$response = $this->client->send($this->awsSignature->signRequest($guzzleRequest, $this->awsCredentials), $options);
} else {
$response = $this->client->send($guzzleRequest, $options);
}
} catch (RequestException $e) {
if (!$e->hasResponse()) {
throw $e;
}
$response = $e->getResponse();
}
return $this->createResponse($response);
}
protected function extractHeaders(BrowserKitRequest $request)
{
$headers = [];
$server = $request->getServer();
$contentHeaders = ['Content-Length' => true, 'Content-Md5' => true, 'Content-Type' => true];
foreach ($server as $header => $val) {
$header = html_entity_decode(implode('-', array_map('ucfirst', explode('-', strtolower(str_replace('_', '-', $header))))), ENT_NOQUOTES);
if (strpos($header, 'Http-') === 0) {
$headers[substr($header, 5)] = $val;
} elseif (isset($contentHeaders[$header])) {
$headers[$header] = $val;
}
}
return $headers;
}
protected function extractFormData(BrowserKitRequest $request)
{
if (!in_array(strtoupper($request->getMethod()), ['POST', 'PUT', 'PATCH', 'DELETE'])) {
return null;
}
// guessing if it is a form data
$headers = $request->getServer();
if (isset($headers['HTTP_CONTENT_TYPE'])) {
// not a form
if ($headers['HTTP_CONTENT_TYPE'] !== 'application/x-www-form-urlencoded') {
return null;
}
}
if ($request->getContent() !== null) {
return null;
}
return $request->getParameters();
}
protected function extractMultipartFormData(Request $request)
{
if (!in_array(strtoupper($request->getMethod()), ['POST', 'PUT', 'PATCH'])) {
return [];
}
$parts = $this->mapFiles($request->getFiles());
if (empty($parts)) {
return [];
}
foreach ($request->getParameters() as $k => $v) {
$parts = $this->formatMultipart($parts, $k, $v);
}
return $parts;
}
protected function formatMultipart($parts, $key, $value)
{
if (is_array($value)) {
foreach ($value as $subKey => $subValue) {
$parts = array_merge($this->formatMultipart([], $key."[$subKey]", $subValue), $parts);
}
return $parts;
}
$parts[] = ['name' => $key, 'contents' => (string) $value];
return $parts;
}
protected function mapFiles($requestFiles, $arrayName = '')
{
$files = [];
foreach ($requestFiles as $name => $info) {
if (!empty($arrayName)) {
$name = $arrayName . '[' . $name . ']';
}
if (is_array($info)) {
if (isset($info['tmp_name'])) {
if ($info['tmp_name']) {
$handle = fopen($info['tmp_name'], 'r');
$filename = isset($info['name']) ? $info['name'] : null;
$files[] = [
'name' => $name,
'contents' => $handle,
'filename' => $filename
];
}
} else {
$files = array_merge($files, $this->mapFiles($info, $name));
}
} else {
$files[] = [
'name' => $name,
'contents' => fopen($info, 'r')
];
}
}
return $files;
}
protected function extractCookies($host)
{
$jar = [];
$cookies = $this->getCookieJar()->all();
foreach ($cookies as $cookie) {
/** @var $cookie Cookie **/
$setCookie = SetCookie::fromString((string)$cookie);
if (!$setCookie->getDomain()) {
$setCookie->setDomain($host);
}
$jar[] = $setCookie;
}
return new CookieJar(false, $jar);
}
public static function createHandler($handler)
{
if ($handler === 'curl') {
return HandlerStack::create(new CurlHandler());
}
if ($handler === 'stream') {
return HandlerStack::create(new StreamHandler());
}
if (class_exists($handler)) {
return HandlerStack::create(new $handler);
}
if (is_callable($handler)) {
return HandlerStack::create($handler);
}
return HandlerStack::create();
}
public function setAwsAuth($config)
{
$this->awsCredentials = new Credentials($config['key'], $config['secret']);
$this->awsSignature = new SignatureV4($config['service'], $config['region']);
}
}

View File

@@ -0,0 +1,354 @@
<?php
namespace Codeception\Lib\Connector;
use Codeception\Lib\Connector\Laravel5\ExceptionHandlerDecorator;
use Codeception\Lib\Connector\Shared\LaravelCommon;
use Codeception\Stub;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Client;
class Laravel5 extends Client
{
use LaravelCommon;
/**
* @var Application
*/
private $app;
/**
* @var \Codeception\Module\Laravel5
*/
private $module;
/**
* @var bool
*/
private $firstRequest = true;
/**
* @var array
*/
private $triggeredEvents = [];
/**
* @var bool
*/
private $exceptionHandlingDisabled;
/**
* @var bool
*/
private $middlewareDisabled;
/**
* @var bool
*/
private $eventsDisabled;
/**
* @var bool
*/
private $modelEventsDisabled;
/**
* @var object
*/
private $oldDb;
/**
* Constructor.
*
* @param \Codeception\Module\Laravel5 $module
*/
public function __construct($module)
{
$this->module = $module;
$this->exceptionHandlingDisabled = $this->module->config['disable_exception_handling'];
$this->middlewareDisabled = $this->module->config['disable_middleware'];
$this->eventsDisabled = $this->module->config['disable_events'];
$this->modelEventsDisabled = $this->module->config['disable_model_events'];
$this->initialize();
$components = parse_url($this->app['config']->get('app.url', 'http://localhost'));
if (array_key_exists('url', $this->module->config)) {
$components = parse_url($this->module->config['url']);
}
$host = isset($components['host']) ? $components['host'] : 'localhost';
parent::__construct($this->app, ['HTTP_HOST' => $host]);
// Parent constructor defaults to not following redirects
$this->followRedirects(true);
}
/**
* Execute a request.
*
* @param SymfonyRequest $request
* @return Response
*/
protected function doRequest($request)
{
if (!$this->firstRequest) {
$this->initialize($request);
}
$this->firstRequest = false;
$this->applyBindings();
$this->applyContextualBindings();
$this->applyInstances();
$this->applyApplicationHandlers();
$request = Request::createFromBase($request);
$response = $this->kernel->handle($request);
$this->app->make('Illuminate\Contracts\Http\Kernel')->terminate($request, $response);
return $response;
}
/**
* Make sure files are \Illuminate\Http\UploadedFile instances with the private $test property set to true.
* Fixes issue https://github.com/Codeception/Codeception/pull/3417.
*
* @param array $files
* @return array
*/
protected function filterFiles(array $files)
{
$files = parent::filterFiles($files);
if (! class_exists('Illuminate\Http\UploadedFile')) {
// The \Illuminate\Http\UploadedFile class was introduced in Laravel 5.2.15,
// so don't change the $files array if it does not exist.
return $files;
}
return $this->convertToTestFiles($files);
}
/**
* @param array $files
* @return array
*/
private function convertToTestFiles(array $files)
{
$filtered = [];
foreach ($files as $key => $value) {
if (is_array($value)) {
$filtered[$key] = $this->convertToTestFiles($value);
} else {
$filtered[$key] = UploadedFile::createFromBase($value, true);
}
}
return $filtered;
}
/**
* Initialize the Laravel framework.
*
* @param SymfonyRequest $request
*/
private function initialize($request = null)
{
// Store a reference to the database object
// so the database connection can be reused during tests
$this->oldDb = null;
if (isset($this->app['db']) && $this->app['db']->connection()) {
$this->oldDb = $this->app['db'];
}
$this->app = $this->kernel = $this->loadApplication();
// Set the request instance for the application,
if (is_null($request)) {
$appConfig = require $this->module->config['project_dir'] . 'config/app.php';
$request = SymfonyRequest::create($appConfig['url']);
}
$this->app->instance('request', Request::createFromBase($request));
// Reset the old database after all the service providers are registered.
if ($this->oldDb) {
$this->app['events']->listen('bootstrapped: Illuminate\Foundation\Bootstrap\RegisterProviders', function () {
$this->app->singleton('db', function () {
return $this->oldDb;
});
});
}
$this->app->make('Illuminate\Contracts\Http\Kernel')->bootstrap();
// Record all triggered events by adding a wildcard event listener
// Since Laravel 5.4 wildcard event handlers receive the event name as the first argument,
// but for earlier Laravel versions the firing() method of the event dispatcher should be used
// to determine the event name.
if (method_exists($this->app['events'], 'firing')) {
$listener = function () {
$this->triggeredEvents[] = $this->normalizeEvent($this->app['events']->firing());
};
} else {
$listener = function ($event) {
$this->triggeredEvents[] = $this->normalizeEvent($event);
};
}
$this->app['events']->listen('*', $listener);
// Replace the Laravel exception handler with our decorated exception handler,
// so exceptions can be intercepted for the disable_exception_handling functionality.
$decorator = new ExceptionHandlerDecorator($this->app['Illuminate\Contracts\Debug\ExceptionHandler']);
$decorator->exceptionHandlingDisabled($this->exceptionHandlingDisabled);
$this->app->instance('Illuminate\Contracts\Debug\ExceptionHandler', $decorator);
if ($this->module->config['disable_middleware'] || $this->middlewareDisabled) {
$this->app->instance('middleware.disable', true);
}
if ($this->module->config['disable_events'] || $this->eventsDisabled) {
$this->mockEventDispatcher();
}
if ($this->module->config['disable_model_events'] || $this->modelEventsDisabled) {
Model::unsetEventDispatcher();
}
$this->module->setApplication($this->app);
}
/**
* Boot the Laravel application object.
* @return Application
* @throws ModuleConfig
*/
private function loadApplication()
{
$app = require $this->module->config['bootstrap_file'];
$app->loadEnvironmentFrom($this->module->config['environment_file']);
$app->instance('request', new Request());
return $app;
}
/**
* Replace the Laravel event dispatcher with a mock.
*/
private function mockEventDispatcher()
{
// Even if events are disabled we still want to record the triggered events.
// But by mocking the event dispatcher the wildcard listener registered in the initialize method is removed.
// So to record the triggered events we have to catch the calls to the fire method of the event dispatcher mock.
$callback = function ($event) {
$this->triggeredEvents[] = $this->normalizeEvent($event);
return [];
};
// In Laravel 5.4 the Illuminate\Contracts\Events\Dispatcher interface was changed,
// the 'fire' method was renamed to 'dispatch'. This code determines the correct method to mock.
$method = method_exists($this->app['events'], 'dispatch') ? 'dispatch' : 'fire';
$mock = Stub::makeEmpty('Illuminate\Contracts\Events\Dispatcher', [
$method => $callback
]);
$this->app->instance('events', $mock);
}
/**
* Normalize events to class names.
*
* @param $event
* @return string
*/
private function normalizeEvent($event)
{
if (is_object($event)) {
$event = get_class($event);
}
if (preg_match('/^bootstrapp(ing|ed): /', $event)) {
return $event;
}
// Events can be formatted as 'event.name: parameters'
$segments = explode(':', $event);
return $segments[0];
}
//======================================================================
// Public methods called by module
//======================================================================
/**
* Did an event trigger?
*
* @param $event
* @return bool
*/
public function eventTriggered($event)
{
$event = $this->normalizeEvent($event);
foreach ($this->triggeredEvents as $triggeredEvent) {
if ($event == $triggeredEvent || is_subclass_of($event, $triggeredEvent)) {
return true;
}
}
return false;
}
/**
* Disable Laravel exception handling.
*/
public function disableExceptionHandling()
{
$this->exceptionHandlingDisabled = true;
$this->app['Illuminate\Contracts\Debug\ExceptionHandler']->exceptionHandlingDisabled(true);
}
/**
* Enable Laravel exception handling.
*/
public function enableExceptionHandling()
{
$this->exceptionHandlingDisabled = false;
$this->app['Illuminate\Contracts\Debug\ExceptionHandler']->exceptionHandlingDisabled(false);
}
/**
* Disable events.
*/
public function disableEvents()
{
$this->eventsDisabled = true;
$this->mockEventDispatcher();
}
/**
* Disable model events.
*/
public function disableModelEvents()
{
$this->modelEventsDisabled = true;
Model::unsetEventDispatcher();
}
/*
* Disable middleware.
*/
public function disableMiddleware()
{
$this->middlewareDisabled = true;
$this->app->instance('middleware.disable', true);
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace Codeception\Lib\Connector\Laravel5;
use Exception;
use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandlerContract;
/**
* Class ExceptionHandlerDecorator
*
* @package Codeception\Lib\Connector\Laravel5
*/
class ExceptionHandlerDecorator implements ExceptionHandlerContract
{
/**
* @var ExceptionHandlerContract
*/
private $laravelExceptionHandler;
/**
* @var boolean
*/
private $exceptionHandlingDisabled = true;
/**
* ExceptionHandlerDecorator constructor.
*
* @param object $laravelExceptionHandler
*/
public function __construct($laravelExceptionHandler)
{
$this->laravelExceptionHandler = $laravelExceptionHandler;
}
/**
* @param boolean $exceptionHandlingDisabled
*/
public function exceptionHandlingDisabled($exceptionHandlingDisabled)
{
$this->exceptionHandlingDisabled = $exceptionHandlingDisabled;
}
/**
* Report or log an exception.
*
* @param \Exception $e
* @return void
*/
public function report(Exception $e)
{
$this->laravelExceptionHandler->report($e);
}
/**
* @param $request
* @param Exception $e
* @return \Symfony\Component\HttpFoundation\Response
* @throws Exception
*/
public function render($request, Exception $e)
{
$response = $this->laravelExceptionHandler->render($request, $e);
if ($this->exceptionHandlingDisabled && $this->isSymfonyExceptionHandlerOutput($response->getContent())) {
// If content was generated by the \Symfony\Component\Debug\ExceptionHandler class
// the Laravel application could not handle the exception,
// so re-throw this exception if the Codeception user disabled Laravel's exception handling.
throw $e;
}
return $response;
}
/**
* Check if the response content is HTML output of the Symfony exception handler class.
*
* @param string $content
* @return bool
*/
private function isSymfonyExceptionHandlerOutput($content)
{
return strpos($content, '<div id="sf-resetcontent" class="sf-reset">') !== false ||
strpos($content, '<div class="exception-summary">') !== false;
}
/**
* Render an exception to the console.
*
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @param \Exception $e
* @return void
*/
public function renderForConsole($output, Exception $e)
{
$this->laravelExceptionHandler->renderForConsole($output, $e);
}
/**
* @param string $method
* @param array $args
* @return mixed
*/
public function __call($method, $args)
{
return call_user_func_array([$this->laravelExceptionHandler, $method], $args);
}
}

View File

@@ -0,0 +1,167 @@
<?php
namespace Codeception\Lib\Connector;
use Codeception\Lib\Connector\Lumen\DummyKernel;
use Codeception\Lib\Connector\Shared\LaravelCommon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Facade;
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Client;
use Illuminate\Http\UploadedFile;
class Lumen extends Client
{
use LaravelCommon;
/**
* @var \Laravel\Lumen\Application
*/
private $app;
/**
* @var \Codeception\Module\Lumen
*/
private $module;
/**
* @var bool
*/
private $firstRequest = true;
/**
* @var object
*/
private $oldDb;
/**
* Constructor.
*
* @param \Codeception\Module\Lumen $module
*/
public function __construct($module)
{
$this->module = $module;
$components = parse_url($this->module->config['url']);
$server = ['HTTP_HOST' => $components['host']];
// Pass a DummyKernel to satisfy the arguments of the parent constructor.
// The actual kernel object is set in the initialize() method.
parent::__construct(new DummyKernel(), $server);
// Parent constructor defaults to not following redirects
$this->followRedirects(true);
$this->initialize();
}
/**
* Execute a request.
*
* @param SymfonyRequest $request
* @return Response
*/
protected function doRequest($request)
{
if (!$this->firstRequest) {
$this->initialize($request);
}
$this->firstRequest = false;
$this->applyBindings();
$this->applyContextualBindings();
$this->applyInstances();
$this->applyApplicationHandlers();
$request = Request::createFromBase($request);
$response = $this->kernel->handle($request);
$method = new \ReflectionMethod(get_class($this->app), 'callTerminableMiddleware');
$method->setAccessible(true);
$method->invoke($this->app, $response);
return $response;
}
/**
* Initialize the Lumen framework.
*
* @param SymfonyRequest|null $request
*/
private function initialize($request = null)
{
// Store a reference to the database object
// so the database connection can be reused during tests
$this->oldDb = null;
if (isset($this->app['db']) && $this->app['db']->connection()) {
$this->oldDb = $this->app['db'];
}
if (class_exists(Facade::class)) {
// If the container has been instantiated ever,
// we need to clear its static fields before create new container.
Facade::clearResolvedInstances();
}
$this->app = $this->kernel = require $this->module->config['bootstrap_file'];
// Lumen registers necessary bindings on demand when calling $app->make(),
// so here we force the request binding before registering our own request object,
// otherwise Lumen will overwrite our request object.
$this->app->make('request');
$request = $request ?: SymfonyRequest::create($this->module->config['url']);
$this->app->instance('Illuminate\Http\Request', Request::createFromBase($request));
// Reset the old database if there is one
if ($this->oldDb) {
$this->app->singleton('db', function () {
return $this->oldDb;
});
Model::setConnectionResolver($this->oldDb);
}
$this->module->setApplication($this->app);
}
/**
* Make sure files are \Illuminate\Http\UploadedFile instances with the private $test property set to true.
* Fixes issue https://github.com/Codeception/Codeception/pull/3417.
*
* @param array $files
* @return array
*/
protected function filterFiles(array $files)
{
$files = parent::filterFiles($files);
if (! class_exists('Illuminate\Http\UploadedFile')) {
// The \Illuminate\Http\UploadedFile class was introduced in Laravel 5.2.15,
// so don't change the $files array if it does not exist.
return $files;
}
return $this->convertToTestFiles($files);
}
/**
* @param array $files
* @return array
*/
private function convertToTestFiles(array $files)
{
$filtered = [];
foreach ($files as $key => $value) {
if (is_array($value)) {
$filtered[$key] = $this->convertToTestFiles($value);
} else {
$filtered[$key] = UploadedFile::createFromBase($value, true);
}
}
return $filtered;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Codeception\Lib\Connector\Lumen;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* Dummy kernel to satisfy the parent constructor of the LumenConnector class.
*/
class DummyKernel implements HttpKernelInterface
{
public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true)
{
//
}
}

View File

@@ -0,0 +1,159 @@
<?php
namespace Codeception\Lib\Connector;
use Closure;
use Phalcon\Di;
use Phalcon\Http;
use RuntimeException;
use ReflectionProperty;
use Codeception\Util\Stub;
use Phalcon\Mvc\Application;
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\BrowserKit\Client;
use Phalcon\Mvc\Micro as MicroApplication;
use Symfony\Component\BrowserKit\Response;
use Codeception\Lib\Connector\Shared\PhpSuperGlobalsConverter;
class Phalcon extends Client
{
use PhpSuperGlobalsConverter;
/**
* Phalcon Application
* @var mixed
*/
private $application;
/**
* Set Phalcon Application by \Phalcon\DI\Injectable, Closure or bootstrap file path
*
* @param mixed $application
*/
public function setApplication($application)
{
$this->application = $application;
}
/**
* Get Phalcon Application
*
* @return Application|MicroApplication
*/
public function getApplication()
{
$application = $this->application;
if ($application instanceof Closure) {
return $application();
} elseif (is_string($application)) {
/** @noinspection PhpIncludeInspection */
return require $application;
}
return $application;
}
/**
* Makes a request.
*
* @param \Symfony\Component\BrowserKit\Request $request
*
* @return \Symfony\Component\BrowserKit\Response
* @throws \RuntimeException
*/
public function doRequest($request)
{
$application = $this->getApplication();
if (!$application instanceof Application && !$application instanceof MicroApplication) {
throw new RuntimeException('Unsupported application class.');
}
$di = $application->getDI();
/** @var Http\Request $phRequest */
if ($di->has('request')) {
$phRequest = $di->get('request');
}
if (!$phRequest instanceof Http\RequestInterface) {
$phRequest = new Http\Request();
}
$uri = $request->getUri() ?: $phRequest->getURI();
$pathString = parse_url($uri, PHP_URL_PATH);
$queryString = parse_url($uri, PHP_URL_QUERY);
$_SERVER = $request->getServer();
$_SERVER['REQUEST_METHOD'] = strtoupper($request->getMethod());
$_SERVER['REQUEST_URI'] = null === $queryString ? $pathString : $pathString . '?' . $queryString;
$_COOKIE = $request->getCookies();
$_FILES = $this->remapFiles($request->getFiles());
$_REQUEST = $this->remapRequestParameters($request->getParameters());
$_POST = [];
$_GET = [];
if ($_SERVER['REQUEST_METHOD'] == 'GET') {
$_GET = $_REQUEST;
} else {
$_POST = $_REQUEST;
}
parse_str($queryString, $output);
foreach ($output as $k => $v) {
$_GET[$k] = $v;
}
$_GET['_url'] = $pathString;
$_SERVER['QUERY_STRING'] = http_build_query($_GET);
Di::reset();
Di::setDefault($di);
$di['request'] = Stub::construct($phRequest, [], ['getRawBody' => $request->getContent()]);
$response = $application->handle();
if (!$response instanceof Http\ResponseInterface) {
$response = $application->response;
}
$headers = $response->getHeaders();
$status = (int) $headers->get('Status');
$headersProperty = new ReflectionProperty($headers, '_headers');
$headersProperty->setAccessible(true);
$headers = $headersProperty->getValue($headers);
if (!is_array($headers)) {
$headers = [];
}
$cookiesProperty = new ReflectionProperty($di['cookies'], '_cookies');
$cookiesProperty->setAccessible(true);
$cookies = $cookiesProperty->getValue($di['cookies']);
if (is_array($cookies)) {
$restoredProperty = new ReflectionProperty('\Phalcon\Http\Cookie', '_restored');
$restoredProperty->setAccessible(true);
$valueProperty = new ReflectionProperty('\Phalcon\Http\Cookie', '_value');
$valueProperty->setAccessible(true);
foreach ($cookies as $name => $cookie) {
if (!$restoredProperty->getValue($cookie)) {
$clientCookie = new Cookie(
$name,
$valueProperty->getValue($cookie),
$cookie->getExpiration(),
$cookie->getPath(),
$cookie->getDomain(),
$cookie->getSecure(),
$cookie->getHttpOnly()
);
$headers['Set-Cookie'][] = (string)$clientCookie;
}
}
}
return new Response(
$response->getContent(),
$status ? $status : 200,
$headers
);
}
}

View File

@@ -0,0 +1,309 @@
<?php
namespace Codeception\Lib\Connector\Phalcon;
use Phalcon\Session\AdapterInterface;
class MemorySession implements AdapterInterface
{
/**
* @var string
*/
protected $sessionId;
/**
* @var string
*/
protected $name;
/**
* @var bool
*/
protected $started = false;
/**
* @var array
*/
protected $memory = [];
/**
* @var array
*/
protected $options = [];
public function __construct(array $options = null)
{
$this->sessionId = $this->generateId();
if (is_array($options)) {
$this->setOptions($options);
}
}
/**
* @inheritdoc
*/
public function start()
{
if ($this->status() !== PHP_SESSION_ACTIVE) {
$this->memory = [];
$this->started = true;
return true;
}
return false;
}
/**
* @inheritdoc
*
* @param array $options
*/
public function setOptions(array $options)
{
if (isset($options['uniqueId'])) {
$this->sessionId = $options['uniqueId'];
}
$this->options = $options;
}
/**
* @inheritdoc
*
* @return array
*/
public function getOptions()
{
return $this->options;
}
/**
* @inheritdoc
*
* @param string $index
* @param mixed $defaultValue
* @param bool $remove
* @return mixed
*/
public function get($index, $defaultValue = null, $remove = false)
{
$key = $this->prepareIndex($index);
if (!isset($this->memory[$key])) {
return $defaultValue;
}
$return = $this->memory[$key];
if ($remove) {
unset($this->memory[$key]);
}
return $return;
}
/**
* @inheritdoc
*
* @param string $index
* @param mixed $value
*/
public function set($index, $value)
{
$this->memory[$this->prepareIndex($index)] = $value;
}
/**
* @inheritdoc
*
* @param string $index
* @return bool
*/
public function has($index)
{
return isset($this->memory[$this->prepareIndex($index)]);
}
/**
* @inheritdoc
*
* @param string $index
*/
public function remove($index)
{
unset($this->memory[$this->prepareIndex($index)]);
}
/**
* @inheritdoc
*
* @return string
*/
public function getId()
{
return $this->sessionId;
}
/**
* @inheritdoc
*
* @return bool
*/
public function isStarted()
{
return $this->started;
}
/**
* Returns the status of the current session
*
* ``` php
* <?php
* if ($session->status() !== PHP_SESSION_ACTIVE) {
* $session->start();
* }
* ?>
* ```
*
* @return int
*/
public function status()
{
if ($this->isStarted()) {
return PHP_SESSION_ACTIVE;
}
return PHP_SESSION_NONE;
}
/**
* @inheritdoc
*
* @param bool $removeData
* @return bool
*/
public function destroy($removeData = false)
{
if ($removeData) {
if (!empty($this->sessionId)) {
foreach ($this->memory as $key => $value) {
if (0 === strpos($key, $this->sessionId . '#')) {
unset($this->memory[$key]);
}
}
} else {
$this->memory = [];
}
}
$this->started = false;
return true;
}
/**
* @inheritdoc
*
* @param bool $deleteOldSession
* @return \Phalcon\Session\AdapterInterface
*/
public function regenerateId($deleteOldSession = true)
{
$this->sessionId = $this->generateId();
return $this;
}
/**
* @inheritdoc
*
* @param string $name
*/
public function setName($name)
{
$this->name = $name;
}
/**
* @inheritdoc
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Dump all session
*
* @return array
*/
public function toArray()
{
return (array) $this->memory;
}
/**
* Alias: Gets a session variable from an application context
*
* @param string $index
* @return mixed
*/
public function __get($index)
{
return $this->get($index);
}
/**
* Alias: Sets a session variable in an application context
*
* @param string $index
* @param mixed $value
*/
public function __set($index, $value)
{
$this->set($index, $value);
}
/**
* Alias: Check whether a session variable is set in an application context
*
* @param string $index
* @return bool
*/
public function __isset($index)
{
return $this->has($index);
}
/**
* Alias: Removes a session variable from an application context
*
* @param string $index
*/
public function __unset($index)
{
$this->remove($index);
}
private function prepareIndex($index)
{
if ($this->sessionId) {
$key = $this->sessionId . '#' . $index;
} else {
$key = $index;
}
return $key;
}
/**
* @return string
*/
private function generateId()
{
return md5(time());
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace Codeception\Lib\Connector\Shared;
/**
* Common functions for Laravel family
*
* @package Codeception\Lib\Connector\Shared
*/
trait LaravelCommon
{
/**
* @var array
*/
private $bindings = [];
/**
* @var array
*/
private $contextualBindings = [];
/**
* @var array
*/
private $instances = [];
/**
* @var array
*/
private $applicationHandlers = [];
/**
* Apply the registered application handlers.
*/
private function applyApplicationHandlers()
{
foreach ($this->applicationHandlers as $handler) {
call_user_func($handler, $this->app);
}
}
/**
* Apply the registered Laravel service container bindings.
*/
private function applyBindings()
{
foreach ($this->bindings as $abstract => $binding) {
list($concrete, $shared) = $binding;
$this->app->bind($abstract, $concrete, $shared);
}
}
/**
* Apply the registered Laravel service container contextual bindings.
*/
private function applyContextualBindings()
{
foreach ($this->contextualBindings as $concrete => $bindings) {
foreach ($bindings as $abstract => $implementation) {
$this->app->addContextualBinding($concrete, $abstract, $implementation);
}
}
}
/**
* Apply the registered Laravel service container instance bindings.
*/
private function applyInstances()
{
foreach ($this->instances as $abstract => $instance) {
$this->app->instance($abstract, $instance);
}
}
//======================================================================
// Public methods called by module
//======================================================================
/**
* Register a Laravel service container binding that should be applied
* after initializing the Laravel Application object.
*
* @param $abstract
* @param $concrete
* @param bool $shared
*/
public function haveBinding($abstract, $concrete, $shared = false)
{
$this->bindings[$abstract] = [$concrete, $shared];
}
/**
* Register a Laravel service container contextual binding that should be applied
* after initializing the Laravel Application object.
*
* @param $concrete
* @param $abstract
* @param $implementation
*/
public function haveContextualBinding($concrete, $abstract, $implementation)
{
if (! isset($this->contextualBindings[$concrete])) {
$this->contextualBindings[$concrete] = [];
}
$this->contextualBindings[$concrete][$abstract] = $implementation;
}
/**
* Register a Laravel service container instance binding that should be applied
* after initializing the Laravel Application object.
*
* @param $abstract
* @param $instance
*/
public function haveInstance($abstract, $instance)
{
$this->instances[$abstract] = $instance;
}
/**
* Register a handler than can be used to modify the Laravel application object after it is initialized.
* The Laravel application object will be passed as an argument to the handler.
*
* @param $handler
*/
public function haveApplicationHandler($handler)
{
$this->applicationHandlers[] = $handler;
}
/**
* Clear the registered application handlers.
*/
public function clearApplicationHandlers()
{
$this->applicationHandlers = [];
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Codeception\Lib\Connector;
class Symfony extends \Symfony\Component\HttpKernel\Client
{
/**
* @var boolean
*/
private $rebootable = true;
/**
* @var boolean
*/
private $hasPerformedRequest = false;
/**
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
private $container = null;
/**
* @var array
*/
public $persistentServices = [];
/**
* Constructor.
*
* @param \Symfony\Component\HttpKernel\Kernel $kernel A booted HttpKernel instance
* @param array $services An injected services
* @param boolean $rebootable
*/
public function __construct(\Symfony\Component\HttpKernel\Kernel $kernel, array $services = [], $rebootable = true)
{
parent::__construct($kernel);
$this->followRedirects(true);
$this->rebootable = (boolean)$rebootable;
$this->persistentServices = $services;
$this->rebootKernel();
}
/**
* @param \Symfony\Component\HttpFoundation\Request $request
*/
protected function doRequest($request)
{
if ($this->rebootable) {
if ($this->hasPerformedRequest) {
$this->rebootKernel();
} else {
$this->hasPerformedRequest = true;
}
}
return parent::doRequest($request);
}
/**
* Reboot kernel
*
* Services from the list of persistent services
* are updated from service container before kernel shutdown
* and injected into newly initialized container after kernel boot.
*/
public function rebootKernel()
{
if ($this->container) {
foreach ($this->persistentServices as $serviceName => $service) {
if ($this->container->has($serviceName)) {
$this->persistentServices[$serviceName] = $this->container->get($serviceName);
}
}
}
$this->kernel->shutdown();
$this->kernel->boot();
$this->container = $this->kernel->getContainer();
foreach ($this->persistentServices as $serviceName => $service) {
$this->container->set($serviceName, $service);
}
if ($this->container->has('profiler')) {
$this->container->get('profiler')->enable();
}
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Codeception\Lib\Connector;
use Symfony\Component\BrowserKit\Client;
use Symfony\Component\BrowserKit\Response;
class Universal extends Client
{
use Shared\PhpSuperGlobalsConverter;
protected $mockedResponse;
protected $index;
public function setIndex($index)
{
$this->index = $index;
}
public function mockResponse($response)
{
$this->mockedResponse = $response;
}
public function doRequest($request)
{
if ($this->mockedResponse) {
$response = $this->mockedResponse;
$this->mockedResponse = null;
return $response;
}
$_COOKIE = $request->getCookies();
$_SERVER = $request->getServer();
$_FILES = $this->remapFiles($request->getFiles());
$uri = str_replace('http://localhost', '', $request->getUri());
$_REQUEST = $this->remapRequestParameters($request->getParameters());
if (strtoupper($request->getMethod()) == 'GET') {
$_GET = $_REQUEST;
} else {
$_POST = $_REQUEST;
}
$_SERVER['REQUEST_METHOD'] = strtoupper($request->getMethod());
$_SERVER['REQUEST_URI'] = $uri;
ob_start();
include $this->index;
$content = ob_get_contents();
ob_end_clean();
$headers = [];
$php_headers = headers_list();
foreach ($php_headers as $value) {
// Get the header name
$parts = explode(':', $value);
if (count($parts) > 1) {
$name = trim(array_shift($parts));
// Build the header hash map
$headers[$name] = trim(implode(':', $parts));
}
}
$headers['Content-type'] = isset($headers['Content-type'])
? $headers['Content-type']
: "text/html; charset=UTF-8";
$response = new Response($content, 200, $headers);
return $response;
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace Codeception\Lib\Connector;
use Codeception\Util\Stub;
use Symfony\Component\BrowserKit\Client;
use Symfony\Component\BrowserKit\Response;
class Yii1 extends Client
{
use Shared\PhpSuperGlobalsConverter;
/**
* http://localhost/path/to/your/app/index.php
* @var string url of the entry Yii script
*/
public $url;
/**
* Current application settings {@see Codeception\Module\Yii1::$appSettings}
* @var array
*/
public $appSettings;
/**
* Full path to your application
* @var string
*/
public $appPath;
/**
* Current request headers
* @var array
*/
private $headers;
/**
*
* @param \Symfony\Component\BrowserKit\Request $request
*
* @return \Symfony\Component\BrowserKit\Response
*/
public function doRequest($request)
{
$this->headers = [];
$_COOKIE = array_merge($_COOKIE, $request->getCookies());
$_SERVER = array_merge($_SERVER, $request->getServer());
$_FILES = $this->remapFiles($request->getFiles());
$_REQUEST = $this->remapRequestParameters($request->getParameters());
$_POST = $_GET = [];
if (strtoupper($request->getMethod()) == 'GET') {
$_GET = $_REQUEST;
} else {
$_POST = $_REQUEST;
}
// Parse url parts
$uriPath = ltrim(parse_url($request->getUri(), PHP_URL_PATH), '/');
$uriQuery = ltrim(parse_url($request->getUri(), PHP_URL_QUERY), '?');
$scriptName = trim(parse_url($this->url, PHP_URL_PATH), '/');
if (!empty($uriQuery)) {
$uriPath .= "?{$uriQuery}";
parse_str($uriQuery, $params);
foreach ($params as $k => $v) {
$_GET[$k] = $v;
}
}
// Add script name to request if none
if ($scriptName and strpos($uriPath, $scriptName) === false) {
$uriPath = "/{$scriptName}/{$uriPath}";
}
// Add forward slash if not exists
if (strpos($uriPath, '/') !== 0) {
$uriPath = "/{$uriPath}";
}
$_SERVER['REQUEST_METHOD'] = strtoupper($request->getMethod());
$_SERVER['REQUEST_URI'] = $uriPath;
/**
* Hack to be sure that CHttpRequest will resolve route correctly
*/
$_SERVER['SCRIPT_NAME'] = "/{$scriptName}";
$_SERVER['SCRIPT_FILENAME'] = $this->appPath;
ob_start();
\Yii::setApplication(null);
\Yii::createApplication($this->appSettings['class'], $this->appSettings['config']);
$app = \Yii::app();
// disabling logging. Logs slow down test execution
if ($app->hasComponent('log')) {
foreach ($app->getComponent('log')->routes as $route) {
$route->enabled = false;
}
}
if ($app->hasComponent('session')) { // disable regenerate id in session
$app->setComponent('session', Stub::make('CHttpSession', ['regenerateID' => false]));
}
$app->onEndRequest->add([$this, 'setHeaders']);
$app->run();
if ($app->hasComponent('db')) {
// close connection
$app->getDb()->setActive(false);
// cleanup metadata cache
$property = new \ReflectionProperty('CActiveRecord', '_md');
$property->setAccessible(true);
$property->setValue([]);
}
$content = ob_get_clean();
$headers = $this->getHeaders();
$statusCode = 200;
foreach ($headers as $header => $val) {
if ($header == 'Location') {
$statusCode = 302;
}
}
$response = new Response($content, $statusCode, $this->getHeaders());
return $response;
}
/**
* Set current client headers when terminating yii application (onEndRequest)
*/
public function setHeaders()
{
$this->headers = \Yii::app()->request->getAllHeaders();
}
/**
* Returns current client headers
* @return array headers
*/
public function getHeaders()
{
return $this->headers;
}
}

View File

@@ -0,0 +1,396 @@
<?php
namespace Codeception\Lib\Connector;
use Codeception\Lib\Connector\Yii2\Logger;
use Codeception\Lib\InnerBrowser;
use Codeception\Util\Debug;
use Symfony\Component\BrowserKit\Client;
use Symfony\Component\BrowserKit\Cookie;
use Symfony\Component\BrowserKit\Response;
use Yii;
use yii\base\ExitException;
use yii\base\Security;
use yii\web\Application;
use yii\web\ErrorHandler;
use yii\web\HttpException;
use yii\web\Request;
use yii\web\Response as YiiResponse;
class Yii2 extends Client
{
use Shared\PhpSuperGlobalsConverter;
const CLEAN_METHODS = [
self::CLEAN_RECREATE,
self::CLEAN_CLEAR,
self::CLEAN_FORCE_RECREATE,
self::CLEAN_MANUAL
];
/**
* Clean the response object by recreating it.
* This might lose behaviors / event handlers / other changes that are done in the application bootstrap phase.
*/
const CLEAN_RECREATE = 'recreate';
/**
* Same as recreate but will not warn when behaviors / event handlers are lost.
*/
const CLEAN_FORCE_RECREATE = 'force_recreate';
/**
* Clean the response object by resetting specific properties via its' `clear()` method.
* This will keep behaviors / event handlers, but could inadvertently leave some changes intact.
* @see \Yii\web\Response::clear()
*/
const CLEAN_CLEAR = 'clear';
/**
* Do not clean the response, instead the test writer will be responsible for manually resetting the response in
* between requests during one test
*/
const CLEAN_MANUAL = 'manual';
/**
* @var string application config file
*/
public $configFile;
/**
* @var string method for cleaning the response object before each request
*/
public $responseCleanMethod;
/**
* @var string method for cleaning the request object before each request
*/
public $requestCleanMethod;
/**
* @var string[] List of component names that must be recreated before each request
*/
public $recreateComponents = [];
/**
* This option is there primarily for backwards compatibility.
* It means you cannot make any modification to application state inside your app, since they will get discarded.
* @var bool whether to recreate the whole application before each request
*/
public $recreateApplication = false;
/**
* @return \yii\web\Application
*/
public function getApplication()
{
if (!isset(Yii::$app)) {
$this->startApp();
}
return Yii::$app;
}
public function resetApplication()
{
codecept_debug('Destroying application');
Yii::$app = null;
\yii\web\UploadedFile::reset();
if (method_exists(\yii\base\Event::className(), 'offAll')) {
\yii\base\Event::offAll();
}
Yii::setLogger(null);
// This resolves an issue with database connections not closing properly.
gc_collect_cycles();
}
public function startApp()
{
codecept_debug('Starting application');
$config = require($this->configFile);
if (!isset($config['class'])) {
$config['class'] = 'yii\web\Application';
}
$config = $this->mockMailer($config);
/** @var \yii\web\Application $app */
Yii::$app = Yii::createObject($config);
Yii::setLogger(new Logger());
}
/**
*
* @param \Symfony\Component\BrowserKit\Request $request
*
* @return \Symfony\Component\BrowserKit\Response
*/
public function doRequest($request)
{
$_COOKIE = $request->getCookies();
$_SERVER = $request->getServer();
$_FILES = $this->remapFiles($request->getFiles());
$_REQUEST = $this->remapRequestParameters($request->getParameters());
$_POST = $_GET = [];
if (strtoupper($request->getMethod()) === 'GET') {
$_GET = $_REQUEST;
} else {
$_POST = $_REQUEST;
}
$uri = $request->getUri();
$pathString = parse_url($uri, PHP_URL_PATH);
$queryString = parse_url($uri, PHP_URL_QUERY);
$_SERVER['REQUEST_URI'] = $queryString === null ? $pathString : $pathString . '?' . $queryString;
$_SERVER['REQUEST_METHOD'] = strtoupper($request->getMethod());
parse_str($queryString, $params);
foreach ($params as $k => $v) {
$_GET[$k] = $v;
}
ob_start();
$this->beforeRequest();
$app = $this->getApplication();
// disabling logging. Logs are slowing test execution down
foreach ($app->log->targets as $target) {
$target->enabled = false;
}
$yiiRequest = $app->getRequest();
if ($request->getContent() !== null) {
$yiiRequest->setRawBody($request->getContent());
$yiiRequest->setBodyParams(null);
} else {
$yiiRequest->setRawBody(null);
$yiiRequest->setBodyParams($_POST);
}
$yiiRequest->setQueryParams($_GET);
try {
/*
* This is basically equivalent to $app->run() without sending the response.
* Sending the response is problematic because it tries to send headers.
*/
$app->trigger($app::EVENT_BEFORE_REQUEST);
$response = $app->handleRequest($yiiRequest);
$app->trigger($app::EVENT_AFTER_REQUEST);
$response->send();
} catch (\Exception $e) {
if ($e instanceof HttpException) {
// Don't discard output and pass exception handling to Yii to be able
// to expect error response codes in tests.
$app->errorHandler->discardExistingOutput = false;
$app->errorHandler->handleException($e);
} elseif (!$e instanceof ExitException) {
// for exceptions not related to Http, we pass them to Codeception
throw $e;
}
$response = $app->response;
}
$this->encodeCookies($response, $yiiRequest, $app->security);
if ($response->isRedirection) {
Debug::debug("[Redirect with headers]" . print_r($response->getHeaders()->toArray(), true));
}
$content = ob_get_clean();
if (empty($content) && !empty($response->content)) {
throw new \Exception('No content was sent from Yii application');
}
return new Response($content, $response->statusCode, $response->getHeaders()->toArray());
}
protected function revertErrorHandler()
{
$handler = new ErrorHandler();
set_error_handler([$handler, 'errorHandler']);
}
/**
* Encodes the cookies and adds them to the headers.
* @param \yii\web\Response $response
* @throws \yii\base\InvalidConfigException
*/
protected function encodeCookies(
YiiResponse $response,
Request $request,
Security $security
) {
if ($request->enableCookieValidation) {
$validationKey = $request->cookieValidationKey;
}
foreach ($response->getCookies() as $cookie) {
/** @var \yii\web\Cookie $cookie */
$value = $cookie->value;
if ($cookie->expire != 1 && isset($validationKey)) {
$data = version_compare(Yii::getVersion(), '2.0.2', '>')
? [$cookie->name, $cookie->value]
: $cookie->value;
$value = $security->hashData(serialize($data), $validationKey);
}
$c = new Cookie(
$cookie->name,
$value,
$cookie->expire,
$cookie->path,
$cookie->domain,
$cookie->secure,
$cookie->httpOnly
);
$this->getCookieJar()->set($c);
}
}
/**
* Replace mailer with in memory mailer
* @param array $config Original configuration
* @return array New configuration
*/
protected function mockMailer(array $config)
{
// options that make sense for mailer mock
$allowedOptions = [
'htmlLayout',
'textLayout',
'messageConfig',
'messageClass',
'useFileTransport',
'fileTransportPath',
'fileTransportCallback',
'view',
'viewPath',
];
$mailerConfig = [
'class' => 'Codeception\Lib\Connector\Yii2\TestMailer',
];
if (isset($config['components']['mailer']) && is_array($config['components']['mailer'])) {
foreach ($config['components']['mailer'] as $name => $value) {
if (in_array($name, $allowedOptions, true)) {
$mailerConfig[$name] = $value;
}
}
}
$config['components']['mailer'] = $mailerConfig;
return $config;
}
public function restart()
{
parent::restart();
$this->resetApplication();
}
/**
* Resets the applications' response object.
* The method used depends on the module configuration.
*/
protected function resetResponse(Application $app)
{
$method = $this->responseCleanMethod;
// First check the current response object.
if (($app->response->hasEventHandlers(\yii\web\Response::EVENT_BEFORE_SEND)
|| $app->response->hasEventHandlers(\yii\web\Response::EVENT_AFTER_SEND)
|| $app->response->hasEventHandlers(\yii\web\Response::EVENT_AFTER_PREPARE)
|| count($app->response->getBehaviors()) > 0
) && $method === self::CLEAN_RECREATE
) {
Debug::debug(<<<TEXT
[WARNING] You are attaching event handlers or behaviors to the response object. But the Yii2 module is configured to recreate
the response object, this means any behaviors or events that are not attached in the component config will be lost.
We will fall back to clearing the response. If you are certain you want to recreate it, please configure
responseCleanMethod = 'force_recreate' in the module.
TEXT
);
$method = self::CLEAN_CLEAR;
}
switch ($method) {
case self::CLEAN_FORCE_RECREATE:
case self::CLEAN_RECREATE:
$app->set('response', $app->getComponents()['response']);
break;
case self::CLEAN_CLEAR:
$app->response->clear();
break;
case self::CLEAN_MANUAL:
break;
}
}
protected function resetRequest(Application $app)
{
$method = $this->requestCleanMethod;
$request = $app->request;
// First check the current request object.
if (count($request->getBehaviors()) > 0 && $method === self::CLEAN_RECREATE) {
Debug::debug(<<<TEXT
[WARNING] You are attaching event handlers or behaviors to the request object. But the Yii2 module is configured to recreate
the request object, this means any behaviors or events that are not attached in the component config will be lost.
We will fall back to clearing the request. If you are certain you want to recreate it, please configure
requestCleanMethod = 'force_recreate' in the module.
TEXT
);
$method = self::CLEAN_CLEAR;
}
switch ($method) {
case self::CLEAN_FORCE_RECREATE:
case self::CLEAN_RECREATE:
$app->set('request', $app->getComponents()['request']);
break;
case self::CLEAN_CLEAR:
$request->getHeaders()->removeAll();
$request->setBaseUrl(null);
$request->setHostInfo(null);
$request->setPathInfo(null);
$request->setScriptFile(null);
$request->setScriptUrl(null);
$request->setUrl(null);
$request->setPort(null);
$request->setSecurePort(null);
$request->setAcceptableContentTypes(null);
$request->setAcceptableLanguages(null);
break;
case self::CLEAN_MANUAL:
break;
}
}
/**
* Called before each request, preparation happens here.
*/
protected function beforeRequest()
{
if ($this->recreateApplication) {
$this->resetApplication();
return;
}
$application = $this->getApplication();
$this->resetResponse($application);
$this->resetRequest($application);
$definitions = $application->getComponents(true);
foreach ($this->recreateComponents as $component) {
// Only recreate if it has actually been instantiated.
if ($application->has($component, true)) {
$application->set($component, $definitions[$component]);
}
}
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Codeception\Lib\Connector\Yii2;
use yii\base\Event;
use yii\db\Connection;
/**
* Class ConnectionWatcher
* This class will watch for new database connection and store a reference to the connection object.
* @package Codeception\Lib\Connector\Yii2
*/
class ConnectionWatcher
{
private $handler;
/** @var Connection[] */
private $connections = [];
public function __construct()
{
$this->handler = function (Event $event) {
if ($event->sender instanceof Connection) {
$this->connectionOpened($event->sender);
}
};
}
protected function connectionOpened(Connection $connection)
{
$this->debug('Connection opened!');
if ($connection instanceof Connection) {
$this->connections[] = $connection;
}
}
public function start()
{
Event::on(Connection::class, Connection::EVENT_AFTER_OPEN, $this->handler);
$this->debug('watching new connections');
}
public function stop()
{
Event::off(Connection::class, Connection::EVENT_AFTER_OPEN, $this->handler);
$this->debug('no longer watching new connections');
}
public function closeAll()
{
$count = count($this->connections);
$this->debug("closing all ($count) connections");
foreach ($this->connections as $connection) {
$connection->close();
}
}
protected function debug($message)
{
$title = (new \ReflectionClass($this))->getShortName();
if (is_array($message) or is_object($message)) {
$message = stripslashes(json_encode($message));
}
codecept_debug("[$title] $message");
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Codeception\Lib\Connector\Yii2;
use yii\test\FixtureTrait;
use yii\test\InitDbFixture;
class FixturesStore
{
use FixtureTrait;
protected $data;
/**
* Expects fixtures config
*
* FixturesStore constructor.
* @param $data
*/
public function __construct($data)
{
$this->data = $data;
}
public function fixtures()
{
return $this->data;
}
public function globalFixtures()
{
return [
InitDbFixture::className()
];
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Codeception\Lib\Connector\Yii2;
use Codeception\Util\Debug;
class Logger extends \yii\log\Logger
{
public function init()
{
// overridden to prevent register_shutdown_function
}
public function log($message, $level, $category = 'application')
{
if (!in_array($level, [
\yii\log\Logger::LEVEL_INFO,
\yii\log\Logger::LEVEL_WARNING,
\yii\log\Logger::LEVEL_ERROR,
])) {
return;
}
if (strpos($category, 'yii\db\Command')===0) {
return; // don't log queries
}
// https://github.com/Codeception/Codeception/issues/3696
if ($message instanceof \yii\base\Exception) {
$message = $message->__toString();
}
Debug::debug("[$category] " . \yii\helpers\VarDumper::export($message));
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Codeception\Lib\Connector\Yii2;
use yii\mail\BaseMailer;
class TestMailer extends BaseMailer
{
public $messageClass = 'yii\swiftmailer\Message';
private $sentMessages = [];
protected function sendMessage($message)
{
$this->sentMessages[] = $message;
return true;
}
protected function saveMessage($message)
{
return $this->sendMessage($message);
}
public function getSentMessages()
{
return $this->sentMessages;
}
public function reset()
{
$this->sentMessages = [];
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace Codeception\Lib\Connector\Yii2;
use yii\base\Event;
use yii\db\Connection;
use yii\db\Transaction;
/**
* Class TransactionForcer
* This class adds support for forcing transactions as well as reusing PDO objects.
* @package Codeception\Lib\Connector\Yii2
*/
class TransactionForcer extends ConnectionWatcher
{
private $ignoreCollidingDSN;
private $pdoCache = [];
private $dsnCache;
private $transactions = [];
public function __construct(
$ignoreCollidingDSN
) {
parent::__construct();
$this->ignoreCollidingDSN = $ignoreCollidingDSN;
}
protected function connectionOpened(Connection $connection)
{
parent::connectionOpened($connection);
/**
* We should check if the known PDO objects are the same, in which case we should reuse the PDO
* object so only 1 transaction is started and multiple connections to the same database see the
* same data (due to writes inside a transaction not being visible from the outside).
*
*/
$key = md5(json_encode([
'dsn' => $connection->dsn,
'user' => $connection->username,
'pass' => $connection->password,
'attributes' => $connection->attributes,
'emulatePrepare' => $connection->emulatePrepare,
'charset' => $connection->charset
]));
/*
* If keys match we assume connections are "similar enough".
*/
if (isset($this->pdoCache[$key])) {
$connection->pdo = $this->pdoCache[$key];
} else {
$this->pdoCache[$key] = $connection->pdo;
}
if (isset($this->dsnCache[$connection->dsn])
&& $this->dsnCache[$connection->dsn] !== $key
&& !$this->ignoreCollidingDSN
) {
$this->debug(<<<TEXT
You use multiple connections to the same DSN ({$connection->dsn}) with different configuration.
These connections will not see the same database state since we cannot share a transaction between different PDO
instances.
You can remove this message by adding 'ignoreCollidingDSN = true' in the module configuration.
TEXT
);
Debug::pause();
}
if (isset($this->transactions[$key])) {
$this->debug('Reusing PDO, so no need for a new transaction');
return;
}
$this->debug('Transaction started for: ' . $connection->dsn);
$this->transactions[$key] = $connection->beginTransaction();
}
public function rollbackAll()
{
/** @var Transaction $transaction */
foreach ($this->transactions as $transaction) {
if ($transaction->db->isActive) {
$transaction->rollBack();
$this->debug('Transaction cancelled; all changes reverted.');
}
}
$this->transactions = [];
$this->pdoCache = [];
$this->dsnCache = [];
}
}

View File

@@ -0,0 +1,145 @@
<?php
namespace Codeception\Lib\Connector;
use Symfony\Component\BrowserKit\Client;
use Symfony\Component\BrowserKit\Response;
use Symfony\Component\BrowserKit\Request as BrowserKitRequest;
class ZF1 extends Client
{
use Shared\PhpSuperGlobalsConverter;
/**
* @var \Zend_Controller_Front
*/
protected $front;
/**
* @var \Zend_Application
*/
protected $bootstrap;
/**
* @var \Zend_Controller_Request_HttpTestCase
*/
protected $zendRequest;
public function setBootstrap($bootstrap)
{
$this->bootstrap = $bootstrap;
$this->front = $this->bootstrap
->getBootstrap()
->getResource('frontcontroller');
$this->front
->throwExceptions(true)
->returnResponse(false);
}
public function doRequest($request)
{
// redirector should not exit
$redirector = \Zend_Controller_Action_HelperBroker::getStaticHelper('redirector');
$redirector->setExit(false);
// json helper should not exit
$json = \Zend_Controller_Action_HelperBroker::getStaticHelper('json');
$json->suppressExit = true;
$zendRequest = new \Zend_Controller_Request_HttpTestCase();
$zendRequest->setMethod($request->getMethod());
$zendRequest->setCookies($request->getCookies());
$zendRequest->setParams($request->getParameters());
// Sf2's BrowserKit does not distinguish between GET, POST, PUT etc.,
// so we set all parameters in ZF's request here to not break apps
// relying on $request->getPost()
$zendRequest->setPost($request->getParameters());
$zendRequest->setRawBody($request->getContent());
$uri = $request->getUri();
$queryString = parse_url($uri, PHP_URL_QUERY);
$requestUri = parse_url($uri, PHP_URL_PATH);
if (!empty($queryString)) {
$requestUri .= '?' . $queryString;
}
$zendRequest->setRequestUri($requestUri);
$zendRequest->setHeaders($this->extractHeaders($request));
$_FILES = $this->remapFiles($request->getFiles());
$_SERVER = array_merge($_SERVER, $request->getServer());
$zendResponse = new \Zend_Controller_Response_HttpTestCase;
$this->front->setRequest($zendRequest)->setResponse($zendResponse);
ob_start();
try {
$this->bootstrap->run();
$_GET = $_POST = [];
} catch (\Exception $e) {
ob_end_clean();
$_GET = $_POST = [];
throw $e;
}
ob_end_clean();
$this->zendRequest = $zendRequest;
$response = new Response(
$zendResponse->getBody(),
$zendResponse->getHttpResponseCode(),
$this->formatResponseHeaders($zendResponse)
);
return $response;
}
/**
* Format up the ZF1 response headers into Symfony\Component\BrowserKit\Response headers format.
*
* @param \Zend_Controller_Response_Abstract $response The ZF1 Response Object.
* @return array the clean key/value headers
*/
private function formatResponseHeaders(\Zend_Controller_Response_Abstract $response)
{
$headers = [];
foreach ($response->getHeaders() as $header) {
$name = $header['name'];
if (array_key_exists($name, $headers)) {
if ($header['replace']) {
$headers[$name] = $header['value'];
}
} else {
$headers[$name] = $header['value'];
}
}
return $headers;
}
/**
* @return \Zend_Controller_Request_HttpTestCase
*/
public function getZendRequest()
{
return $this->zendRequest;
}
private function extractHeaders(BrowserKitRequest $request)
{
$headers = [];
$server = $request->getServer();
$contentHeaders = ['Content-Length' => true, 'Content-Md5' => true, 'Content-Type' => true];
foreach ($server as $header => $val) {
$header = html_entity_decode(implode('-', array_map('ucfirst', explode('-', strtolower(str_replace('_', '-', $header))))), ENT_NOQUOTES);
if (strpos($header, 'Http-') === 0) {
$headers[substr($header, 5)] = $val;
} elseif (isset($contentHeaders[$header])) {
$headers[$header] = $val;
}
}
return $headers;
}
}

View File

@@ -0,0 +1,192 @@
<?php
namespace Codeception\Lib\Connector;
use Codeception\Exception\ModuleException;
use Codeception\Lib\Connector\ZF2\PersistentServiceManager;
use Symfony\Component\BrowserKit\Client;
use Symfony\Component\BrowserKit\Request;
use Symfony\Component\BrowserKit\Response;
use Zend\Http\Request as HttpRequest;
use Zend\Http\Headers as HttpHeaders;
use Zend\Mvc\Application;
use Zend\Stdlib\Parameters;
use Zend\Uri\Http as HttpUri;
use Symfony\Component\BrowserKit\Request as BrowserKitRequest;
class ZF2 extends Client
{
/**
* @var \Zend\Mvc\ApplicationInterface
*/
protected $application;
/**
* @var array
*/
protected $applicationConfig;
/**
* @var \Zend\Http\PhpEnvironment\Request
*/
protected $zendRequest;
/**
* @var PersistentServiceManager
*/
private $persistentServiceManager;
/**
* @param array $applicationConfig
*/
public function setApplicationConfig($applicationConfig)
{
$this->applicationConfig = $applicationConfig;
$this->createApplication();
}
/**
* @param Request $request
*
* @return Response
* @throws \Exception
*/
public function doRequest($request)
{
$this->createApplication();
$zendRequest = $this->application->getRequest();
$uri = new HttpUri($request->getUri());
$queryString = $uri->getQuery();
$method = strtoupper($request->getMethod());
$zendRequest->setCookies(new Parameters($request->getCookies()));
$query = [];
$post = [];
$content = $request->getContent();
if ($queryString) {
parse_str($queryString, $query);
}
if ($method !== HttpRequest::METHOD_GET) {
$post = $request->getParameters();
}
$zendRequest->setQuery(new Parameters($query));
$zendRequest->setPost(new Parameters($post));
$zendRequest->setFiles(new Parameters($request->getFiles()));
$zendRequest->setContent($content);
$zendRequest->setMethod($method);
$zendRequest->setUri($uri);
$requestUri = $uri->getPath();
if (!empty($queryString)) {
$requestUri .= '?' . $queryString;
}
$zendRequest->setRequestUri($requestUri);
$zendRequest->setHeaders($this->extractHeaders($request));
$this->application->run();
// get the response *after* the application has run, because other ZF
// libraries like API Agility may *replace* the application's response
//
$zendResponse = $this->application->getResponse();
$this->zendRequest = $zendRequest;
$exception = $this->application->getMvcEvent()->getParam('exception');
if ($exception instanceof \Exception) {
throw $exception;
}
$response = new Response(
$zendResponse->getBody(),
$zendResponse->getStatusCode(),
$zendResponse->getHeaders()->toArray()
);
return $response;
}
/**
* @return \Zend\Http\PhpEnvironment\Request
*/
public function getZendRequest()
{
return $this->zendRequest;
}
private function extractHeaders(BrowserKitRequest $request)
{
$headers = [];
$server = $request->getServer();
$contentHeaders = ['Content-Length' => true, 'Content-Md5' => true, 'Content-Type' => true];
foreach ($server as $header => $val) {
$header = html_entity_decode(implode('-', array_map('ucfirst', explode('-', strtolower(str_replace('_', '-', $header))))), ENT_NOQUOTES);
if (strpos($header, 'Http-') === 0) {
$headers[substr($header, 5)] = $val;
} elseif (isset($contentHeaders[$header])) {
$headers[$header] = $val;
}
}
$zendHeaders = new HttpHeaders();
$zendHeaders->addHeaders($headers);
return $zendHeaders;
}
public function grabServiceFromContainer($service)
{
$serviceManager = $this->application->getServiceManager();
if (!$serviceManager->has($service)) {
throw new \PHPUnit\Framework\AssertionFailedError("Service $service is not available in container");
}
if ($service === 'Doctrine\ORM\EntityManager' && !isset($this->persistentServiceManager)) {
if (!method_exists($serviceManager, 'addPeeringServiceManager')) {
throw new ModuleException('Codeception\Module\ZF2', 'integration with Doctrine2 module is not compatible with ZF3');
}
$this->persistentServiceManager = new PersistentServiceManager($serviceManager);
}
return $serviceManager->get($service);
}
public function addServiceToContainer($name, $service)
{
if (!isset($this->persistentServiceManager)) {
$serviceManager = $this->application->getServiceManager();
if (!method_exists($serviceManager, 'addPeeringServiceManager')) {
throw new ModuleException('Codeception\Module\ZF2', 'addServiceToContainer method is not compatible with ZF3');
}
$this->persistentServiceManager = new PersistentServiceManager($serviceManager);
$serviceManager->addPeeringServiceManager($this->persistentServiceManager);
$serviceManager->setRetrieveFromPeeringManagerFirst(true);
}
$this->persistentServiceManager->setAllowOverride(true);
$this->persistentServiceManager->setService($name, $service);
$this->persistentServiceManager->setAllowOverride(false);
}
private function createApplication()
{
$this->application = Application::init($this->applicationConfig);
$serviceManager = $this->application->getServiceManager();
if (isset($this->persistentServiceManager)) {
$serviceManager->addPeeringServiceManager($this->persistentServiceManager);
$serviceManager->setRetrieveFromPeeringManagerFirst(true);
}
$sendResponseListener = $serviceManager->get('SendResponseListener');
$events = $this->application->getEventManager();
if (class_exists('Zend\EventManager\StaticEventManager')) {
$events->detach($sendResponseListener); //ZF2
} else {
$events->detach([$sendResponseListener, 'sendResponse']); //ZF3
}
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Codeception\Lib\Connector\ZF2;
use \Zend\ServiceManager\ServiceLocatorInterface;
use \Zend\ServiceManager\ServiceManager;
class PersistentServiceManager extends ServiceManager implements ServiceLocatorInterface
{
/**
* @var ServiceLocatorInterface Used to retrieve Doctrine services
*/
private $serviceManager;
public function __construct(ServiceLocatorInterface $serviceManager)
{
$this->serviceManager = $serviceManager;
}
public function get($name, $usePeeringServiceManagers = true)
{
if (parent::has($name)) {
return parent::get($name, $usePeeringServiceManagers);
}
return $this->serviceManager->get($name);
}
public function has($name, $checkAbstractFactories = true, $usePeeringServiceManagers = true)
{
if (parent::has($name, $checkAbstractFactories, $usePeeringServiceManagers)) {
return true;
}
if (preg_match('/doctrine/i', $name)) {
return $this->serviceManager->has($name);
}
return false;
}
public function setService($name, $service)
{
parent::setService($name, $service);
}
}

View File

@@ -0,0 +1,142 @@
<?php
namespace Codeception\Lib\Connector;
use Codeception\Lib\Connector\ZendExpressive\ResponseCollector;
use Symfony\Component\BrowserKit\Client;
use Symfony\Component\BrowserKit\Request;
use Symfony\Component\BrowserKit\Response;
use Symfony\Component\BrowserKit\Request as BrowserKitRequest;
use Zend\Diactoros\ServerRequest;
use Zend\Expressive\Application;
use Zend\Diactoros\UploadedFile;
class ZendExpressive extends Client
{
/**
* @var Application
*/
protected $application;
/**
* @var ResponseCollector
*/
protected $responseCollector;
/**
* @param Application
*/
public function setApplication(Application $application)
{
$this->application = $application;
}
/**
* @param ResponseCollector $responseCollector
*/
public function setResponseCollector(ResponseCollector $responseCollector)
{
$this->responseCollector = $responseCollector;
}
/**
* @param Request $request
*
* @return Response
* @throws \Exception
*/
public function doRequest($request)
{
$inputStream = fopen('php://memory', 'r+');
$content = $request->getContent();
if ($content !== null) {
fwrite($inputStream, $content);
rewind($inputStream);
}
$queryParams = [];
$postParams = [];
$queryString = parse_url($request->getUri(), PHP_URL_QUERY);
if ($queryString != '') {
parse_str($queryString, $queryParams);
}
if ($request->getMethod() !== 'GET') {
$postParams = $request->getParameters();
}
$serverParams = $request->getServer();
if (!isset($serverParams['SCRIPT_NAME'])) {
//required by WhoopsErrorHandler
$serverParams['SCRIPT_NAME'] = 'Codeception';
}
$zendRequest = new ServerRequest(
$serverParams,
$this->convertFiles($request->getFiles()),
$request->getUri(),
$request->getMethod(),
$inputStream,
$this->extractHeaders($request)
);
$zendRequest = $zendRequest->withCookieParams($request->getCookies())
->withQueryParams($queryParams)
->withParsedBody($postParams);
$cwd = getcwd();
chdir(codecept_root_dir());
$this->application->run($zendRequest);
chdir($cwd);
$this->request = $zendRequest;
$response = $this->responseCollector->getResponse();
$this->responseCollector->clearResponse();
return new Response(
$response->getBody(),
$response->getStatusCode(),
$response->getHeaders()
);
}
private function convertFiles(array $files)
{
$fileObjects = [];
foreach ($files as $fieldName => $file) {
if ($file instanceof UploadedFile) {
$fileObjects[$fieldName] = $file;
} elseif (!isset($file['tmp_name']) && !isset($file['name'])) {
$fileObjects[$fieldName] = $this->convertFiles($file);
} else {
$fileObjects[$fieldName] = new UploadedFile(
$file['tmp_name'],
$file['size'],
$file['error'],
$file['name'],
$file['type']
);
}
}
return $fileObjects;
}
private function extractHeaders(BrowserKitRequest $request)
{
$headers = [];
$server = $request->getServer();
$contentHeaders = ['Content-Length' => true, 'Content-Md5' => true, 'Content-Type' => true];
foreach ($server as $header => $val) {
$header = html_entity_decode(implode('-', array_map('ucfirst', explode('-', strtolower(str_replace('_', '-', $header))))), ENT_NOQUOTES);
if (strpos($header, 'Http-') === 0) {
$headers[substr($header, 5)] = $val;
} elseif (isset($contentHeaders[$header])) {
$headers[$header] = $val;
}
}
return $headers;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Codeception\Lib\Connector\ZendExpressive;
use Psr\Http\Message\ResponseInterface;
use Zend\Diactoros\Response\EmitterInterface;
class ResponseCollector implements EmitterInterface
{
/**
* @var ResponseInterface
*/
private $response;
public function emit(ResponseInterface $response)
{
$this->response = $response;
}
public function getResponse()
{
if ($this->response === null) {
throw new \LogicException('Response wasn\'t emitted yet');
}
return $this->response;
}
public function clearResponse()
{
$this->response = null;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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";
}
}

View File

@@ -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>");
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Codeception\Lib;
/**
* Populates a db using a parameterized command built from the Db module configuration.
*/
class DbPopulator
{
/**
* The command to be executed.
*
* @var string
*/
private $builtCommand;
/**
* @var array
*/
protected $config;
/**
* Constructs a DbPopulator object for the given command and Db module.
*
* @param $config
* @internal param string $command The parameterized command to evaluate and execute later.
* @internal param Codeception\Module\Db|null $dbModule The Db module used to build the populator command or null.
*/
public function __construct($config)
{
$this->config = $config;
$command = $this->config['populator'];
$this->builtCommand = $this->buildCommand((string) $command);
}
/**
* Builds out a command replacing any found `$key` with its value if found in the given configuration.
*
* Process any $key found in the configuration array as a key of the array and replaces it with
* the found value for the key. Example:
*
* ```php
* <?php
*
* $command = 'Hello $name';
* $config = ['name' => 'Mauro'];
*
* // With the above parameters it will return `'Hello Mauro'`.
* ```
*
* @param string $command The command to be evaluated using the given config
* @param array $config The configuration values used to replace any found $keys with values from this array.
* @return string The resulting command string after evaluating any configuration's key
*/
protected function buildCommand($command)
{
$dsn = isset($this->config['dsn']) ? $this->config['dsn'] : '';
$dsnVars = [];
$dsnWithoutDriver = preg_replace('/^[a-z]+:/i', '', $dsn);
foreach (explode(';', $dsnWithoutDriver) as $item) {
$keyValueTuple = explode('=', $item);
if (count($keyValueTuple) > 1) {
list($k, $v) = array_values($keyValueTuple);
$dsnVars[$k] = $v;
}
}
$vars = array_merge($dsnVars, $this->config);
foreach ($vars as $key => $value) {
$vars['$'.$key] = $value;
unset($vars[$key]);
}
return str_replace(array_keys($vars), array_values($vars), $command);
}
/**
* Executes the command built using the Db module configuration.
*
* Uses the PHP `exec` to spin off a child process for the built command.
*
* @return bool
*/
public function run()
{
$command = $this->getBuiltCommand();
codecept_debug("[Db] Executing Populator: `$command`");
exec($command, $output, $exitCode);
if (0 !== $exitCode) {
throw new \RuntimeException(
"The populator command did not end successfully: \n" .
" Exit code: $exitCode \n" .
" Output:" . implode("\n", $output)
);
}
codecept_debug("[Db] Populator Finished.");
return true;
}
public function getBuiltCommand()
{
return $this->builtCommand;
}
}

Some files were not shown because too many files have changed in this diff Show More