Created
July 6, 2011 12:02
-
-
Save hpbuniat/1067072 to your computer and use it in GitHub Desktop.
Wrapper for selenium-tests, to execute tests in parallel
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Wrapper for selenium-tests, to execute tests in parallel | |
* | |
* @author Hans-Peter Buniat <[email protected]> | |
* @copyright 2011-2012 Hans-Peter Buniat <[email protected]> | |
* @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