This commit is contained in:
2020-10-06 14:27:47 +07:00
commit 586be80cf6
16613 changed files with 3274099 additions and 0 deletions

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