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