<?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);
}
}