/ */ class TestUtility { private $_testClass; private $_testMethod; private $_logFile; private $_p4Params = array(); private $_testSites = array(); const TEST_MAX_TRY_COUNT = 1000; /** * Constructor for this class * * @param string $testClass The name of class of the test * @param string $testMethod The name of method of the test */ public function __construct($testClass, $testMethod) { $this->_testClass = $testClass; $this->_testMethod = $testMethod; $this->_logFile = TEST_LOG_PATH .'/'. $testClass .'-'. $testMethod .'.log'; // configure p4 connection params. $serverRoot = TEST_DATA_PATH .'/server-'. $testClass .'-'. $testMethod; $clientRoot = TEST_DATA_PATH .'/clients-'. $testClass .'-'. $testMethod; $this->_p4Params = array( 'serverRoot' => $serverRoot, 'clientRoot' => $clientRoot, 'port' => 'rsh:' . P4D_PATH . ' -iqr ' . $serverRoot . ' -J off ' . '-vtrack=0 -vserver.locks.dir=disabled', 'user' => 'tester', 'client' => 'test-client', 'group' => 'test-group', 'password' => 'testing123' ); // define at least one site configuration for testing purposes. $prefix = '//' . P4Cms_Site::SITE_PREFIX; $suffix = '/' . P4Cms_Site::DEFAULT_BRANCH; $this->_testSites = array( $prefix . 'test' . $suffix => array( 'urls' => array( defined('HTTP_HOST') ? preg_replace('#^http://#', '', HTTP_HOST) : 'test-host.com' ) ) ); } /** * Get Perforce config parameters * * @param string $param Optional - specific Perforce parameter to get * * @return mixed A specific Perforce parameter, or all parameters */ public function getP4Params($param = null) { $params = $this->_p4Params; if ($param) { return isset($params[$param]) ? $params[$param] : null; } return $params; } /** * Get test sites configuration * * @return array A site configuration array */ public function getTestSites() { return $this->_testSites; } /** * Create a Perforce connection for testing. The perforce connection will * connect using a p4d started with the -i (run for inetd) flag. * * @param string $type Allow caller to force the API implementation. * * @return P4_Connection_Interface A Perforce API implementation. */ public function createP4Connection($type = null) { // make sure we're using the bundled p4/p4d. require_once(APPLICATION_PATH . '/Bootstrap.php'); Bootstrap::initPath(); extract($this->_p4Params); if (!is_dir($serverRoot)) { throw new P4_Exception('Unable to create new server.'); } // create connection. $p4 = P4_Connection::factory( $port, $user, $client, $password, null, $type ); // create user. $userForm = array( 'User' => $user, 'Email' => $user . '@testhost', 'FullName' => 'Test User', 'Password' => $password ); $p4->run('user', '-i', $userForm); $p4->run('login', array(), $password); // establish protections. // This looks like a no-op, but remember that fresh P4 servers consider // every user to be a superuser. These operations make only the configured // user a superuser, and subsequent users will be 'normal' users. $result = $p4->run('protect', '-o'); $protect = $result->getData(0); $p4->run('protect', '-i', $protect); // create client $clientForm = array( 'Client' => $client, 'Owner' => $user, 'Root' => $clientRoot .'/superuser', 'View' => array('//depot/... //'. $client .'/...') ); $p4->run('client', '-i', $clientForm); $this->openPermissions($serverRoot, true); return $p4; } /** * Recursively remove a directory and all of it's file contents. * * @param string $directory The directory to remove. * @param boolean $recursive when true, recursively delete directories. * @param boolean $removeRoot when true, remove the root (passed) directory too */ public function removeDirectory($directory, $recursive = true, $removeRoot = true) { if (is_dir($directory)) { $files = new RecursiveDirectoryIterator($directory); foreach ($files as $file) { if ($files->isDot()) { continue; } if ($file->isFile()) { // on Windows, it may take some time for open file handles to // be closed. We try to unlink a file for TEST_MAX_TRY_COUNT // times and then bail out. $count = 0; chmod($file->getPathname(), 0777); while ($count <= self::TEST_MAX_TRY_COUNT) { try { unlink($file->getPathname()); break; } catch (Exception $e) { $count++; if ($count == self::TEST_MAX_TRY_COUNT) { throw new Exception( "Can't delete '" . $file->getPathname() . "' with message ".$e->getMessage() ); } } } } elseif ($file->isDir() && $recursive) { $this->removeDirectory($file->getPathname(), true, true); } } if ($removeRoot) { chmod($directory, 0777); $count = 0; while ($count <= self::TEST_MAX_TRY_COUNT) { try { rmdir($directory); break; } catch (Exception $e) { $count++; if ($count == self::TEST_MAX_TRY_COUNT) { throw new Exception( "Can't delete '" . $directory->getPathname() . "' with message ".$e->getMessage() ); } } } } } } /** * Removes any existing sites (streams) and creates new depots * and 'live' streams based on the passed sites array. * * @param array $sites sites to create. */ public function saveSites($sites) { $p4 = $this->p4; $result = $p4->run('streams'); foreach ($result->getData() as $stream) { // remove any clients of this stream so we can delete it $clients = $p4->run('clients', array('-S', $stream['Stream'])); foreach ($clients->getData() as $client) { $p4->run('client', array('-d', '-f', $client['client'])); } $p4->run('stream', array('-d', '-f', $stream['Stream'])); } // we're done if no sites were passed; return early if (empty($sites)) { return; } // remember our original test client // (site->setConnection will change it below) $client = $p4->getClient(); foreach ($sites as $id => $config) { preg_match('#^//([^/]+)/(.+)#', $id, $matches); $depot = $matches[1]; $stream = $matches[2]; $input = array( 'Depot' => $depot, 'Type' => 'stream', 'Map' => $depot . '/...' ); $result = $p4->run('depot', '-i', $input); $input = array( 'Stream' => $id, 'Name' => $stream, 'Parent' => 'none', 'Type' => 'mainline', 'Owner' => $p4->getUser(), 'Paths' => array('share ...') ); $result = $p4->run('stream', '-i', $input); // write site branch config. $site = new P4Cms_Site; $site->setId($id) ->setConnection($p4) ->getConfig() ->setValues($config) ->save(); } // force the disconnect callback(s) the site objects // added to run prior to setting back our client $p4->disconnect()->connect(); // restore original client. $p4->setClient($client); } /** * Generate the test sites file. * * @todo consider using setup controller's createSite() method * to more faithfully mimic properly configured sites. */ public function createTestSites() { P4Cms_Site::setSitesPackagesPath(TEST_SITES_PATH); P4Cms_Site::setSitesDataPath(TEST_DATA_PATH . '/sites'); // save test sites. $this->saveSites($this->_testSites); // configure environment for first site. $firstSite = reset($this->_testSites); $_SERVER['HTTP_HOST'] = $firstSite['urls'][0]; $_SERVER['REQUEST_URI'] = '/'; // create built-in system roles for first site. $site = P4Cms_Site::fetch(key($this->_testSites)); $site->getConfig() ->setTitle('testsite') ->setDescription('description of the test site') ->save(); $this->createSiteRoles($site); } /** * Create the built-in system roles (member and admin) * * @param P4Cms_Site $site site object */ public function createSiteRoles($site) { $adapter = $site->getStorageAdapter(); $user = $this->getP4Params('user'); // create the base site group and add system user to it. $siteGroup = new P4_Group($adapter->getConnection()); $siteGroup->setId($adapter->getProperty(P4Cms_Acl_Role::PARENT_GROUP)) ->setUsers(array($user)) ->save(); // create an administrator role $role = new P4Cms_Acl_Role; $role->setAdapter($adapter) ->setId(P4Cms_Acl_Role::ROLE_ADMINISTRATOR) ->setUsers(array($user)) ->save(); // create a member role $role = new P4Cms_Acl_Role; $role->setAdapter($adapter) ->setId(P4Cms_Acl_Role::ROLE_MEMBER) ->addOwner($user) ->setUsers(array($user)) ->save(); } /** * Remove all sites. */ public function removeSites() { return $this->saveSites(array()); } /** * Ensure that directories needed for testing exist. */ public function createTestDirectories() { // remove existing directories to start fresh w. each test. $this->removeTestDirectories(); extract($this->_p4Params); $directories = array( TEST_DATA_PATH, $serverRoot, $clientRoot, $clientRoot . '/superuser', $clientRoot . '/testuser', TEST_SESSION_SAVE_PATH, TEST_DATA_PATH . '/sites' ); foreach ($directories as $directory) { if (!is_dir($directory)) { mkdir($directory, 0777, true); } } } /** * Remove test directories (clean-up). */ public function removeTestDirectories() { // remove the entire data directory since it is test-specific $this->removeDirectory(TEST_DATA_PATH); } /** * Open up permissions (possibly recursively) on a directory. All files * in the directory (including the directory itself) will be given a * permission mask of 0777. This method checks that the owner of the * running PHP process owns each file before it attempts to change * permissions on it. * * @param string $directory the directory to change permissions on. * @param bool $recursive optional - whether to do so recursively. */ private function openPermissions($directory, $recursive = false) { $uid = getmyuid(); $files = new RecursiveDirectoryIterator($directory); foreach ($files as $file) { $stat = stat($file->getPathname()); if ($stat['uid'] != $uid) continue; // skip files we don't own if (!chmod($file->getPathname(), 0777)) { throw new Exception( "Can't set permissions on '" . $file->getPathname() . "'" ); } if ($file->isDir() && $recursive) { if ($files->isDot()) { continue; } $this->openPermissions($file->getPathname(), $recursive); } } chmod($directory, 0777); } /** * Setup the logger to direct to a file based on the class/method */ public function setUpLogger() { if (!is_dir(dirname($this->_logFile))) { mkdir(dirname($this->_logFile), 0777, true); } if (is_file($this->_logFile)) { chmod($this->_logFile, 0777); unlink($this->_logFile); } $writer = new Zend_Log_Writer_Stream($this->_logFile); $logger = new Zend_Log($writer); P4_Log::setLogger($logger); } /** * If the test passed, remove the log * * @param boolean $failed Indicates whether the recently completed test has failed. */ public function tearDownLogger($failed) { P4_Log::getLogger()->__destruct(); P4Cms_Log::getLogger()->__destruct(); // Disable teardown for testing return; // if we didn't fail, remove the log file if (!$failed && is_file($this->_logFile)) { chmod($this->_logFile, 0777); unlink($this->_logFile); } } /** * Add core modules to the include path. * Some tests depend on availability of core module facilities. */ public function initCoreModules() { P4Cms_Module::reset(); // tell p4cms module where to find core modules. P4Cms_Module::setCoreModulesPath(APPLICATION_PATH); // init. $modules = P4Cms_Module::fetchAllCore(); foreach ($modules as $module) { $module->init(); } } /** * Dump variable content into a string within test context. * * @param mixed $var variable to dump */ public function dumper($var) { ob_start(); var_dump($var); return ob_get_clean(); } /** * Perform application bootstrap. * * @param string|null $environment optional - The application environment to use * @param string|array|Zend_Config $options String path to bootstrap configuration file, * or array/Zend_Config of configuration options * @return Zend_Application the zend application instance we ran boostrap on */ public function doBootstrap($environment = null, $options = null) { $application = new P4Cms_Application( $environment ?: APPLICATION_ENV, $options ?: array('resources' => array('perforce' => $this->_p4Params)) ); $application->bootstrap(); // explicitly enable the stream wrapper so that when the tests // are run in a PHP with short tags disabled, the tests can // complete successfully. $view = Zend_Layout::getMvcInstance()->getView(); $view->setUseStreamWrapper(true); // re-introduce sites/all/modules because $site->load() // of the test site will remove it in favor of mock modules P4Cms_Module::addPackagesPath(MODULE_PATH); return $application; } /** * Simulate post of a file input field where no file has been selected. * * @param string $field The name for the file input form field. */ public function simulateEmptyFileInput($field) { if (!is_array($_FILES)) { $_FILES = array(); } $_FILES[$field] = array( 'name' => "", 'type' => null, 'tmp_name' => null, 'error' => UPLOAD_ERR_NO_FILE, 'size' => 0 ); } /** * Reset zend library components. */ public function resetZend() { Zend_Registry::_unsetInstance(); $front = Zend_Controller_Front::getInstance(); $front->resetInstance(); Zend_Layout::resetMvcInstance(); } /** * Reset p4 library components. */ public function resetP4() { // clear default connection if (class_exists('P4_Connection', false)) { P4_Connection::clearDefaultConnection(); } // clear out shutdown callbacks if (class_exists('P4_Environment', false)) { P4_Environment::setShutdownCallbacks(null); } // clear logger. if (class_exists('P4_Log', false)) { P4_Log::setLogger(null); } } /** * Reset p4cms library components. */ public function resetP4Cms() { // clear active user. if (class_exists('P4Cms_User', false)) { P4Cms_User::clearActive(); } // reset theme. if (class_exists('P4Cms_Theme', false)) { P4Cms_Theme::reset(); } // reset site component. if (class_exists('P4Cms_Site', false)) { P4Cms_Site::clearActive(); P4Cms_Site::setSitesPackagesPath(null); P4Cms_Site::setSitesDataPath(null); } // clear pub/sub provider. if (class_exists('P4Cms_PubSub', false)) { P4Cms_PubSub::setInstance(new P4Cms_PubSub_Provider); } // reset modules. if (class_exists('P4Cms_Module', false)) { P4Cms_Module::reset(); } // clear default adapter. if (class_exists('P4Cms_Record', false)) { P4Cms_Record::clearDefaultAdapter(); } // clear logger. if (class_exists('P4Cms_Log', false)) { if (P4Cms_Log::hasLogger()) { $logger = P4Cms_Log::getLogger(); $logger->__destruct(); } P4Cms_Log::setLogger(null); } // clear any registered form plugin paths. if (class_exists('P4Cms_Form', false)) { P4Cms_Form::clearPrefixPathRegistry(); } // clear cache of widget types if (class_exists('P4Cms_Widget_Type', false)) { P4Cms_Widget_Type::clearCache(); } // clear active ACL. if (class_exists('P4Cms_Acl', false)) { P4Cms_Acl::setActive(null); } // clear loader packages. if (class_exists('P4Cms_Loader', false)) { P4Cms_Loader::setPackagePaths(array()); } // clear static cache manager. if (class_exists('P4Cms_Loader', false)) { P4Cms_Cache::setManager(null); } } /** * Reset the application */ public function resetApplication() { // purge the search instance from search module; if present if (class_exists('Search_Module', false)) { if (Search_Module::hasSearchInstance()) { $proxy = Search_Module::factory(); $proxyReflection = new ReflectionObject($proxy); $proxyIndex = $proxyReflection->getProperty('_index'); $proxyIndex->setAccessible(true); $index = $proxyIndex->getValue($proxy); $index->__destruct(); Search_Module::clearSearchInstances(); } } // clear out auto-loader spl_autoload_unregister(array('P4Cms_Loader', 'autoload')); } /** * Perform setup of a library test. * * @param PHPUnit_Test $test An instance of a not-yet-run test. */ public function setUp($test) { $this->createTestDirectories(); $this->setUpLogger(); $test->p4 = $this->createP4Connection(); $this->p4 = $test->p4; Zend_Session::$_unitTestEnabled = true; } /** * Perform setup of a module test * * @param PHPUnit_Test $test An instance of a not-yet-run test. * @param string|null $environment optional - The application environment to use * @param string|array|Zend_Config $options String path to bootstrap configuration file, * or array/Zend_Config of configuration options */ public function setUpModuleTest($test, $environment = null, $options = null) { // add sites/all/modules to packages. P4Cms_Module::addPackagesPath( dirname(APPLICATION_PATH) . '/sites/all/modules' ); $this->createTestSites(); if ($test instanceof Zend_Test_PHPUnit_ControllerTestCase) { $utility = $this; $test->bootstrap = function() use ($utility, $environment, $options, $test) { $test->bootstrap = $utility->doBootstrap($environment, $options); }; } else { $this->doBootstrap($environment, $options); } } /** * Perform tear down of a library test * * @param PHPUnit_Test $test An instance of a just-completed test. */ public function tearDown($test) { // call p4 library shutdown functions // (closes all p4 connections, cleans up temp specs) if (class_exists('P4_Environment', false)) { P4_Environment::runShutdownCallbacks(); } // disconnect the P4 connection, if exists if (isset($test->p4)) $test->p4->disconnect(); $this->tearDownLogger($test->hasFailed()); $this->resetP4Cms(); $this->resetP4(); $this->resetZend(); // forces collection of any existing garbage cycles // so no open file handles prevent files/directories // from being removed. gc_collect_cycles(); $this->removeTestDirectories(); } /** * Perform tear down of a module test * * @param PHPUnit_Test $test An instance of a just-completed test. */ public function tearDownModuleTest($test) { $this->resetApplication(); } /** * Impersonate a logged-in user with the given role (e.g. member, * administrator, ...). Installs the default ACL and the named role. * * @param string $roleId the id of the role to act as. * @param P4Cms_Site $site the site to impersonate the role on * */ public function impersonate($roleId, P4Cms_Site $site = null) { $site = $site ?: P4Cms_Site::fetchActive(); $acl = $site->getAcl(); // if role is not anonymous (virtual) we need // to create it and authenticate a mock user if ($roleId != P4Cms_Acl_Role::ROLE_ANONYMOUS) { // create pretend user. $user = new P4Cms_User; $user->setId('mweiss') ->setFullName('Michael T. Weiss') ->setEmail('mweiss@thepretender.tv') ->setPersonalAdapter(P4Cms_Record::getDefaultAdapter()) ->save(); P4Cms_User::setActive($user); // assign user to named role. $role = new P4Cms_Acl_Role(); $role->setId($roleId) ->setUsers(array($user->getId())) ->save(); $auth = Zend_Auth::getInstance(); $auth->authenticate($user); // update acl roles. $acl->setRoles(P4Cms_Acl_Role::fetchAll()); } // update acl defaults now that named role exists. $acl->installDefaults(); } }