Skip to content

Instantly share code, notes, and snippets.

@hpbuniat
Created July 6, 2011 12:02
Show Gist options
  • Save hpbuniat/1067072 to your computer and use it in GitHub Desktop.
Save hpbuniat/1067072 to your computer and use it in GitHub Desktop.
Wrapper for selenium-tests, to execute tests in parallel
<?php
/**
* Wrapper for selenium-tests, to execute tests in parallel
*
* @author Hans-Peter Buniat <hpbuniat@googlemail.com>
* @copyright 2011-2012 Hans-Peter Buniat <hpbuniat@googlemail.com>
* @license http://www.opensource.org/licenses/bsd-license.php BSD License
*/
class ParallelTests {
/**
* The Test-Cases
*
* @var array
*/
private $_aTests = array();
/**
* Running processes
*
* @var array
*/
private $_aProc = array();
/**
* The Test-Results
*
* @var array
*/
private $_aResult = array();
/**
* Shared-Memory
*
* @var resource
*/
private $_rShared = null;
/**
* Number of parallel threads
*
* @var int
*/
private $_iThreads = 0;
/**
* The environment
*
* @var string
*/
protected $_sEnv;
/**
* Filter specific portals
*
* @var array
*/
protected $_aFilter;
/**
* The test-count
*
* @var int
*/
protected $_iCount = 0;
/**
* Number of threads (default)
*
* @var int
*/
const THREADS = 15;
/**
* The default environment
*
* @var string
*/
const ENVIRONMENT = 'staging';
/**
* Init the Wrapper
*/
public function __construct() {
$this->_loadTests();
$this->_rShared = shm_attach(ftok(tempnam('/tmp', __FILE__), 'a'), '1048576');
}
/**
* Load the tests
*
*@return ParallelTests
*/
protected function _loadTests() {
$this->_aTests = glob('*/Test*.php');
return $this;
}
/**
* Set number of threads
*
* @param int $iThreads Number of parallel Threads
*
* @return ParallelTests
*/
public function threads($iThreads) {
$this->_iThreads = (int) $iThreads;
if ($this->_iThreads === 0) {
$this->_iThreads = self::THREADS;
}
return $this;
}
/**
* Set the environment
*
* @param string $sEnvironment
*
* @return ParallelTests
*/
public function env($sEnvironment = self::ENVIRONMENT) {
$this->_sEnv = $sEnvironment;
return $this;
}
/**
* Set the portal-filter
*
* @param string $sFilter
*
* @return ParallelTests
*/
public function filter($sFilter) {
$this->_loadTests();
$aFilter = array();
if (empty($sFilter) !== true) {
$aFilter = explode(',', $sFilter);
array_walk($aFilter, 'trim');
if (empty($aFilter) !== true) {
$this->_aFilter = $aFilter;
$aTests = array();
foreach ($this->_aTests as $sTest) {
$oReflection = $this->_getTestClass($sTest);
if (in_array($oReflection->getConstant('PORTAL'), $this->_aFilter) === true or in_array($sTest, $this->_aFilter) === true) {
$aTests[] = $sTest;
}
}
$this->_aTests = $aTests;
}
}
return $this;
}
/**
* Extract all test-methods from the tests to execute them in parallel
*
* @return ParallelTests
*/
public function parallelize() {
$aTests = array();
foreach ($this->_aTests as $sTest) {
$oReflection = $this->_getTestClass($sTest);
$aTestMethods = $oReflection->getMethods();
foreach ($aTestMethods as $oMethod) {
if (substr($oMethod->getName(), 0, 8) === 'testCase') {
$aTests[] = array(
'test' => $sTest,
'method' => $oMethod->getName(),
'description' => $this->_parseComment($oMethod->getDocComment())
);
}
}
}
$this->_iCount = count($aTests);
$this->_aTests = $aTests;
return $this;
}
/**
* Get the test-class of a file
*
* @param string $sTest
*
* @return ReflectionClass
*/
protected function _getTestClass($sTest) {
$sClass = str_replace(array('/', '.php'), array('_', ''), $sTest);
require_once $sTest;
return new ReflectionClass($sClass);
}
/**
* Parse a doc-domment
*
* @param string $sComment
*
* @return string
*/
protected function _parseComment($sComment) {
if (empty($sComment) !== true) {
$aLines = array();
preg_match_all('#^\s*\*(.*)#m', $sComment, $aLines);
if (empty($aLines) !== true) {
$sComment = trim($aLines[1][0]);
}
}
return $sComment;
}
/**
* Get a string as test-description
*
* @param array $aTest
*
* @return string
*/
protected function _getTestString($aTest) {
return sprintf('running %s :: %s (%s)', $aTest['test'], $aTest['method'], $aTest['description']);
}
/**
* Run
*
* @return ParallelTests
*/
public function run() {
$this->parallelize();
$this->dump(sprintf('Found %d Tests', $this->_iCount));
foreach ($this->_aTests as $iTest => $aTest) {
$iChildren = count($this->_aProc);
$this->dump($this->_getTestString($aTest));
if ($iChildren < $this->_iThreads or $this->_iThreads === 0) {
$this->_aProc[$iTest] = pcntl_fork();
if ($this->_aProc[$iTest] == -1) {
die('could not fork');
}
elseif ($this->_aProc[$iTest] === 0) {
$this->_execute($aTest, $iTest);
}
}
while (count($this->_aProc) >= $this->_iThreads and $this->_iThreads !== 0) {
$this->_wait()->_read();
}
}
$this->_wait(true)->_read();
shm_remove($this->_rShared);
shm_detach($this->_rShared);
return $this;
}
/**
* Execute a child
*
* @param array $aTest Test to execute
* @param int $iTest Test-Index
*
* @return ParallelTests
*/
private function _execute($aTest, $iTest) {
$rCommand = popen(sprintf('sh selenium.sh %s %s %s', $aTest['test'], $this->_sEnv, $aTest['method']), 'r');
$sContent = '';
while (feof($rCommand) !== true) {
$sContent .= fread($rCommand, 4096);
}
$iStatus = pclose($rCommand);
shm_put_var($this->_rShared, $iTest, array(
'code' => $iStatus,
'output' => $sContent
));
posix_kill(getmypid(), 9);
return $this;
}
/**
* Wait for runnings childs to finish
*
* @param boolean $bAll
*
* @return ParallelTests
*/
private function _wait($bAll = false) {
$iChildren = count($this->_aProc);
do {
$iStatus = null;
$iPid = pcntl_waitpid(-1, $iStatus, WNOHANG);
$bUnset = false;
foreach ($this->_aProc as $sChild => $iChild) {
if ($iChild == $iPid) {
unset($this->_aProc[$sChild]);
$bUnset = true;
}
}
if ($bUnset === false) {
usleep(10000);
}
$iChildren = count($this->_aProc);
}
while ($iChildren > 0 and $bAll === true);
return $this;
}
/**
* Read the test-results from shared-memory
*
* @return ParallelTests
*/
private function _read() {
foreach ($this->_aTests as $iTest => $aTest) {
if (shm_has_var($this->_rShared, $iTest) === true) {
$this->_aTests[$iTest] = array(
'name' => $this->_getTestString($aTest)
);
$this->_aTests[$iTest] = array_merge($this->_aTests[$iTest], shm_get_var($this->_rShared, $iTest));
$this->dump(sprintf('Test %s finished: %s', $this->_aTests[$iTest]['name'], ($this->_hasErrors($this->_aTests[$iTest]) === true) ? 'Error' : 'Success'));
shm_remove_var($this->_rShared, $iTest);
}
}
return $this;
}
/**
* Print something to stdout
*
* @param string $sText
* @param boolean $bCr
*
* @return ParallelTests
*/
public function dump($sText = '', $bCr = false) {
$p = '[';
print_r((($bCr) ? "\r" : '') . $p . date('H:i:s') . ']: ' . $sText . (($bCr) ? " \r" : PHP_EOL));
return $this;
}
/**
* Analyse the results
*
* @return void
*/
public function finish() {
$aCounts = array(
'success' => 0,
'failure' => 0
);
foreach ($this->_aTests as $aTest) {
if ($this->_hasErrors($aTest) === true) {
$this->dump('Failures in Test: ' . $aTest['name']);
print_r($aTest['output']);
$aCounts['failure']++;
}
else {
$aCounts['success']++;
}
}
$this->dump('Summary: ' . print_r($aCounts, true));
}
/**
* Determine if the test was not successful
*
* @param array $aTest
*
* @return boolean
*/
protected function _hasErrors(array $aTest) {
return ($aTest['code'] !== 0 or stripos($aTest['output'], 'FAILURES!') !== false);
}
}
/**
* Usage:
*
* script -t 15 Number of parallel threads
* -m online Mode which is passed to the phpunit invoker
* -f portalA,Test123 Filter tests according to portal or test-name
*/
$aArgs = getopt('t:m:f:');
$o = new ParallelTests();
$o->threads(isset($aArgs['t']) === true ? $aArgs['t'] : ParallelTests::THREADS)
->env(isset($aArgs['m']) === true ? $aArgs['m'] : ParallelTests::ENVIRONMENT)
->filter(isset($aArgs['f']) === true ? $aArgs['f'] : null)
->run()
->finish();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment