diff --git a/README.md b/README.md
index 5d119710..ea3c21cc 100644
--- a/README.md
+++ b/README.md
@@ -50,7 +50,7 @@ Options:
--max-batch-size (-m) Max batch size (only for functional mode). (default: 0)
--filter Filter (only for functional mode).
--phpunit The PHPUnit binary to execute. (default: vendor/bin/phpunit)
- --runner Runner or WrapperRunner. (default: Runner)
+ --runner Runner, WrapperRunner or SqliteRunner. (default: Runner)
--bootstrap The bootstrap file to be used by PHPUnit.
--configuration (-c) The PHPUnit configuration file to use.
--group (-g) Only runs tests from the specified group(s).
@@ -75,9 +75,9 @@ To get the most out of paratest, you have to adjust the parameters carefully.
Given you have few testcases (classes) with many long running methods, you should use the `-f` option to enable the `functional mode` and allow different methods of the same class to be executed in parallel. Keep in mind that the default is per-testcase-parallelization to address inter-testmethod dependencies. Note that in most projects, using `-f` is **slower** since each test **method** will need to be bootstrapped separately.
- 3. **Use the WrapperRunner if possible**
+ 3. **Use the WrapperRunner or SqliteRunner if possible**
- The default Runner for PHPUnit spawns a new process for each testcase (or method in functional mode). This provides the highest compatibility but comes with the cost of many spawned processes and a bootstrapping for each process. Especially when you have a slow bootstrapping in your tests (like a database setup) you should try the WrapperRunner with `--runner WrapperRunner`. It spawns one "worker"-process for each parallel process (`-p`), executes the bootstrapping once and reuses these processes for each test executed. That way the overhead of process spawning and bootstrapping is reduced to the minimum.
+ The default Runner for PHPUnit spawns a new process for each testcase (or method in functional mode). This provides the highest compatibility but comes with the cost of many spawned processes and a bootstrapping for each process. Especially when you have a slow bootstrapping in your tests (like a database setup) you should try the WrapperRunner with `--runner WrapperRunner` or the SqliteRunner with `--runner SqliteRunner`. It spawns one "worker"-process for each parallel process (`-p`), executes the bootstrapping once and reuses these processes for each test executed. That way the overhead of process spawning and bootstrapping is reduced to the minimum.
4. **Tune batch max size `--max-batch-size`**
diff --git a/bin/phpunit-sqlite-wrapper b/bin/phpunit-sqlite-wrapper
new file mode 100755
index 00000000..fc7f47f1
--- /dev/null
+++ b/bin/phpunit-sqlite-wrapper
@@ -0,0 +1,41 @@
+query('SELECT id, command FROM tests WHERE reserved_by_process_id IS NULL ORDER BY file_name LIMIT 1')->fetch()) {
+ $statement = $db->prepare('UPDATE tests SET reserved_by_process_id = :procId WHERE id = :id AND reserved_by_process_id IS NULL');
+ $statement->execute([
+ ':procId' => getmypid(),
+ ':id' => $test['id'],
+ ]);
+
+ if ($statement->rowCount() !== 1) {
+ // Seems like this test has already been reserved. Continue to the next one.
+ continue;
+ }
+
+ try {
+ if (!preg_match_all('/\'([^\']*)\'[ ]?/', $test['command'], $arguments)) {
+ throw new \Exception("Failed to parse arguments from command line: \"" . $test['command'] . "\"");
+ }
+ $_SERVER['argv'] = $arguments[1];
+
+ PHPUnit\TextUI\Command::main(false);
+ } finally {
+ $db->prepare('UPDATE tests SET completed = 1 WHERE id = :id')
+ ->execute([':id' => $test['id']]);
+ }
+}
diff --git a/src/Console/Testers/PHPUnit.php b/src/Console/Testers/PHPUnit.php
index cd146a14..ae5400c6 100644
--- a/src/Console/Testers/PHPUnit.php
+++ b/src/Console/Testers/PHPUnit.php
@@ -4,9 +4,10 @@
namespace ParaTest\Console\Testers;
+use InvalidArgumentException;
+use ParaTest\Runners\PHPUnit\BaseRunner;
use ParaTest\Runners\PHPUnit\Configuration;
use ParaTest\Runners\PHPUnit\Runner;
-use ParaTest\Runners\PHPUnit\WrapperRunner;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -35,7 +36,7 @@ public function configure(Command $command)
{
$command
->addOption('phpunit', null, InputOption::VALUE_REQUIRED, 'The PHPUnit binary to execute. (default: vendor/bin/phpunit)')
- ->addOption('runner', null, InputOption::VALUE_REQUIRED, 'Runner or WrapperRunner. (default: Runner)')
+ ->addOption('runner', null, InputOption::VALUE_REQUIRED, 'Runner, WrapperRunner or SqliteRunner. (default: Runner)')
->addOption('bootstrap', null, InputOption::VALUE_REQUIRED, 'The bootstrap file to be used by PHPUnit.')
->addOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'The PHPUnit configuration file to use.')
->addOption('group', 'g', InputOption::VALUE_REQUIRED, 'Only runs tests from the specified group(s).')
@@ -64,22 +65,7 @@ public function execute(InputInterface $input, OutputInterface $output)
$this->displayHelp($input, $output);
}
- if ($input->getOption('runner') === 'WrapperRunner') {
- $runner = new WrapperRunner($this->getRunnerOptions($input));
- } else {
- if ($input->getOption('runner') !== '') {
- // because we want to have to bootstrap script inherited before check/initialization
- $runnerOption = $this->getRunnerOptions($input);
- $runnerClass = $input->getOption('runner');
- if (null !== $runnerClass && class_exists($runnerClass)) {
- $runner = new $runnerClass($runnerOption);
- }
- }
- }
-
- if (!isset($runner)) {
- $runner = new Runner($this->getRunnerOptions($input));
- }
+ $runner = $this->initializeRunner($input);
$runner->run();
@@ -236,4 +222,20 @@ protected function getBootstrapFile(InputInterface $input, array $options): stri
return ($bootstrap) ? $config->getConfigDir() . $bootstrap : '';
}
+
+ private function initializeRunner(InputInterface $input): BaseRunner
+ {
+ if ($input->getOption('runner')) {
+ $runnerClass = $input->getOption('runner') ?: '';
+ $runnerClass = class_exists($runnerClass) ? $runnerClass : ('\\ParaTest\\Runners\\PHPUnit\\' . $runnerClass);
+ } else {
+ $runnerClass = Runner::class;
+ }
+
+ if (!class_exists($runnerClass)) {
+ throw new InvalidArgumentException('Selected runner does not exist.');
+ }
+
+ return new $runnerClass($this->getRunnerOptions($input));
+ }
}
diff --git a/src/Runners/PHPUnit/BaseRunner.php b/src/Runners/PHPUnit/BaseRunner.php
index 3454afd9..aada9e07 100644
--- a/src/Runners/PHPUnit/BaseRunner.php
+++ b/src/Runners/PHPUnit/BaseRunner.php
@@ -31,7 +31,7 @@ abstract class BaseRunner
* A collection of pending ExecutableTest objects that have
* yet to run.
*
- * @var array
+ * @var ExecutableTest[]
*/
protected $pending = [];
@@ -67,10 +67,7 @@ public function __construct(array $opts = [])
public function run()
{
- $this->verifyConfiguration();
- $this->initCoverage();
- $this->load(new SuiteLoader($this->options));
- $this->printer->start($this->options);
+ $this->initialize();
}
/**
@@ -176,4 +173,12 @@ protected function getCoverage()
{
return $this->coverage;
}
+
+ protected function initialize(): void
+ {
+ $this->verifyConfiguration();
+ $this->initCoverage();
+ $this->load(new SuiteLoader($this->options));
+ $this->printer->start($this->options);
+ }
}
diff --git a/src/Runners/PHPUnit/ExecutableTest.php b/src/Runners/PHPUnit/ExecutableTest.php
index a110b33c..a67d4f69 100644
--- a/src/Runners/PHPUnit/ExecutableTest.php
+++ b/src/Runners/PHPUnit/ExecutableTest.php
@@ -51,7 +51,7 @@ abstract class ExecutableTest
*
* @var string
*/
- protected $lastCommand;
+ protected $lastCommand = '';
public function __construct(string $path, string $fullyQualifiedClassName = null)
{
diff --git a/src/Runners/PHPUnit/SqliteRunner.php b/src/Runners/PHPUnit/SqliteRunner.php
new file mode 100644
index 00000000..2cb675bb
--- /dev/null
+++ b/src/Runners/PHPUnit/SqliteRunner.php
@@ -0,0 +1,152 @@
+dbFileName = (string)($opts['database'] ?? tempnam(sys_get_temp_dir(), 'paratest_db_'));
+ $this->db = new PDO('sqlite:' . $this->dbFileName);
+ }
+
+ public function run()
+ {
+ $this->initialize();
+
+ $this->createTable();
+ $this->assignAllPendingTests();
+ $this->startWorkers();
+ $this->waitForAllToFinish();
+ $this->complete();
+ $this->checkIfWorkersCrashed();
+ }
+
+ /**
+ * Start all workers.
+ */
+ protected function startWorkers(): void
+ {
+ $wrapper = realpath(__DIR__ . '/../../../bin/phpunit-sqlite-wrapper');
+
+ for ($i = 1; $i <= $this->options->processes; ++$i) {
+ $worker = new SqliteWorker($this->dbFileName);
+ if ($this->options->noTestTokens) {
+ $token = null;
+ $uniqueToken = null;
+ } else {
+ $token = $i;
+ $uniqueToken = uniqid();
+ }
+ $worker->start($wrapper, $token, $uniqueToken);
+ $this->workers[] = $worker;
+ }
+ }
+
+ /**
+ * Wait for all workers to complete their tests and print output.
+ */
+ private function waitForAllToFinish(): void
+ {
+ do {
+ foreach ($this->workers as $key => $worker) {
+ if (!$worker->isRunning()) {
+ unset($this->workers[$key]);
+ }
+ }
+ usleep(10000);
+ $this->printOutput();
+ } while (count($this->workers) > 0);
+ }
+
+ /**
+ * Initialize test queue table.
+ *
+ * @throws Exception
+ */
+ private function createTable(): void
+ {
+ $statement = 'CREATE TABLE tests (
+ id INTEGER PRIMARY KEY,
+ command TEXT NOT NULL UNIQUE,
+ file_name TEXT NOT NULL,
+ reserved_by_process_id INTEGER,
+ completed INTEGER DEFAULT 0
+ )';
+
+ if ($this->db->exec($statement) === false) {
+ throw new Exception('Error while creating sqlite database table: ' . $this->db->errorCode());
+ }
+ }
+
+ /**
+ * Push all tests onto test queue.
+ */
+ private function assignAllPendingTests(): void
+ {
+ foreach ($this->pending as $fileName => $test) {
+ $this->db->prepare('INSERT INTO tests (command, file_name) VALUES (:command, :fileName)')
+ ->execute([
+ ':command' => $test->command($this->options->phpunit, $this->options->filtered),
+ ':fileName' => $fileName
+ ]);
+ }
+ }
+
+ /**
+ * Loop through all completed tests and print their output.
+ */
+ private function printOutput(): void
+ {
+ foreach ($this->db->query('SELECT id, file_name FROM tests WHERE completed = 1')->fetchAll() as $test) {
+ $this->printer->printFeedback($this->pending[$test['file_name']]);
+ $this->db->prepare('DELETE FROM tests WHERE id = :id')->execute([
+ 'id' => $test['id']
+ ]);
+ }
+ }
+
+ public function __destruct()
+ {
+ if ($this->db !== null) {
+ unset($this->db);
+ unlink($this->dbFileName);
+ }
+ }
+
+ /**
+ * Make sure that all tests were executed successfully.
+ */
+ private function checkIfWorkersCrashed(): void
+ {
+ if ($this->db->query('SELECT COUNT(id) FROM tests')->fetchColumn(0) === "0") {
+ return;
+ }
+
+ throw new RuntimeException(
+ 'Some workers have crashed.' . PHP_EOL
+ . '----------------------' . PHP_EOL
+ . 'All workers have quit, but some tests are still to be executed.' . PHP_EOL
+ . 'This may be the case if some tests were killed forcefully (for example, using exit()).' . PHP_EOL
+ . '----------------------' . PHP_EOL
+ . 'Failed test command(s):' . PHP_EOL
+ . '----------------------' . PHP_EOL
+ . implode(PHP_EOL, $this->db->query('SELECT command FROM tests')->fetchAll(PDO::FETCH_COLUMN))
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Runners/PHPUnit/Worker.php b/src/Runners/PHPUnit/Worker.php
deleted file mode 100644
index 78059cf8..00000000
--- a/src/Runners/PHPUnit/Worker.php
+++ /dev/null
@@ -1,239 +0,0 @@
- ['pipe', 'r'],
- 1 => ['pipe', 'w'],
- 2 => ['pipe', 'w'],
- ];
- private $proc;
- private $pipes;
- private $inExecution = 0;
- private $isRunning = false;
- private $exitCode = null;
- private $commands = [];
- private $chunks = '';
- private $alreadyReadOutput = '';
- /**
- * @var ExecutableTest
- */
- private $currentlyExecuting;
-
- public function start(string $wrapperBinary, $token = 1, $uniqueToken = null)
- {
- $bin = 'PARATEST=1 ';
- if (is_numeric($token)) {
- $bin .= "TEST_TOKEN=$token ";
- }
- if ($uniqueToken) {
- $bin .= "UNIQUE_TEST_TOKEN=$uniqueToken ";
- }
- $finder = new PhpExecutableFinder();
- $bin .= $finder->find() . " \"$wrapperBinary\"";
- $pipes = [];
- $this->proc = proc_open($bin, self::$descriptorspec, $pipes);
- $this->pipes = $pipes;
- $this->isRunning = true;
- }
-
- public function stdout()
- {
- return $this->pipes[1];
- }
-
- public function execute(string $testCmd)
- {
- $this->checkStarted();
- $this->commands[] = $testCmd;
- fwrite($this->pipes[0], $testCmd . "\n");
- ++$this->inExecution;
- }
-
- public function assign(ExecutableTest $test, string $phpunit, array $phpunitOptions)
- {
- if ($this->currentlyExecuting !== null) {
- throw new Exception('Worker already has a test assigned - did you forget to call reset()?');
- }
- $this->currentlyExecuting = $test;
- $this->execute($test->command($phpunit, $phpunitOptions));
- }
-
- public function printFeedback(ResultPrinter $printer)
- {
- if ($this->currentlyExecuting !== null) {
- $printer->printFeedback($this->currentlyExecuting);
- }
- }
-
- public function reset()
- {
- $this->currentlyExecuting = null;
- }
-
- public function isStarted(): bool
- {
- return $this->proc !== null && $this->pipes !== null;
- }
-
- private function checkStarted()
- {
- if (!$this->isStarted()) {
- throw new \RuntimeException('You have to start the Worker first!');
- }
- }
-
- public function stop()
- {
- fwrite($this->pipes[0], "EXIT\n");
- fclose($this->pipes[0]);
- }
-
- /**
- * This is an utility function for tests.
- * Refactor or write it only in the test case.
- */
- public function waitForFinishedJob()
- {
- if ($this->inExecution === 0) {
- return;
- }
- $tellsUsItHasFinished = false;
- stream_set_blocking($this->pipes[1], true);
- while ($line = fgets($this->pipes[1])) {
- if (strstr($line, "FINISHED\n")) {
- $tellsUsItHasFinished = true;
- --$this->inExecution;
- break;
- }
- }
- if (!$tellsUsItHasFinished) {
- throw new \RuntimeException('The Worker terminated without finishing the job.');
- }
- }
-
- public function isFree(): bool
- {
- $this->checkNotCrashed();
- $this->updateStateFromAvailableOutput();
-
- return $this->inExecution === 0;
- }
-
- /**
- * @deprecated
- * This function consumes a lot of CPU while waiting for
- * the worker to finish. Use it only in testing paratest
- * itself.
- */
- public function waitForStop()
- {
- $status = proc_get_status($this->proc);
- while ($status['running']) {
- $status = proc_get_status($this->proc);
- $this->setExitCode($status);
- }
- }
-
- public function getCoverageFileName()
- {
- if ($this->currentlyExecuting !== null) {
- return $this->currentlyExecuting->getCoverageFileName();
- }
- }
-
- private function setExitCode(array $status)
- {
- if (!$status['running']) {
- if ($this->exitCode === null) {
- $this->exitCode = $status['exitcode'];
- }
- }
- }
-
- public function isRunning(): bool
- {
- $this->checkNotCrashed();
- $this->updateStateFromAvailableOutput();
-
- return $this->isRunning;
- }
-
- public function isCrashed(): bool
- {
- if (!$this->isStarted()) {
- return false;
- }
- $status = proc_get_status($this->proc);
-
- $this->updateStateFromAvailableOutput();
- if (!$this->isRunning) {
- return false;
- }
-
- $this->setExitCode($status);
- if ($this->exitCode === null) {
- return false;
- }
-
- return $this->exitCode !== 0;
- }
-
- private function checkNotCrashed()
- {
- if ($this->isCrashed()) {
- throw new \RuntimeException(
- 'This worker has crashed. Last executed command: ' . end($this->commands) . PHP_EOL
- . 'Output:' . PHP_EOL
- . '----------------------' . PHP_EOL
- . $this->alreadyReadOutput . PHP_EOL
- . '----------------------' . PHP_EOL
- . $this->readAllStderr()
- );
- }
- }
-
- private function readAllStderr()
- {
- return stream_get_contents($this->pipes[2]);
- }
-
- /**
- * Have to read even incomplete lines to play nice with stream_select()
- * Otherwise it would continue to non-block because there are bytes to be read,
- * but fgets() won't pick them up.
- */
- private function updateStateFromAvailableOutput()
- {
- if (isset($this->pipes[1])) {
- stream_set_blocking($this->pipes[1], false);
- while ($chunk = fread($this->pipes[1], 4096)) {
- $this->chunks .= $chunk;
- $this->alreadyReadOutput .= $chunk;
- }
- $lines = explode("\n", $this->chunks);
- // last element is not a complete line,
- // becomes part of a line completed later
- $this->chunks = $lines[count($lines) - 1];
- unset($lines[count($lines) - 1]);
- // delivering complete lines to this Worker
- foreach ($lines as $line) {
- $line .= "\n";
- if (strstr($line, "FINISHED\n")) {
- --$this->inExecution;
- }
- if (strstr($line, "EXITED\n")) {
- $this->isRunning = false;
- }
- }
- stream_set_blocking($this->pipes[1], true);
- }
- }
-}
diff --git a/src/Runners/PHPUnit/Worker/BaseWorker.php b/src/Runners/PHPUnit/Worker/BaseWorker.php
new file mode 100644
index 00000000..abd8d40f
--- /dev/null
+++ b/src/Runners/PHPUnit/Worker/BaseWorker.php
@@ -0,0 +1,146 @@
+ ['pipe', 'r'],
+ 1 => ['pipe', 'w'],
+ 2 => ['pipe', 'w'],
+ ];
+ protected $proc;
+ protected $pipes;
+ protected $inExecution = 0;
+ private $exitCode = null;
+ private $chunks = '';
+ private $alreadyReadOutput = '';
+
+ public function start(string $wrapperBinary, $token = 1, $uniqueToken = null, array $parameters = [])
+ {
+ $bin = 'PARATEST=1 ';
+ if (is_numeric($token)) {
+ $bin .= "TEST_TOKEN=$token ";
+ }
+ if ($uniqueToken) {
+ $bin .= "UNIQUE_TEST_TOKEN=$uniqueToken ";
+ }
+ $finder = new PhpExecutableFinder();
+ $bin .= $finder->find() . " \"$wrapperBinary\"";
+ if ($parameters) {
+ $bin .= ' ' . implode(' ', array_map('escapeshellarg', $parameters));
+ }
+ $pipes = [];
+ $process = proc_open($bin, self::$descriptorspec, $pipes);
+ $this->proc = is_resource($process) ? $process : null;
+ $this->pipes = $pipes;
+ }
+
+ public function isFree(): bool
+ {
+ $this->checkNotCrashed();
+ $this->updateStateFromAvailableOutput();
+
+ return $this->inExecution === 0;
+ }
+
+ public function isRunning(): bool
+ {
+ if ($this->proc === null) {
+ return false;
+ }
+
+ $status = proc_get_status($this->proc);
+
+ return $status ? $status['running'] : false;
+ }
+
+ public function isStarted(): bool
+ {
+ return $this->proc !== null && $this->pipes !== null;
+ }
+
+ public function isCrashed(): bool
+ {
+ if (!$this->isStarted()) {
+ return false;
+ }
+ $status = proc_get_status($this->proc);
+
+ $this->updateStateFromAvailableOutput();
+
+ $this->setExitCode($status);
+ if ($this->exitCode === null) {
+ return false;
+ }
+
+ return $this->exitCode !== 0;
+ }
+
+ public function checkNotCrashed()
+ {
+ if ($this->isCrashed()) {
+ $lastCommand = isset($this->commands) ? ' Last executed command: ' . end($this->commands) : '';
+ throw new \RuntimeException(
+ 'This worker has crashed.' . $lastCommand . PHP_EOL
+ . 'Output:' . PHP_EOL
+ . '----------------------' . PHP_EOL
+ . $this->alreadyReadOutput . PHP_EOL
+ . '----------------------' . PHP_EOL
+ . $this->readAllStderr()
+ );
+ }
+ }
+
+ public function stop()
+ {
+ fclose($this->pipes[0]);
+ }
+
+ protected function setExitCode(array $status)
+ {
+ if (!$status['running']) {
+ if ($this->exitCode === null) {
+ $this->exitCode = $status['exitcode'];
+ }
+ }
+ }
+
+ private function readAllStderr()
+ {
+ return stream_get_contents($this->pipes[2]);
+ }
+
+ /**
+ * Have to read even incomplete lines to play nice with stream_select()
+ * Otherwise it would continue to non-block because there are bytes to be read,
+ * but fgets() won't pick them up.
+ */
+ private function updateStateFromAvailableOutput()
+ {
+ if (isset($this->pipes[1])) {
+ stream_set_blocking($this->pipes[1], false);
+ while ($chunk = fread($this->pipes[1], 4096)) {
+ $this->chunks .= $chunk;
+ $this->alreadyReadOutput .= $chunk;
+ }
+ $lines = explode("\n", $this->chunks);
+ // last element is not a complete line,
+ // becomes part of a line completed later
+ $this->chunks = $lines[count($lines) - 1];
+ unset($lines[count($lines) - 1]);
+ // delivering complete lines to this Worker
+ foreach ($lines as $line) {
+ $line .= "\n";
+ if (strstr($line, "FINISHED\n")) {
+ --$this->inExecution;
+ }
+ }
+ stream_set_blocking($this->pipes[1], true);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Runners/PHPUnit/Worker/SqliteWorker.php b/src/Runners/PHPUnit/Worker/SqliteWorker.php
new file mode 100644
index 00000000..634a761a
--- /dev/null
+++ b/src/Runners/PHPUnit/Worker/SqliteWorker.php
@@ -0,0 +1,23 @@
+dbFileName = $dbFileName;
+ }
+
+ public function start(string $wrapperBinary, $token = 1, $uniqueToken = null, array $parameters = [])
+ {
+ $parameters[] = $this->dbFileName;
+
+ parent::start($wrapperBinary, $token, $uniqueToken, $parameters);
+ }
+}
\ No newline at end of file
diff --git a/src/Runners/PHPUnit/Worker/WrapperWorker.php b/src/Runners/PHPUnit/Worker/WrapperWorker.php
new file mode 100644
index 00000000..bf983f6b
--- /dev/null
+++ b/src/Runners/PHPUnit/Worker/WrapperWorker.php
@@ -0,0 +1,110 @@
+pipes[1];
+ }
+
+ public function execute(string $testCmd)
+ {
+ $this->checkStarted();
+ $this->commands[] = $testCmd;
+ fwrite($this->pipes[0], $testCmd . "\n");
+ ++$this->inExecution;
+ }
+
+ public function assign(ExecutableTest $test, string $phpunit, array $phpunitOptions)
+ {
+ if ($this->currentlyExecuting !== null) {
+ throw new Exception('Worker already has a test assigned - did you forget to call reset()?');
+ }
+ $this->currentlyExecuting = $test;
+ $this->execute($test->command($phpunit, $phpunitOptions));
+ }
+
+ public function printFeedback(ResultPrinter $printer)
+ {
+ if ($this->currentlyExecuting !== null) {
+ $printer->printFeedback($this->currentlyExecuting);
+ }
+ }
+
+ public function reset()
+ {
+ $this->currentlyExecuting = null;
+ }
+
+ protected function checkStarted()
+ {
+ if (!$this->isStarted()) {
+ throw new \RuntimeException('You have to start the Worker first!');
+ }
+ }
+
+ public function stop()
+ {
+ fwrite($this->pipes[0], "EXIT\n");
+ parent::stop();
+ }
+
+ /**
+ * This is an utility function for tests.
+ * Refactor or write it only in the test case.
+ */
+ public function waitForFinishedJob()
+ {
+ if ($this->inExecution === 0) {
+ return;
+ }
+ $tellsUsItHasFinished = false;
+ stream_set_blocking($this->pipes[1], true);
+ while ($line = fgets($this->pipes[1])) {
+ if (strstr($line, "FINISHED\n")) {
+ $tellsUsItHasFinished = true;
+ --$this->inExecution;
+ break;
+ }
+ }
+ if (!$tellsUsItHasFinished) {
+ throw new \RuntimeException('The Worker terminated without finishing the job.');
+ }
+ }
+
+ /**
+ * @deprecated
+ * This function consumes a lot of CPU while waiting for
+ * the worker to finish. Use it only in testing paratest
+ * itself.
+ */
+ public function waitForStop()
+ {
+ $status = proc_get_status($this->proc);
+ while ($status['running']) {
+ $status = proc_get_status($this->proc);
+ $this->setExitCode($status);
+ }
+ }
+
+ public function getCoverageFileName()
+ {
+ if ($this->currentlyExecuting !== null) {
+ return $this->currentlyExecuting->getCoverageFileName();
+ }
+ }
+}
diff --git a/src/Runners/PHPUnit/WrapperRunner.php b/src/Runners/PHPUnit/WrapperRunner.php
index a980ca2f..2ee06aa9 100644
--- a/src/Runners/PHPUnit/WrapperRunner.php
+++ b/src/Runners/PHPUnit/WrapperRunner.php
@@ -4,6 +4,8 @@
namespace ParaTest\Runners\PHPUnit;
+use ParaTest\Runners\PHPUnit\Worker\WrapperWorker;
+
class WrapperRunner extends BaseRunner
{
const PHPUNIT_FAILURES = 1;
@@ -15,7 +17,7 @@ class WrapperRunner extends BaseRunner
protected $streams;
/**
- * @var Worker[]
+ * @var WrapperWorker[]
*/
protected $workers;
@@ -43,11 +45,11 @@ protected function load(SuiteLoader $loader)
parent::load($loader);
}
- private function startWorkers()
+ protected function startWorkers()
{
$wrapper = realpath(__DIR__ . '/../../../bin/phpunit-wrapper');
for ($i = 1; $i <= $this->options->processes; ++$i) {
- $worker = new Worker();
+ $worker = new WrapperWorker();
if ($this->options->noTestTokens) {
$token = null;
$uniqueToken = null;
@@ -119,7 +121,7 @@ private function waitForStreamsToChange(array $modified)
/**
* put on WorkersPool.
*
- * @return Worker[]
+ * @return WrapperWorker[]
*/
private function progressedWorkers(): array
{
@@ -156,7 +158,7 @@ private function streamsOf(array $workers): array
return $streams;
}
- private function complete()
+ protected function complete()
{
$this->setExitCode();
$this->printer->printResults();
@@ -180,7 +182,7 @@ private function setExitCode()
}
}
- private function flushWorker(Worker $worker)
+ private function flushWorker(WrapperWorker $worker)
{
if ($this->hasCoverage()) {
$this->getCoverage()->addCoverageFromFile($worker->getCoverageFileName());
diff --git a/test/functional/PHPUnitTest.php b/test/functional/PHPUnitTest.php
index d35c2751..3d440662 100644
--- a/test/functional/PHPUnitTest.php
+++ b/test/functional/PHPUnitTest.php
@@ -41,6 +41,14 @@ public function testWithWrapperRunner()
]));
}
+ public function testWithSqliteRunner()
+ {
+ $this->assertTestsPassed($this->invokeParatest('passing-tests', [
+ 'configuration' => PHPUNIT_CONFIGURATION,
+ 'runner' => 'SqliteRunner',
+ ]));
+ }
+
public function testWithCustomRunner()
{
$cb = new ProcessCallback();
@@ -97,6 +105,13 @@ public function testParatestEnvironmentVariableWithWrapperRunnerandWithoutTestTo
$this->assertRegexp('/Failures: 1/', $proc->getOutput());
}
+ public function testParatestEnvironmentVariableWithSqliteRunner()
+ {
+ $this->assertTestsPassed($this->invokeParatest('paratest-only-tests/EnvironmentTest.php',
+ ['bootstrap' => BOOTSTRAP, 'runner' => 'SqliteRunner']
+ ));
+ }
+
public function testWithConfigurationInDirWithoutConfigFile()
{
chdir(dirname(FIXTURES));
diff --git a/test/functional/ParaTest/Runners/PHPUnit/WorkerTest.php b/test/functional/ParaTest/Runners/PHPUnit/WorkerTest.php
index 2657223b..554f48ec 100644
--- a/test/functional/ParaTest/Runners/PHPUnit/WorkerTest.php
+++ b/test/functional/ParaTest/Runners/PHPUnit/WorkerTest.php
@@ -4,6 +4,7 @@
namespace ParaTest\Runners\PHPUnit;
+use ParaTest\Runners\PHPUnit\Worker\WrapperWorker;
use SimpleXMLElement;
class WorkerTest extends \TestBase
@@ -38,7 +39,7 @@ public function testReadsAPHPUnitCommandFromStdInAndExecutesItItsOwnProcess()
{
$testLog = '/tmp/test.xml';
$testCmd = $this->getCommand('passing-tests/TestOfUnits.php', $testLog);
- $worker = new Worker();
+ $worker = new WrapperWorker();
$worker->start($this->phpunitWrapper);
$worker->execute($testCmd);
@@ -52,7 +53,7 @@ public function testKnowsWhenAJobIsFinished()
{
$testLog = '/tmp/test.xml';
$testCmd = $this->getCommand('passing-tests/TestOfUnits.php', $testLog);
- $worker = new Worker();
+ $worker = new WrapperWorker();
$worker->start($this->phpunitWrapper);
$worker->execute($testCmd);
$worker->waitForFinishedJob();
@@ -64,7 +65,7 @@ public function testTellsWhenItsFree()
{
$testLog = '/tmp/test.xml';
$testCmd = $this->getCommand('passing-tests/TestOfUnits.php', $testLog);
- $worker = new Worker();
+ $worker = new WrapperWorker();
$worker->start($this->phpunitWrapper);
$this->assertTrue($worker->isFree());
@@ -77,7 +78,7 @@ public function testTellsWhenItsFree()
public function testTellsWhenItsStopped()
{
- $worker = new Worker();
+ $worker = new WrapperWorker();
$this->assertFalse($worker->isRunning());
$worker->start($this->phpunitWrapper);
@@ -88,14 +89,13 @@ public function testTellsWhenItsStopped()
$this->assertFalse($worker->isRunning());
}
- public function testProcessIsNotMarkedAsCrashedWhenItFinishesCorrectly()
+ public function testProcessIsMarkedAsCrashedWhenItFinishesWithNonZeroExitCode()
{
- // fake state: process has already exited (clean) but worker did not yet notice
- $worker = new Worker();
- $this->setPerReflection($worker, 'isRunning', false);
+ // fake state: process has already exited (with non-zero exit code) but worker did not yet notice
+ $worker = new WrapperWorker();
$this->setPerReflection($worker, 'proc', $this->createSomeClosedProcess());
$this->setPerReflection($worker, 'pipes', [0 => true]);
- $this->assertFalse($worker->isCrashed());
+ $this->assertTrue($worker->isCrashed());
}
private function createSomeClosedProcess()
@@ -127,7 +127,7 @@ public function testCanExecuteMultiplePHPUnitCommands()
{
$bin = 'bin/phpunit-wrapper';
- $worker = new Worker();
+ $worker = new WrapperWorker();
$worker->start($this->phpunitWrapper);
$testLog = '/tmp/test.xml';
diff --git a/test/functional/SqliteRunnerTest.php b/test/functional/SqliteRunnerTest.php
new file mode 100644
index 00000000..6e3ef30d
--- /dev/null
+++ b/test/functional/SqliteRunnerTest.php
@@ -0,0 +1,81 @@
+generate(self::TEST_CLASSES, self::TEST_METHODS_PER_CLASS);
+
+ $proc = $this->invokeParatest($generator->path, [
+ 'runner' => 'SqliteRunner',
+ 'processes' => 3,
+ ]);
+
+ $expected = self::TEST_CLASSES * self::TEST_METHODS_PER_CLASS;
+ $this->assertTestsPassed($proc, $expected, $expected);
+ }
+
+ public function testMultiLineClassDeclarationWithFilenameDifferentThanClassnameIsSupported()
+ {
+ $this->assertTestsPassed($this->invokeParatest('special-classes', [
+ 'runner' => 'SqliteRunner',
+ 'processes' => 3,
+ ]));
+ }
+
+ public function testRunningFewerTestsThanTheWorkersIsPossible()
+ {
+ $generator = new TestGenerator();
+ $generator->generate(1, 1);
+
+ $proc = $this->invokeParatest($generator->path, [
+ 'runner' => 'SqliteRunner',
+ 'processes' => 2,
+ ]);
+
+ $this->assertTestsPassed($proc, 1, 1);
+ }
+
+ public function testExitCodes()
+ {
+ $options = [
+ 'runner' => 'SqliteRunner',
+ 'processes' => 1,
+ ];
+ $proc = $this->invokeParatest('wrapper-runner-exit-code-tests/ErrorTest.php', $options);
+ $output = $proc->getOutput();
+
+ $this->assertContains('Tests: 1', $output);
+ $this->assertContains('Failures: 0', $output);
+ $this->assertContains('Errors: 1', $output);
+ $this->assertEquals(2, $proc->getExitCode());
+
+ $proc = $this->invokeParatest('wrapper-runner-exit-code-tests/FailureTest.php', $options);
+ $output = $proc->getOutput();
+
+ $this->assertContains('Tests: 1', $output);
+ $this->assertContains('Failures: 1', $output);
+ $this->assertContains('Errors: 0', $output);
+ $this->assertEquals(1, $proc->getExitCode());
+
+ $proc = $this->invokeParatest('wrapper-runner-exit-code-tests/SuccessTest.php', $options);
+ $output = $proc->getOutput();
+
+ $this->assertContains('OK (1 test, 1 assertion)', $output);
+ $this->assertEquals(0, $proc->getExitCode());
+
+ $options['processes'] = 3;
+ $proc = $this->invokeParatest('wrapper-runner-exit-code-tests', $options);
+ $output = $proc->getOutput();
+ $this->assertContains('Tests: 3', $output);
+ $this->assertContains('Failures: 1', $output);
+ $this->assertContains('Errors: 1', $output);
+ $this->assertEquals(2, $proc->getExitCode()); // There is at least one error so the exit code must be 2
+ }
+}
diff --git a/test/unit/Console/Commands/ParaTestCommandTest.php b/test/unit/Console/Commands/ParaTestCommandTest.php
index ab8ab0c5..1fb4ec71 100644
--- a/test/unit/Console/Commands/ParaTestCommandTest.php
+++ b/test/unit/Console/Commands/ParaTestCommandTest.php
@@ -40,7 +40,7 @@ public function testConfiguredDefinitionWithPHPUnitTester()
new InputOption('functional', 'f', InputOption::VALUE_NONE, 'Run methods instead of suites in separate processes.'),
new InputOption('help', 'h', InputOption::VALUE_NONE, 'Display this help message.'),
new InputOption('phpunit', null, InputOption::VALUE_REQUIRED, 'The PHPUnit binary to execute. (default: vendor/bin/phpunit)'),
- new InputOption('runner', null, InputOption::VALUE_REQUIRED, 'Runner or WrapperRunner. (default: Runner)'),
+ new InputOption('runner', null, InputOption::VALUE_REQUIRED, 'Runner, WrapperRunner or SqliteRunner. (default: Runner)'),
new InputOption('bootstrap', null, InputOption::VALUE_REQUIRED, 'The bootstrap file to be used by PHPUnit.'),
new InputOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'The PHPUnit configuration file to use.'),
new InputOption('group', 'g', InputOption::VALUE_REQUIRED, 'Only runs tests from the specified group(s).'),
diff --git a/test/unit/Console/Testers/PHPUnitTest.php b/test/unit/Console/Testers/PHPUnitTest.php
index e681d5ca..e722e070 100644
--- a/test/unit/Console/Testers/PHPUnitTest.php
+++ b/test/unit/Console/Testers/PHPUnitTest.php
@@ -16,7 +16,7 @@ public function testConfigureAddsOptionsAndArgumentsToCommand()
$testCommand = new TestCommand();
$definition = new InputDefinition([
new InputOption('phpunit', null, InputOption::VALUE_REQUIRED, 'The PHPUnit binary to execute. (default: vendor/bin/phpunit)'),
- new InputOption('runner', null, InputOption::VALUE_REQUIRED, 'Runner or WrapperRunner. (default: Runner)'),
+ new InputOption('runner', null, InputOption::VALUE_REQUIRED, 'Runner, WrapperRunner or SqliteRunner. (default: Runner)'),
new InputOption('bootstrap', null, InputOption::VALUE_REQUIRED, 'The bootstrap file to be used by PHPUnit.'),
new InputOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'The PHPUnit configuration file to use.'),
new InputOption('group', 'g', InputOption::VALUE_REQUIRED, 'Only runs tests from the specified group(s).'),