* @copyright 2011-2012 Hans-Peter Buniat * @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();