<?php
/**
* Perforce Swarm
*
* @copyright 2014 Perforce Software. All rights reserved.
* @license Please see LICENSE.txt in top-level folder of this distribution.
* @version <release>/<patch>
*/
namespace BehatTests;
use P4\ClientPool\ClientPool;
use P4\Connection\Connection;
use P4\Uuid\Uuid;
use P4\Spec\Triggers;
use P4\Spec\Protections;
class P4Context extends AbstractContext
{
protected $superP4;
protected $adminP4;
protected $userP4;
protected $p4users = array (
'super' => array (
'User' => 'swarm-super',
'Email' => 'super@testhost',
'FullName' => 'Super User',
'Password' => 'testsuper'
),
'admin' => array(
'User' => 'swarm-admin',
'Email' => 'admin@testhost',
'FullName' => 'Admin User',
'Password' => 'testadmin'
),
'non-admin' => array(
'User' => 'non-admin',
'Email' => 'non-admin@testhost',
'FullName' => 'NonAdmin User',
'Password' => 'testnon-admin'
)
);
protected $p4Params = array();
protected $uuid;
protected $p4BaseDir;
protected $url;
protected $configParams = array();
public function __construct(array $parameters = null)
{
$this->configParams = $parameters;
}
/**
* @return array P4d Connection details for p4 super user
*/
public function getSuperUserConnection()
{
return $this->superP4;
}
/**
* @return array P4d Connection details for p4 admin user
*/
public function getAdminUserConnection()
{
return $this->adminP4;
}
/**
* @return array P4d Connection details for p4 non-admin user
*/
public function getNonAdminUserConnection()
{
return $this->userP4;
}
/**
* @return string Unique id generated for each test scenario
*/
public function getUUID()
{
return $this->uuid;
}
/**
* Return a valid P4 user
*
* @param $user string P4 user
* @return array Returns a valid P4 user if defined in $p4users
* @throws \Exception Thrown if $user is not a valid P4 user defined in $p4users
*/
public function getP4User($user)
{
if (!isset($this->p4users[$user])) {
throw new \Exception("Invalid P4 user: {$user}");
}
return $this->p4users[$user];
}
/**
* P4d & Swarm host setup method that gets runs before each Scenario (or Scenario Outline example)
* A minimum version of p4d can be specified when running a particular scenario
* All runs of that scenario with p4d version >= min version should pass successfully
*
* @param null $minVersion Minimum version of p4d binary against which the scenario run must pass
*
* @Given /^I setup p4d server connection$/
* @Given /^I setup p4d server connection with minimum version "(?P<version>[^"]*)"$/
*/
public function setUp($minVersion = null)
{
$this->uuid = new Uuid;
// prefix swarm host with 'pid' of executing test so that it can be easily identified with failure data
// The UUID is also used as the Swarm license token and hence it can only contain
// aplha-numeric characters and a hyphen
$this->uuid = getmypid() . '-' . $this->uuid;
$this->url = $this->configParams['base_url'];
// prints unique uuid, used to uniquely identify each Scenario, on console for better test debugging
$this->printDebug("UUID: " . $this->uuid);
// set up P4d connections and Swarm data path for executing Scenario
$this->p4BaseDir = $this->configParams['data_dir'] . '/' . $this->uuid;
$this->prepareP4Connections($minVersion);
$this->createP4Connections();
$this->setupTriggers();
// ensure we can cleanup properly when done.
exec('chmod -R a+wr ' . escapeshellarg($this->p4BaseDir));
// We identify the unique test run by its UUID passed in to apache through the cookie named 'SwarmDataPath'
$this->setSwarmCookie();
}
/**
* @param null $minVersion Minimum version of p4d binary against which the scenario run must pass
* @return string $p4d The location of p4d binary under
* @throws \Exception If p4d binary is not found under "BASE_PATH . '/tests/p4-bin"
*/
public function detectP4d($minVersion = null)
{
// capture the p4d version from the behat test run, specified in the config file
$version = $this->configParams['p4d_version'];
if (isset($minVersion)) {
if (floatval($version) < floatval($minVersion)) {
// If p4d run version less than min. version specified in scenario,
// then run that particular scenario at the minimum specified version
$version = $minVersion;
}
}
$p4d = $this->configParams['p4d_dir'] . '/p4d_r' . $version;
// Copy over p4d version if it does not exist in behat/p4d directory
// The correct OS is detected for the P4D binary
if (!is_file($p4d)) {
if (preg_match('/Darwin/i', PHP_OS)) {
// 64-bit MacOSX version of p4d
$file = BASE_PATH . '/tests/p4-bin/bin.darwin90x86_64/p4d_r' . $version;
} else {
// 64-bit linux version of p4d
$file = BASE_PATH . '/tests/p4-bin/bin.linux26x86_64/p4d_r' . $version;
}
if (!is_file($file)) {
throw new \Exception("p4d binary \"$file\" does not exist");
}
copy($file, $p4d);
// Ensure everyone has execute permissions. Setting uid is necessary here because without it
// tests which attempt to access the server's db files fail with a CommandException as permissions
// are denied.
chmod($p4d, 04111);
}
return $p4d;
}
/**
* Sets up paths and basic depot infrastructure.
*
* @param null $minVersion Minimum version of p4d binary against which the scenario run must pass
* @throws \Exception Throws exception if starting config file is not found.
*/
public function prepareP4Connections($minVersion)
{
$p4d = $this->detectP4d($minVersion);
// ensure failures directory has been created for our p4d log
@mkdir($this->configParams['failures_dir'] . '/' . $this->uuid, 04777);
// Prepare connection params and create p4 connection
// The p4d instance will run against the 'rsh' port and will have its log stored under
// "failures/<UUID>" directory. This log will be persisted only if scenario fails
$this->p4Params = array(
'baseDir' => $this->p4BaseDir,
'serverRoot' => $this->p4BaseDir . '/server',
'clientRootAdmin' => $this->p4BaseDir . '/clientAdmin',
'clientRootSuper' => $this->p4BaseDir . '/clientSuper',
'clientsRoot' => $this->p4BaseDir . '/clients',
'port' => 'rsh:' . $p4d . ' -iqr ' . $this->p4BaseDir . '/server'
. ' -J off '
. '-vtrack=0 -vserver.locks.dir=disabled '
. ' -L ' . $this->configParams['failures_dir'] . '/' .$this->uuid . '/p4d_log',
'client' => 'test-client',
'group' => 'test-group',
'mail' => $this->p4BaseDir . '/mail'
);
$directories = array(
$this->p4BaseDir,
$this->p4Params['serverRoot'],
$this->p4Params['clientRootAdmin'],
$this->p4Params['clientRootSuper'],
$this->p4Params['clientsRoot'],
$this->p4Params['mail'],
);
// Create directories and assign permissions
// umask() is needed to ensure that created directory gets 'w' permission for group and other users
$old_mask = umask(0);
foreach ($directories as $directory) {
if (!is_dir($directory)) {
if ($directory == $this->p4Params['baseDir']) {
mkdir($directory, 0777, true);
} else {
// For any sub-directory created under $p4BaseDir (e.g. server/client/clients), set Uid
// This way, the user should own all files that get created within those directories
mkdir($directory, 0777, true);
chmod($directory, 04777);
}
}
}
umask($old_mask);
// creating the data/config.php file for swarm admin user
$this->generateSwarmConfig();
}
/*
* Function needed to setup the data/config.php file for the test. It can also
* read in a custom config array and replace the default config.php with the custom one
*
* @param array|null $config Array containing values needed to setup the config.php file
*/
protected function generateSwarmConfig($config = null)
{
if (is_file($this->p4BaseDir . '/config.php')) {
// delete the default config.php created for the test
unlink($this->p4BaseDir . '/config.php');
}
// defining the default test config.php array
$defaultConfig = array(
'avatars' => array(
'http_url' => false,
'https_url' => false
),
'environment' => array(
'hostname' => $this->configParams['base_url'],
),
'p4' => array(
'port' => $this->p4Params['port'],
'user' => $this->p4users['admin']['User'],
'password' => $this->p4users['admin']['Password'],
),
// default Swarm log levels set to 5
// Can be set to a max of '7' if a greater debugging need arises
'log' => array(
'priority' => 5
),
// path to mail dir
'mail' => array(
'transport' => array(
'path' => $this->p4Params['mail']
)
)
);
// Construct the merged array needed for config.php if additional Swarm config values are passed in
// If keys are same, values from the passed in config array would trump the default config
if (isset($config)) {
$defaultConfig = array_merge($defaultConfig, $config);
}
// Create the config.php file based on above defined array
file_put_contents($this->p4BaseDir . '/config.php', "<?php\nreturn ");
file_put_contents($this->p4BaseDir . '/config.php', var_export($defaultConfig, true) . ';', FILE_APPEND);
}
/**
* The apache web user creates files/dirs under data that the user running
* the test cannot remove without 'sudo' (e.g. log/cache/sessions)
* Therefore, the tearDown process creates a script accessible to the web
* user which contains information on the files and paths that the web user
* has created and instructions to remove them, then then calls the script
* via a web request, then removes the script.
* This is done in the tearDown method so this script is not present for
* the duration of the test, as incorrect invocation could lead to unstable
* test results.
*
* IMP: To persist the data directory for debugging purposes, COMMENT OUT the
* '@AfterScenario' tag on the line below, else it will be removed after each scenario
*
* @AfterScenario
*/
public function tearDown($event)
{
// 1. Setting the p4d log under failures dir with 'set uid' bit,so that user can own it
// this would allow the user owned php script to delete this log file
$file = $this->configParams['failures_dir'] . '/' . $this->uuid . '/p4d_log';
chmod($file, 04707);
// 2. Deleting the directory containing p4d log under failures dir, if there is no failure
// in the scenario (e.g. failures/<UUID> where UUID=86227_30142599-48af-3290-608b-8589c14e6ce4)
// This ensures that failures dir. will contain sub-dirs for failing scenarios only
if (FeatureMinkContext::$stepFailed == false) {
@unlink($file);
@rmdir($this->configParams['failures_dir'] . '/' . $this->uuid);
}
// 3. Run the cleanup script to delete the data directory for the particular scenario
// (e.g. data/<UUID> where UUID=86227_30142599-48af-3290-608b-8589c14e6ce4)
// The cleanup script is stored under 'public' dir. and gets run by the web server user
if (is_dir($this->p4Params['baseDir'])) {
$cleanupScript = 'cleanup-' . $this->uuid . '.php';
file_put_contents(
BASE_PATH . '/public/' . $cleanupScript,
"<?php\n@exec('rm -rf " . $this->p4Params['baseDir'] . "');\n"
);
file_get_contents($this->url . '/' . $cleanupScript);
// unlink file stored under 'public directory'
unlink(BASE_PATH . '/public/' . $cleanupScript);
// If data directory still exists after it is cleaned up by Web service user, then let the script
// user clean it up ( for files and dirs where web user does not have delete permissions)
if (is_dir($this->p4Params['baseDir'])) {
$this->removeDirectory($this->p4Params['baseDir']);
}
}
}
/**
* Create a Perforce connection for testing. The perforce connection will
* connect using a p4d started with the -i (run for inetd) flag.
*
* We create a super, admin and non-admin user connection.
*
* Also, sets the admin connection as the default connection and $this->p4
* ensuring the bulk of our work is done with those permissions (to more
* accurately mirror our suggested deployment configuration).
*
* @param string|null $type allow caller to force the API implementation.
* @throws \Exception
*/
public function createP4Connections($type = null)
{
$clientsRoot = $this->p4Params['clientsRoot'];
$clientRootAdmin = $this->p4Params['clientRootAdmin'];
$clientRootSuper = $this->p4Params['clientRootSuper'];
$serverRoot = $this->p4Params['serverRoot'];
$port = $this->p4Params['port'];
$client = $this->p4Params['client'];
if (!is_dir($serverRoot)) {
throw new \Exception('Unable to create new server.');
}
// create super user connection.
$p4 = Connection::factory(
$port,
$this->p4users['super']['User'],
$client,
$this->p4users['super']['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);
}
}
// give the connection a client manager
$clients = new ClientPool($p4);
$clients->setMax(10)->setRoot($clientsRoot)->setPrefix(getmypid() . '-');
$p4->setService('clients', $clients);
// create P4 super user.
$p4->run('user', '-i', $this->p4users['super']);
$p4->run('login', array(), $this->p4users['super']['Password']);
// establish protections.
// A newly instantiated P4 server considers the first user to invoke
// 'p4 protects' to be a superuser. The below operations make only the
// this 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' => $this->p4users['super']['User'],
'Root' => $clientRootSuper,
'View' => array('//depot/... //' . $client . '/...')
);
$p4->run('client', '-i', $clientForm);
// pull out the parent created super connection for later use
$superP4 = $this->superP4 = $p4;
// create the admin user and store its connection
$adminP4 = $this->p4 = Connection::factory(
$port,
$this->p4users['admin']['User'],
null,
$this->p4users['admin']['Password']
);
$adminP4->run('user', '-i', $this->p4users['admin']);
// add 'admin' protections for our new admin user
$protections = Protections::fetch($this->superP4);
$protections->addProtection('admin', 'user', $this->p4users['admin']['User'], '*', '//...');
$protections->save();
// clear the client from the super p4 and set a client for the admin user
$superP4->setClient(null);
$clientForm = array(
'Client' => $client,
'Owner' => $this->p4users['admin']['User'],
'Root' => $clientRootAdmin,
'View' => array('//depot/... //' . $client . '/...')
);
$superP4->run('client', array('-d', $client));
$adminP4->run('client', '-i', $clientForm);
$adminP4->setClient($client);
Connection::setDefaultConnection($adminP4);
// lastly create the non-admin standard user account
$userP4 = Connection::factory(
$port,
$this->p4users['non-admin']['User'],
null,
''
);
// actually create the regular user
$userP4->run('user', '-i', $this->p4users['non-admin']);
$this->userP4 = $userP4;
$this->adminP4 = $adminP4;
}
/**
* Sets trigger token by creating file in data/queue/tokens/
* Copies default script to /public folder and alters it to include
* the token.
* Sets up the perforce triggers using p4php, referencing the tests's
* trigger script.
*
* @throws \Exception Throws Exception if the default trigger script
* for these tests was not found in the base path.
*/
public function setupTriggers()
{
// add swarm triggers using the superuser's p4 connection
// 0-length file, name is token
mkdir($this->p4BaseDir . '/queue');
mkdir($this->p4BaseDir . '/queue/tokens');
// Creating the Swarm token file with a Swarm license value same as UUID of the test
file_put_contents($this->p4BaseDir . '/queue/tokens/'. $this->uuid, null);
// Copy and update trigger script to contain reference to this test's token.
$script = file_get_contents(BASE_PATH . '/p4-bin/scripts/swarm-trigger.sh');
if ($script === false) {
throw new \Exception("Could not read default Swarm trigger script.");
}
// Modifying the default trigger script to set the Swarm Host and Token
// We also modify the wget and curl commands so that the trigger script is made aware of the unique
// Swarm Data Path for each scenario ( created under the behat/data directory)
// This is set through the 'SwarmDataPath' cookie generated by the test scenario
$scriptArray = explode("\n", $script);
$setHost = false;
$setToken = false;
$setWget = false;
$setCurl = false;
foreach ($scriptArray as $index => $line) {
if (substr(trim($line), 0, 10) == 'SWARM_HOST') {
$scriptArray[$index] = "SWARM_HOST=\"$this->url\"";
$setHost = true;
} elseif (substr(trim($line), 0, 11) == 'SWARM_TOKEN') {
$scriptArray[$index] = "SWARM_TOKEN=\"$this->uuid\"";
$setToken = true;
} elseif (substr(trim($line), 0, 12) == 'wget --quiet') {
$command = $scriptArray[$index];
$command = preg_replace(
'/wget --quiet/',
'wget --quiet --header="Cookie: SwarmDataPath=${SWARM_TOKEN}"',
$command
);
$scriptArray[$index] = $command;
$setWget = true;
} elseif (substr(trim($line), 0, 13) == 'curl --silent') {
$command = $scriptArray[$index];
$command = preg_replace(
'/curl --silent/',
'curl --silent --cookie "SwarmDataPath=${SWARM_TOKEN}"',
$command
);
$scriptArray[$index] = $command;
$setCurl = true;
}
if ($setHost && $setToken && $setWget && $setCurl) {
break; // exit foreach loop
}
}
$triggerScript = $this->p4BaseDir . '/script-triggers.sh';
file_put_contents($triggerScript, implode("\n", $scriptArray));
exec('chmod a+x ' . $triggerScript);
// Executing the trigger script to get the list of default Swarm triggers
// Testing with any additional triggers would require the triggers to be defined in the test setup
exec("$triggerScript -o", $triggers);
$result = array_map('trim', $triggers);
// set triggers in perforce
$triggers = Triggers::fetch($this->superP4);
$triggers->setTriggers($result)->save();
// force a reconnect as triggers seem to require it
$triggers->getConnection()->disconnect();
}
/**
* Function to instantiate workers to process activities within the queue
*/
public function instantiateWorker()
{
$uuid = $this->getP4Context()->getUUID();
// Create header context with the SwarmDataPath cookie
$opts = array(
'http' => array(
'method' => "GET",
'header' => "Cookie: SwarmDataPath=$uuid\r\n"
)
);
$context = stream_context_create($opts);
// Instantiating & retiring a worker to process the tasks in swarm queue
file_get_contents($this->configParams['base_url'] . '/queue/worker?retire=1', false, $context);
file_get_contents($this->configParams['base_url'] . '/queue/worker?retire=2', false, $context);
file_get_contents($this->configParams['base_url'] . '/queue/worker?retire=3', false, $context);
}
/**
* Use the super-user connection to create a non-admin user with the given username
*
* @param string $username
*/
public function createRegularUser($username)
{
$userConfiguration = array(
"User" => "$username",
"Email" => "$username@testhost",
"FullName" => "$username",
"Password" => "$username"
);
$superP4 = $this->superP4;
$superP4->run('user', array('-f', '-i'), $userConfiguration);
}
}