Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Features/ Uniquely identify concurrent processes #23

Merged
merged 3 commits into from

3 participants

@dbaltas
Collaborator

Allows phpunit to run with an extra environment variable, TEST_TOKEN
which is unique per running process.

Paratest attaches the TEST_TOKEN environment variable to the command.

TEST_TOKEN=1 phpunit file1
TEST_TOKEN=2 phpunit file2
TEST_TOKEN=3 phpunit file3

When a running process ends, the token is released to be acquired by the next process to run.

The test suite can use the value of the variable to ensure a unique resource (database, file system) for parallel processes .

In the application code (TestBootstrap)

        $testToken = getenv('TEST_TOKEN');
        if (is_numeric($testToken)) {
            $databaseName .= sprintf("%s", (int)$testToken+1);
        }
@brianium
Owner

Very cool. Pulling now. I might want to spend a little time looking at this one. Definitely a cool feature, and I think it belongs in paratest. I want to keep an eye on the ExecutableTest base and the Runner as they grow. I should have some input for you tonight or tomorrow hopefully. Thanks again for your work!

@dbaltas
Collaborator

Here is the script we used to clone the test databases, and synchronize their schema on our ZF1.x codebase
https://github.com/tripsta/wink-clone-databases

Our test suite consisting of 13.000 tests dropped from 16 minutes to 4:10 (~75%).
We used 8 parallel processes.

@giorgiosironi
Collaborator

I think I could use the same mechanism for reuse_bootstrap. However, the TEST_TOKEN will be passed to the Worker process once at its startup.

@giorgiosironi
Collaborator

Have been using this code on our real world test suite and it works like a charm. +1 for me

@giorgiosironi giorgiosironi merged commit ef7ce73 into from
@giorgiosironi
Collaborator

Merged manually due to a conflict in ExecutableTest.

@brianium
Owner

This bit is pretty impressive. I think we might need to get a wiki going to demonstrate how this stuff can be leveraged.

@giorgiosironi
Collaborator

After my phpDay presentation I will paste some code from it in the wiki of the project.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
2  it/ParaTest/Runners/PHPUnit/ExecutableTestTest.php → ...Runners/PHPUnit/ExecutableTestIntegrationTest.php
@@ -1,6 +1,6 @@
<?php namespace ParaTest\Runners\PHPUnit;
-class ExecutableTestTest extends \TestBase
+class ExecutableTestIntegrationTest extends \TestBase
{
protected $suite;
View
25 src/ParaTest/Runners/PHPUnit/ExecutableTest.php
@@ -8,6 +8,7 @@
protected $process;
protected $status;
protected $stderr;
+ protected $token;
protected static $descriptors = array(
0 => array('pipe', 'r'),
@@ -69,7 +70,7 @@ public function isDoneRunning()
}
/**
- * Called after a polling context to retrieve
+ * Called after a polling context to retrieve
* the exit code of the phpunit process
*/
public function getExitCode()
@@ -77,10 +78,11 @@ public function getExitCode()
return $this->status['exitcode'];
}
- public function run($binary, $options = array())
+ public function run($binary, $options = array(), $environmentVariables = array())
{
$options = array_merge($this->prepareOptions($options), array('log-junit' => $this->getTempFile()));
- $command = $this->getCommandString($binary, $options);
+ $this->handleEnvironmentVariables($environmentVariables);
+ $command = $this->getCommandString($binary, $options, $environmentVariables);
$this->process = proc_open($command, self::$descriptors, $this->pipes);
return $this;
}
@@ -101,12 +103,25 @@ protected function prepareOptions($options)
return $options;
}
- protected function getCommandString($binary, $options = array())
+ protected function getCommandString($binary, $options = array(), $environmentVariables = array())
{
$command = $binary;
+ $environmentVariablePrefix = '';
+
foreach($options as $key => $value) $command .= " --$key %s";
- $args = array_merge(array("$command %s"), array_values($options), array($this->getPath()));
+ foreach($environmentVariables as $key => $value) $environmentVariablePrefix .= "$key=%s ";
+ $args = array_merge(array("$environmentVariablePrefix$command %s"), array_values($environmentVariables), array_values($options), array($this->getPath()));
$command = call_user_func_array('sprintf', $args);
return $command;
}
+
+ public function getToken()
+ {
+ return $this->token;
+ }
+
+ protected function handleEnvironmentVariables($environmentVariables)
+ {
+ if (isset($environmentVariables['TEST_TOKEN'])) $this->token = $environmentVariables['TEST_TOKEN'];
+ }
}
View
44 src/ParaTest/Runners/PHPUnit/Runner.php
@@ -13,12 +13,14 @@ class Runner
protected $interpreter;
protected $printer;
protected $exitcode = -1;
-
+ protected $tokens = array();
+
public function __construct($opts = array())
{
$this->options = new Options($opts);
$this->interpreter = new LogInterpreter();
$this->printer = new ResultPrinter($this->interpreter);
+ $this->initTokens();
}
public function run()
@@ -28,7 +30,10 @@ public function run()
$this->printer->start($this->options);
while(count($this->running) || count($this->pending)) {
foreach($this->running as $key => $test)
- if(!$this->testIsStillRunning($test)) unset($this->running[$key]);
+ if(!$this->testIsStillRunning($test)) {
+ unset($this->running[$key]);
+ $this->releaseToken($key);
+ }
$this->fillRunQueue();
}
$this->complete();
@@ -78,8 +83,13 @@ private function log()
private function fillRunQueue()
{
$opts = $this->options;
- while(sizeof($this->pending) && sizeof($this->running) < $opts->processes)
- $this->running[] = array_shift($this->pending)->run($opts->phpunit, $opts->filtered);
+ while(sizeof($this->pending) && sizeof($this->running) < $opts->processes) {
+ $token = $this->getNextAvailableToken();
+ if ($token !== false) {
+ $this->acquireToken($token);
+ $this->running[$token] = array_shift($this->pending)->run($opts->phpunit, $opts->filtered, array('TEST_TOKEN' => $token));
+ }
+ }
}
private function testIsStillRunning($test)
@@ -99,4 +109,30 @@ private function setExitCode(ExecutableTest $test)
if($exit > $this->exitcode)
$this->exitcode = $exit;
}
+
+ protected function initTokens()
+ {
+ $this->tokens = array();
+ for ($i=0; $i< $this->options->processes; $i++) {
+ $this->tokens[$i] = true;
+ }
+ }
+
+ protected function getNextAvailableToken()
+ {
+ for ($i=0; $i< count($this->tokens); $i++) {
+ if ($this->tokens[$i]) return $i;
+ }
+ return false;
+ }
+
+ protected function releaseToken($tokenIdentifier)
+ {
+ $this->tokens[$tokenIdentifier] = true;
+ }
+
+ protected function acquireToken($tokenIdentifier)
+ {
+ $this->tokens[$tokenIdentifier] = false;
+ }
}
View
58 test/ParaTest/Runners/PHPUnit/ExecutableTestTest.php
@@ -0,0 +1,58 @@
+<?php namespace ParaTest\Runners\PHPUnit;
+
+class ExecutableTestChild extends ExecutableTest
+{
+
+}
+
+class ExecutableTestTest extends \TestBase
+{
+ /**
+ *
+ * @var ExecutableTestChild
+ */
+ protected $executableTestChild;
+
+ public function setUp()
+ {
+ $this->executableTestChild = new ExecutableTestChild('pathToFile');
+ parent::setUp();
+ }
+
+ public function testConstructor()
+ {
+ $this->assertEquals('pathToFile', $this->getObjectValue($this->executableTestChild, 'path'));
+ }
+
+ public function testGetCommandStringIncludesOptions()
+ {
+ $options = array('bootstrap' => 'test/bootstrap.php');
+ $binary = '/usr/bin/phpunit';
+
+ $command = $this->call($this->executableTestChild, 'getCommandString', $binary, $options);
+ $this->assertEquals('/usr/bin/phpunit --bootstrap test/bootstrap.php pathToFile', $command);
+ }
+
+ public function testGetCommandStringIncludesEnvironmentVariables()
+ {
+ $options = array('bootstrap' => 'test/bootstrap.php');
+ $binary = '/usr/bin/phpunit';
+ $environmentVariables = array('TEST_TOKEN' => 3, 'APPLICATION_ENVIRONMENT_VAR' => 'abc');
+ $command = $this->call($this->executableTestChild, 'getCommandString', $binary, $options, $environmentVariables);
+
+ $this->assertEquals('TEST_TOKEN=3 APPLICATION_ENVIRONMENT_VAR=abc /usr/bin/phpunit --bootstrap test/bootstrap.php pathToFile', $command);
+ }
+
+ public function testHandleEnvironmentVariablesAssignsToken()
+ {
+ $environmentVariables = array('TEST_TOKEN' => 3, 'APPLICATION_ENVIRONMENT_VAR' => 'abc');
+ $this->call($this->executableTestChild, 'handleEnvironmentVariables', $environmentVariables);
+ $this->assertEquals(3, $this->getObjectValue($this->executableTestChild, 'token'));
+ }
+
+ public function testGetTokenReturnsValidToken()
+ {
+ $this->setObjectValue($this->executableTestChild, 'token', 3);
+ $this->assertEquals(3, $this->executableTestChild->getToken());
+ }
+}
View
43 test/ParaTest/Runners/PHPUnit/RunnerTest.php
@@ -34,4 +34,47 @@ public function testGetExitCode()
{
$this->assertEquals(-1, $this->runner->getExitCode());
}
+
+ public function testConstructorAssignsTokens()
+ {
+ $opts = array('processes' => 4, 'path' => FIXTURES . DS . 'tests', 'bootstrap' => 'hello', 'functional' => true);
+ $runner = new Runner($opts);
+ $tokens = $this->getObjectValue($runner, 'tokens');
+ $this->assertEquals(4, count($tokens));
+ }
+
+ public function testGetsNextAvailableTokenReturnsTokenIdentifier()
+ {
+ $tokens = array(0 => false, 1 => false, 2 => true, 3 => false);
+ $opts = array('processes' => 4, 'path' => FIXTURES . DS . 'tests', 'bootstrap' => 'hello', 'functional' => true);
+ $runner = new Runner($opts);
+ $this->setObjectValue($runner, 'tokens', $tokens);
+
+ $token = $this->call($runner, 'getNextAvailableToken');
+ $this->assertEquals(2, $token);
+ }
+
+ public function testGetNextAvailableTokenReturnsFalseWhenNoTokensAreAvailable()
+ {
+ $tokens = array(0 => false, 1 => false, 2 => false, 3 => false);
+ $opts = array('processes' => 4, 'path' => FIXTURES . DS . 'tests', 'bootstrap' => 'hello', 'functional' => true);
+ $runner = new Runner($opts);
+ $this->setObjectValue($runner, 'tokens', $tokens);
+
+ $token = $this->call($runner, 'getNextAvailableToken');
+ $this->assertTrue($token === false);
+ }
+
+ public function testReleaseTokenMakesTokenAvailable()
+ {
+ $tokens = array(0 => false, 1 => false, 2 => false, 3 => false);
+ $opts = array('processes' => 4, 'path' => FIXTURES . DS . 'tests', 'bootstrap' => 'hello', 'functional' => true);
+ $runner = new Runner($opts);
+ $this->setObjectValue($runner, 'tokens', $tokens);
+
+ $this->assertFalse($tokens[1]);
+ $this->call($runner, 'releaseToken', 1);
+ $tokens = $this->getObjectValue($runner, 'tokens');
+ $this->assertTrue($tokens[1]);
+ }
}
View
10 test/ParaTest/Runners/PHPUnit/TestMethodTest.php
@@ -0,0 +1,10 @@
+<?php namespace ParaTest\Runners\PHPUnit;
+
+class TestMethodTest extends \TestBase
+{
+ public function testConstructor()
+ {
+ $testMethod = new TestMethod('pathToFile', 'methodName');
+ $this->assertEquals('pathToFile', $this->getObjectValue($testMethod, 'path'));
+ }
+}
View
6 test/TestBase.php
@@ -16,6 +16,12 @@ protected function getObjectValue($object, $property)
return $prop->getValue($object);
}
+ protected function setObjectValue($object, $property, $value)
+ {
+ $prop = $this->getAccessibleProperty($object, $property);
+ return $prop->setValue($object, $value);
+ }
+
private function getAccessibleProperty($object, $property)
{
$refl = new \ReflectionObject($object);
Something went wrong with that request. Please try again.