diff --git a/src/Codeception/Command/Run.php b/src/Codeception/Command/Run.php
index 237af968d5..e31aac10b6 100644
--- a/src/Codeception/Command/Run.php
+++ b/src/Codeception/Command/Run.php
@@ -3,8 +3,6 @@
use Codeception\Codecept;
use Codeception\Configuration;
-use Codeception\Lib\GroupManager;
-use Codeception\Util\PathResolver;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -330,7 +328,7 @@ public function execute(InputInterface $input, OutputInterface $output)
$suite = $input->getArgument('suite');
$test = $input->getArgument('test');
-
+
if ($this->options['group']) {
$this->output->writeln(sprintf("[Groups] %s ", implode(', ', $this->options['group'])));
}
@@ -348,6 +346,7 @@ public function execute(InputInterface $input, OutputInterface $output)
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);
@@ -360,7 +359,7 @@ public function execute(InputInterface $input, OutputInterface $output)
$testsPath = $include . DIRECTORY_SEPARATOR. $config['paths']['tests'];
try {
- list(, $suite, $test) = $this->matchTestFromFilename($suite, $testsPath);
+ [, $suite, $test] = $this->matchTestFromFilename($suite, $testsPath);
$isIncludeTest = true;
} catch (\InvalidArgumentException $e) {
// Incorrect include match, continue trying to find one
@@ -369,7 +368,7 @@ public function execute(InputInterface $input, OutputInterface $output)
} else {
$result = $this->matchSingleTest($suite, $config);
if ($result) {
- list(, $suite, $test) = $result;
+ [, $suite, $test] = $result;
}
}
}
@@ -381,14 +380,18 @@ public function execute(InputInterface $input, OutputInterface $output)
} elseif (!empty($suite)) {
$result = $this->matchSingleTest($suite, $config);
if ($result) {
- list(, $suite, $test) = $result;
+ [, $suite, $test] = $result;
}
}
}
-
+
if ($test) {
$userOptions['filter'] = $this->matchFilteredTestName($test);
- } elseif ($suite) {
+ } elseif (
+ $suite
+ && ! $this->isWildcardSuiteName($suite)
+ && ! $this->isSuiteInMultiApplication($suite)
+ ) {
$userOptions['filter'] = $this->matchFilteredTestName($suite);
}
if (!$this->options['silent'] && $config['settings']['shuffle']) {
@@ -405,18 +408,60 @@ public function execute(InputInterface $input, OutputInterface $output)
// 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) {
+ $raw_suites = $suite ? explode(',', $suite) : Configuration::suites();
+
+ /** @var string[] $main_app_suites */
+ $main_app_suites = [];
+
+ /** @var array $app_specific_suites */
+ $app_specific_suites = [];
+
+ /** @var string[] $wildcard_suites */
+ $wildcard_suites = [];
+
+ foreach ($raw_suites as $raw_suite) {
+ if($this->isWildcardSuiteName($raw_suite)){
+ $wildcard_suites[] = explode('*::', $raw_suite)[1];
+ continue;
+ }
+ if($this->isSuiteInMultiApplication($raw_suite)){
+ $app_and_suite = explode('::', $raw_suite);
+ $app_specific_suites[$app_and_suite[0]][] = $app_and_suite[1];
+ continue;
+ }
+ $main_app_suites[] = $raw_suite;
+ }
+
+ if([] !== $main_app_suites) {
+ $this->executed = $this->runSuites($main_app_suites, $this->options['skip']);
+ }
+
+ if(!empty($wildcard_suites) && ! empty($app_specific_suites)) {
+ $this->output->writeLn('Wildcard options can not be combined with specific suites of included apps.');
+ return self::INVALID;
+ }
+
+ if(!empty($config['include'])) {
+
$current_dir = Configuration::projectDir();
- $suites += $config['include'];
- $this->runIncludedSuites($config['include'], $current_dir);
+ $included_apps = $config['include'];
+
+ if(!empty($app_specific_suites)){
+ $included_apps = array_intersect($included_apps, array_keys($app_specific_suites));
+ }
+
+ $this->runIncludedSuites(
+ $included_apps,
+ $current_dir,
+ $app_specific_suites,
+ $wildcard_suites
+ );
+
}
-
+
if ($this->executed === 0) {
throw new \RuntimeException(
- sprintf("Suite '%s' could not be found", implode(', ', $suites))
+ sprintf("Suite '%s' could not be found", implode(', ', $raw_suites))
);
}
}
@@ -469,7 +514,7 @@ protected function matchSingleTest($suite, $config)
if (strpos($realTestDir, $cwd) === 0) {
$file = $suite;
if (strpos($file, ':') !== false) {
- list($file) = explode(':', $suite, -1);
+ [$file] = explode(':', $suite, -1);
}
$realPath = $cwd . DIRECTORY_SEPARATOR . $file;
if (file_exists($realPath) && strpos($realPath, $realTestDir) === 0) {
@@ -488,8 +533,10 @@ protected function matchSingleTest($suite, $config)
*
* @param array $suites
* @param string $parent_dir
+ * @param array $filter_app_suites An array keyed by included app name where values are suite names to run.
+ * @param string[] $filter_suites_by_wildcard A list of suite names (applies to all included apps)
*/
- protected function runIncludedSuites($suites, $parent_dir)
+ protected function runIncludedSuites($suites, $parent_dir, $filter_app_suites = [], $filter_suites_by_wildcard = [])
{
$defaultConfig = Configuration::config();
$absolutePath = \Codeception\Configuration::projectDir();
@@ -506,7 +553,15 @@ protected function runIncludedSuites($suites, $parent_dir)
}
$suites = Configuration::suites();
-
+
+ if(!empty($filter_suites_by_wildcard)){
+ $suites = array_intersect($suites, $filter_suites_by_wildcard);
+ }
+
+ if(isset($filter_app_suites[$relativePath])) {
+ $suites = array_intersect($suites, $filter_app_suites[$relativePath]);
+ }
+
$namespace = $this->currentNamespace();
$this->output->writeln(
"\n\n[$namespace]: tests from $current_dir\n"
@@ -534,6 +589,7 @@ protected function currentNamespace()
protected function runSuites($suites, $skippedSuites = [])
{
+
$executed = 0;
foreach ($suites as $suite) {
if (in_array($suite, $skippedSuites)) {
@@ -555,10 +611,10 @@ protected function matchTestFromFilename($filename, $testsPath)
if (strpos($filename, ':') !== false) {
if ((PHP_OS === 'Windows' || PHP_OS === 'WINNT') && $filename[1] === ':') {
// match C:\...
- list($drive, $path, $filter) = explode(':', $filename, 3);
+ [$drive, $path, $filter] = explode(':', $filename, 3);
$filename = $drive . ':' . $path;
} else {
- list($filename, $filter) = explode(':', $filename, 2);
+ [$filename, $filter] = explode(':', $filename, 2);
}
if ($filter) {
@@ -592,7 +648,7 @@ private function matchFilteredTestName(&$path)
{
$test_parts = explode(':', $path, 2);
if (count($test_parts) > 1) {
- list($path, $filter) = $test_parts;
+ [$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, '^');
@@ -661,4 +717,25 @@ private function ensurePhpExtIsAvailable($ext)
);
}
}
+
+ /**
+ * @param string $suite_name
+ *
+ * @return bool
+ */
+ private function isWildcardSuiteName($suite_name)
+ {
+ return '*::' === substr($suite_name, 0, 3);
+ }
+
+ /**
+ * @param string $suite_name
+ *
+ * @return bool
+ */
+ private function isSuiteInMultiApplication($suite_name)
+ {
+ return false !== strpos($suite_name, '::');
+ }
+
}
diff --git a/tests/cli/IncludedCest.php b/tests/cli/IncludedCest.php
index e63371842e..63a65e45d1 100644
--- a/tests/cli/IncludedCest.php
+++ b/tests/cli/IncludedCest.php
@@ -205,4 +205,78 @@ public function includedSuitesAreNotRunTwice (CliGuy $I) {
$I->seeInShellOutput('2 tests');
$I->dontSeeInShellOutput('4 tests');
}
+
+ /**
+ * @before moveToIncluded
+ * @param CliGuy $I
+ */
+ public function someSuitesForSomeIncludedApplicationCanBeRun(CliGuy $I)
+ {
+ $I->executeCommand('run jazz::functional');
+
+ $I->seeInShellOutput('Jazz.functional Tests');
+ $I->dontSeeInShellOutput('Jazz.unit Tests');
+ $I->dontSeeInShellOutput('Shire.functional');
+
+ $I->executeCommand('run jazz::functional,jazz::unit');
+
+ $I->seeInShellOutput('Jazz.functional Tests');
+ $I->seeInShellOutput('Jazz.unit Tests');
+
+ $I->dontSeeInShellOutput('Shire.functional');
+
+ $I->executeCommand('run jazz::unit,shire::functional');
+
+ $I->seeInShellOutput('Shire.functional Tests');
+ $I->seeInShellOutput('Jazz.unit Tests');
+ $I->dontSeeInShellOutput('Jazz.functional Tests');
+
+ $I->executeCommand('run jazz/pianist::functional');
+
+ $I->dontSeeInShellOutput('Jazz.functional Tests');
+ $I->seeInShellOutput('Jazz\Pianist.functional');
+ }
+
+ /**
+ * @before moveToIncluded
+ * @param CliGuy $I
+ */
+ public function someSuitesCanBeRunForAllIncludedApplications(\CliGuy $I)
+ {
+ $I->executeCommand('run *::functional');
+
+ // only functional tests are run
+ $I->seeInShellOutput('Jazz.functional Tests');
+ $I->seeInShellOutput('Jazz\Pianist.functional');
+ $I->seeInShellOutput('Shire.functional Tests');
+ // unit suites are not run
+ $I->dontSeeInShellOutput('Jazz.unit Tests');
+
+
+ $I->executeCommand('run *::unit');
+ // only unit tests are run
+ $I->seeInShellOutput('Jazz.unit Tests');
+ $I->dontSeeInShellOutput('Jazz.functional Tests');
+ $I->dontSeeInShellOutput('Jazz\Pianist.functional');
+ $I->dontSeeInShellOutput('Shire.functional Tests');
+
+ $I->executeCommand('run *::functional,*::unit');
+ // Both suites are run now
+ $I->seeInShellOutput('Jazz.functional Tests');
+ $I->seeInShellOutput('Jazz\Pianist.functional');
+ $I->seeInShellOutput('Shire.functional Tests');
+ $I->seeInShellOutput('Jazz.unit Tests');
+ }
+
+ /**
+ * @before moveToIncluded
+ * @param CliGuy $I
+ */
+ public function wildCardSuitesAndAppSpecificSuitesCantBeCombined(CliGuy $I)
+ {
+ $I->executeCommand('run jazz::unit,*::functional', false);
+ $I->seeResultCodeIs(2);
+ $I->seeInShellOutput('Wildcard options can not be combined with specific suites of included apps.');
+ }
+
}
diff --git a/tests/data/snapshots/tests/_data/Snapshot.NotAJsonSnapshot.xml b/tests/data/snapshots/tests/_data/Snapshot.NotAJsonSnapshot.xml
index 3ff04027b5..2218714739 100644
--- a/tests/data/snapshots/tests/_data/Snapshot.NotAJsonSnapshot.xml
+++ b/tests/data/snapshots/tests/_data/Snapshot.NotAJsonSnapshot.xml
@@ -1,8 +1,8 @@
- Son
- Vader
- Disclaimer
- I'm your father!
+ Tove
+ Jani
+ Reminder
+ Don't forget me this weekend!
\ No newline at end of file