<?php /** * Parent class for all TestCases. * * @copyright 2012 Perforce Software. All rights reserved. * @license Please see LICENSE.txt in top-level folder of this distribution. * @version <release>/<patch> */ namespace P4Test; use P4; use P4\ClientPool\ClientPool; use P4\Connection\Connection; use P4\Connection\ConnectionInterface; use P4\Spec\Protections as P4Protections; use P4\Spec\User; class TestCase extends \PHPUnit_Framework_TestCase { const TEST_MAX_TRY_COUNT = 1000; public $p4; protected $p4Params = array(); protected $noP4dStdErr = false; /** * Setup test directories and a functioning perforce server. */ public function setUp() { // limit the amount of memory any given test can use to 2GB ini_set('memory_limit', '4G'); // get name of the testing class - replace slashes in class // name to avoid propagating them into directories' names $testClass = str_replace('\\', '_', get_class($this)); $testMethod = $this->getName(); // remove existing directories to start fresh w. each test. $this->removeDirectory(DATA_PATH); // replace any sketchy characters with - to prevent file creation issues $testSuffix = preg_replace('/[^\w-]/', '-', $testClass . '-' . $testMethod); // create directories needed for testing $serverRoot = DATA_PATH . '/server-' . $testSuffix; $clientRoot = DATA_PATH . '/clients-' . $testSuffix; $directories = array( DATA_PATH, $serverRoot, $clientRoot, $clientRoot . '/superuser', $clientRoot . '/testuser', ); foreach ($directories as $directory) { if (!is_dir($directory)) { mkdir($directory, 0777, true); } } // prepare connection params and create p4 connection $this->p4Params = array( 'serverRoot' => $serverRoot, 'clientRoot' => $clientRoot, 'port' => 'rsh:' . P4D_BINARY . ' -i -qr ' . $serverRoot . ' -J off ' . '-vtrack=0 -vserver.locks.dir=disabled', 'user' => 'tester', 'client' => 'test-client', 'group' => 'test-group', 'password' => 'testing123' ); // some tests can cause spurious output on stderr on mac // optionally wrap the rsh invocation and redirect stderr to /dev/null if ($this->noP4dStdErr) { $this->p4Params['port'] = 'rsh:bash -c "' . substr($this->p4Params['port'], 4) . ' 2> /dev/null"'; } $this->createP4Connection(); parent::setUp(); } /** * Clean up after ourselves. */ public function tearDown() { // call p4 library shutdown functions if (class_exists('P4\Environment\Environment', false)) { P4\Environment\Environment::runShutdownCallbacks(); } // disconnect the p4 connection, if exists if (isset($this->p4)) { $this->p4->disconnect(); } // clear default connection if (class_exists('P4\Connection\Connection', false)) { Connection::clearDefaultConnection(); } // clear out shutdown callbacks if (class_exists('P4\Environment\Environment', false)) { P4\Environment\Environment::setShutdownCallbacks(null); } // forces collection of any existing garbage cycles // so no open file handles prevent files/directories // from being removed. gc_collect_cycles(); // remove testing directory $this->removeDirectory(DATA_PATH); parent::tearDown(); // if phpunit wants to use a bunch of memory after a test runs (e.g. for code coverage) so be it ini_set('memory_limit', -1); } /** * Create a Perforce connection for testing. The perforce connection will * connect using a p4d started with the -i (run for inetd) flag. * * @param string|null $type allow caller to force the API * implementation. * @return P4\Connection\ConnectionInterface a Perforce API implementation */ public function createP4Connection($type = null) { extract($this->p4Params); if (!is_dir($serverRoot)) { throw new P4\Exception('Unable to create new server.'); } // create connection. $p4 = Connection::factory($port, $user, $client, $password, null, $type); // set server into Unicode mode if a charset was set (or set to something other than 'none') if (USE_UNICODE_P4D) { exec(P4D_BINARY . ' -xi -r ' . $serverRoot, $output, $status); if ($status != 0) { die("error (" . $status . "): problem setting server into Unicode mode:\n" . $output); } } // add noisy triggers if requested if (USE_NOISY_TRIGGERS) { $triggers = P4\Spec\Triggers::fetch($this->p4); // start with the unique triggers $script = "%quote%" . __DIR__ . "/assets/scripts/noisyTrigger.sh%quote%"; $lines = array( "noisy.change-submit change-submit //... \"$script change-submit\"", "noisy.change-content change-content //... \"$script change-content\"", "noisy.change-commit change-commit //... \"$script change-commit\"", "noisy.fix-add fix-add fix \"$script fix-add\"", "noisy.fix-delete fix-delete fix \"$script fix-delete\"", "noisy.shelve-submit shelve-submit //... \"$script shelve-submit\"", "noisy.shelve-commit shelve-commit //... \"$script shelve-commit\"", "noisy.shelve-delete shelve-delete //... \"$script shelve-delete\"" ); // put in in/out/save/commit/delete for various form types $forms = array( 'branch', 'change', 'client', 'depot', 'group', 'job', 'label', 'spec', 'stream', 'triggers', 'typemap', 'user' ); foreach ($forms as $form) { $lines[] = "noisy.$form-form-in form-in $form \"$script $form-form-in\""; $lines[] = "noisy.$form-form-out form-out $form \"$script $form-form-out\""; $lines[] = "noisy.$form-form-save form-save $form \"$script $form-form-save\""; $lines[] = "noisy.$form-form-commit form-commit $form \"$script $form-form-commit\""; $lines[] = "noisy.$form-form-delete form-delete $form \"$script $form-form-delete\""; } $triggers->setTriggers($lines)->save(); // force a reconnect as triggers seem to require it $triggers->getConnection()->disconnect(); } // give the connection a client manager $clients = new ClientPool($p4); $clients->setMax(10)->setRoot(DATA_PATH . '/clients')->setPrefix('test-'); $p4->setService('clients', $clients); // 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); $this->p4 = $p4; return $this->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)) { chmod($directory, 0777); $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() ); } } } } } } /** * 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; } /** * Helper method to create and connect as a user with limited access to depot. * This will modify protections table by adding lines to grant access for the specified user * to only those paths specified. Access defaults to 'list', but a specific mode can be given * for each path by specifying the path as the key and the mode as the value. * * @param string $user user to create * @param array $paths list of paths to grant user access to each path can be specified as: * ['path' => 'permission'] or ['path'] * @param ConnectionInterafce $p4Super optional - super user connection needed * to modify protections table * @return Connection connection for the new user */ public function connectWithAccess($user, array $paths, ConnectionInterface $p4Super = null) { $p4Super = $p4Super ?: $this->p4; // throw if user already exists if (User::exists($user, $this->p4)) { throw new \Exception("User already exists."); } // create user $model = new User($this->p4); $model->setId($user) ->setFullName("$user (limited access)") ->setEmail("$user@limited") ->save(); // add paths to the permissions table $protectionLines = array(); foreach ($paths as $path => $permission) { if ($path === (int) $path) { $path = $permission; $permission = 'list'; } $protectionLines[] = "$permission user $user * $path"; } $protections = P4Protections::fetch($p4Super); $protections->setProtections( array_merge( $protections->getProtections(), array("list user $user * -//..."), $protectionLines ) )->save(); // return connection for the new user return Connection::factory( $this->getP4Params('port'), $user, 'client-' . $user . '-test', '', null, null ); } /** * 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. */ protected function openPermissions($directory, $recursive = false) { $uid = getmyuid(); $files = new \RecursiveDirectoryIterator($directory); foreach ($files as $file) { $stat = stat($file->getPathname()); if ($stat['uid'] != $uid) { // skip files we don't own continue; } 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); } }