- <?php
- /**
- * Test the bash trigger script.
- *
- * @copyright 2015 Perforce Software. All rights reserved.
- * @license Please see LICENSE.txt in top-level folder of this distribution.
- * @version <release>/<patch>
- */
-
- namespace ModuleTest;
-
- use P4\File\File;
- use P4\Spec\Change;
- use P4\Spec\Group;
- use P4\Spec\Job;
- use P4\Spec\Triggers;
- use P4\Spec\User;
- use P4\Uuid\Uuid;
- use Reviews\Model\Review;
-
- class BashTriggerTest extends TestControllerCase
- {
- protected $scriptBasename = 'swarm-trigger.sh';
- protected $scriptConfigFile = null;
-
- /**
- * Extends parent by creating p4 config file and a config to be used by the trigger script.
- */
- public function setUp()
- {
- // our trigger tests cause unwanted output on p4d's stderr which
- // on some platforms (e.g. mac) surfaces to the console, silence it!
- $this->noP4dStdErr = true;
-
- // run parent to set up perforce connection and prepare directories
- parent::setUp();
-
- // reset external triggers config file
- $this->configureScript();
- }
-
- public function testUsage()
- {
- // no arguments should produce usage
- exec($this->getScriptPath() . ' 2>&1', $output);
- $this->assertSame($this->getExpectedUsage(), implode("\n", $output));
- unset($output);
-
- // -t without -v should produce usage
- // first line should indicate error
- exec($this->getScriptPath() . ' -t commit 2>&1', $output);
- $first = array_shift($output);
- $this->assertRegExp("/no (ID )?value supplied/i", $first);
- $this->assertSame($this->getExpectedUsage(), implode("\n", $output));
- unset($output);
-
- // -v without -t should produce usage
- // first line should indicate error
- exec($this->getScriptPath() . ' -v 54321 2>&1', $output);
- $first = array_shift($output);
- $this->assertRegExp("/no event type supplied/i", $first);
- $this->assertSame($this->getExpectedUsage(), implode("\n", $output));
- unset($output);
-
- // -h should produce usage output
- exec($this->getScriptPath() . ' -h 2>&1', $output);
- $this->assertSame($this->getExpectedUsage(true), implode("\n", $output));
- unset($output);
- }
-
- /**
- * Startup a local web server and verify trigger pings it
- */
- public function testTaskQueuing()
- {
- $this->markTestSkipped();
- // built-in web-server requires php 5.4+ and posix_kill
- if (version_compare(PHP_VERSION, '5.4', '<') || !function_exists('posix_kill')) {
- $this->markTestSkipped('Requires PHP 5.4+ and posix_kill()');
- }
-
- // ensure we have a port between 1024 and 65535
- $port = (getmypid() % 60000) + 1024;
- $token = strtoupper(new Uuid);
- $this->configureScript(
- array(
- 'SWARM_HOST' => '"http://localhost:' . $port . '"',
- 'SWARM_TOKEN' => '"' . $token . '"'
- )
- );
-
- $this->installTriggers(
- array(
- array('job', 'job', array()),
- array('user', 'user', array()),
- array('userdel', 'user', array()),
- array('group', 'group', array()),
- array('groupdel', 'group', array()),
- array('changesave', 'change', array()),
- array('shelve', '//...', array()),
- array('commit', '//...', array())
- )
- );
-
- // launch a web-server to monitor for requests from the trigger script
- $logFile = DATA_PATH . '/web-server.log';
- $pid = $this->startWebServer($port, $logFile);
-
- try {
- // test job event
- $this->p4->disconnect(); // need to disconnect here or trigger won't fire
- $job = new Job;
- $job->setDescription('test')->save();
- $job = Job::fetch('job000001');
-
- $this->assertSame("test\n", $job->getDescription());
- $this->assertTaskQueued($logFile, $token);
-
- // test user add event
- $user = new User;
- $user->setId('jdoe')->setEmail('jdoe@host.com')->setFullName('J. Doe')->save();
- $this->assertTrue(User::exists('jdoe'));
- $this->assertTaskQueued($logFile, $token);
-
- // test user delete event
- $this->superP4->disconnect(); // need to disconnect here or trigger won't fire
- User::fetch('jdoe', $this->superP4)->delete();
- $this->assertFalse(User::exists('jdoe'));
- $this->assertTaskQueued($logFile, $token);
-
- // test group add event
- $group = new Group($this->superP4);
- $group->setId('trigger')->setOwners(array('tester'))->save();
- $this->assertTrue(Group::exists('trigger'));
- $this->assertTaskQueued($logFile, $token);
-
- // test group delete event
- Group::fetch('trigger', $this->superP4)->delete();
- $this->assertFalse(Group::exists('trigger'));
- $this->assertTaskQueued($logFile, $token);
-
- // test change save event
- $change = new Change;
- $change->setDescription('test')->save();
- $this->assertSame(1, $change->getId());
- $this->assertTaskQueued($logFile, $token);
-
- // test shelve commit event
- $file = new File;
- $file->setFilespec('//depot/foo')->setLocalContents('test')->add(1);
- $this->p4->run('shelve', array('-c', '1', '//...'));
- $result = $this->p4->run('fstat', array('-Rs', '-e', '1', '//...'));
- $this->assertSame('//depot/foo', $result->getData(0, 'depotFile'));
- $this->assertTaskQueued($logFile, $token);
-
- // test commit event
- $file = new File;
- $file->setFilespec('//depot/bar')->setLocalContents('test')->add()->submit('test');
- $result = $this->p4->run('files', array('//...'));
- $this->assertSame('//depot/bar', $result->getData(0, 'depotFile'));
- $this->assertTaskQueued($logFile, $token);
- } catch (\Exception $e) {
- }
-
- // now kill the web-server
- posix_kill($pid, 15);
-
- if (isset($e)) {
- throw $e;
- }
- }
-
- /**
- * Verify that config files are sourced as expected.
- * We will test it on local copy of the original trigger script with modified paths to config
- * scripts. We set different SWARM_TOKEN values in these scripts, fire triggers and check the
- * token value that was used to add a task to the queue (we will need a web-server for this test).
- */
- public function testConfigSource()
- {
- // built-in web-server requires php 5.4+ and posix_kill
- if (version_compare(PHP_VERSION, '5.4', '<') || !function_exists('posix_kill')) {
- $this->markTestSkipped('Requires PHP 5.4+ and posix_kill()');
- }
-
- // prepare a dir where we put our custom config files for testing
- $configDir = DATA_PATH . '/config-scripts';
- $triggerScriptPath = $configDir . '/' . $this->scriptBasename;
- mkdir($configDir, 0777);
-
- // to test config sourcing, we don't want to place testing config scripts in the
- // default locations defined in the script; instead, we create a local copy of
- // the script and modify it to define paths pointing to locations of our choice and
- // then set this script to be used by triggers
- $candiateConfigPaths = array(
- $configDir . '/1.conf',
- $configDir . '/2.conf'
- );
- $this->adjustTriggerScript(
- $triggerScriptPath,
- array_map(
- function ($path) {
- return ' "' . $path . '" \\';
- },
- $candiateConfigPaths
- ),
- array(
- 150 => 'source_config',
- 151 => '{',
- 155 => ' for file in',
- 158 => ' "$MYDIR/swarm-trigger.conf"'
- ),
- array(156, 157),
- 155
- );
-
- // ensure we have a port between 1024 and 65535
- $port = (getmypid() % 60000) + 1024;
- $config = array(
- 'SWARM_HOST' => '"http://localhost:' . $port . '"'
- );
- $this->configureScript($config);
- $this->installTriggers(
- array(
- array('job', 'job', array())
- ),
- $triggerScriptPath
- );
-
- // launch a web-server to monitor for requests from the trigger script
- $logFile = DATA_PATH . '/web-server.log';
- $pid = $this->startWebServer($port, $logFile);
-
- try {
- // test case #1: just a config passed to trigger via -c
- $token = strtoupper(new Uuid);
- $this->configureScript($config + array('SWARM_TOKEN' => $token));
-
- $this->p4->disconnect(); // need to disconnect here or trigger won't fire
- $job = new Job;
- $job->setDescription('test 1')->save();
- $this->assertTaskQueued($logFile, $token);
-
- // test case #2: 3 config files, ensure the last wins
- $token1 = strtoupper(new Uuid);
- $token2 = strtoupper(new Uuid);
- $token3 = strtoupper(new Uuid);
- $this->configureScript($config + array('SWARM_TOKEN' => $token3));
- file_put_contents($candiateConfigPaths[0], 'SWARM_TOKEN="' . $token1 . '"');
- file_put_contents($candiateConfigPaths[1], 'SWARM_TOKEN="' . $token2 . '"');
-
- $this->p4->disconnect(); // need to disconnect here or trigger won't fire
- $job = new Job;
- $job->setDescription('test 2')->save();
- $this->assertTaskQueued($logFile, $token3);
-
- // test case #3: 3 config files, but only one contains a token
- $token1 = strtoupper(new Uuid);
- $token2 = strtoupper(new Uuid);
- $this->configureScript($config + array('FOO' => 'bar'));
- file_put_contents($candiateConfigPaths[0], 'SWARM_TOKEN="' . $token1 . '"' . PHP_EOL);
- file_put_contents($candiateConfigPaths[1], 'NO_TOKEN="' . $token2 . '"' . PHP_EOL);
-
- $this->p4->disconnect(); // need to disconnect here or trigger won't fire
- $job = new Job;
- $job->setDescription('test 3')->save();
- $this->assertTaskQueued($logFile, $token1);
-
- // test case #4: no config passed via -c
- $triggers = Triggers::fetch($this->superP4);
- $triggerJobCommand = '%quote%' . $triggerScriptPath . '%quote% -t job -v %formname%';
- $triggers->setTriggers(
- array(
- array(
- 'name' => 'job',
- 'type' => 'form-commit',
- 'path' => 'job',
- 'command' => $triggerJobCommand
- )
- )
- )->save();
-
- $this->configureScript($config + array('SWARM_TOKEN' => $token));
- file_put_contents($candiateConfigPaths[1], file_get_contents($this->scriptConfigFile));
-
- $this->p4->disconnect(); // need to disconnect here or trigger won't fire
- $job = new Job;
- $job->setDescription('test 4')->save();
- $this->assertTaskQueued($logFile, $token);
-
- // test #5: config passed via -c but as a blank file
- $triggerJobCommand = '%quote%' . $triggerScriptPath . '%quote% -c %quote%%quote% -t job -v %formname%';
- $triggers->setTriggers(
- array(
- array(
- 'name' => 'job',
- 'type' => 'form-commit',
- 'path' => 'job',
- 'command' => $triggerJobCommand
- )
- )
- )->save();
-
- $this->configureScript($config + array('SWARM_TOKEN' => $token1));
- file_put_contents($candiateConfigPaths[1], file_get_contents($this->scriptConfigFile));
-
- $this->p4->disconnect(); // need to disconnect here or trigger won't fire
- $job = new Job;
- $job->setDescription('test 5')->save();
- $this->assertTaskQueued($logFile, $token1);
- } catch (\Exception $e) {
- }
-
- // now kill the web-server
- posix_kill($pid, 15);
-
- if (isset($e)) {
- throw $e;
- }
- }
-
- /**
- * @dataProvider triggerTestProvider
- */
- public function testStrictAndEnforceTriggers(
- array $triggers,
- array $changeData,
- $expectedError = null,
- $preRunCallback = null
- ) {
- // configure trigger script
- $this->configureScript();
-
- // install triggers
- $this->installTriggers($triggers);
-
- // create a change to test on
- $change = new Change($this->p4);
- $change->setDescription(
- isset($changeData['description'])
- ? $changeData['description']
- : 'testing change'
- );
-
- // invoke pre-run callback if set
- if ($preRunCallback && is_callable($preRunCallback)) {
- $preRunCallback($this->superP4, $change);
- }
-
- // add specified files to the change
- foreach ((array) $changeData['files'] as $fileData) {
- $fileSpec = isset($fileData[0]) ? $fileData[0] : null;
- $content = isset($fileData[1]) ? $fileData[1] : 'file content';
-
- $file = new File($this->p4);
- $file->setFilespec($fileSpec)->open()->setLocalContents($content);
-
- // change file type if set
- if (isset($fileData[2])) {
- $file->reopen(null, $fileData[2]);
- }
-
- $change->addFile($file);
- }
-
- // shelve the change to mimic how reviews are created by end user
- // this will also put files in Perforce (needed when trigger script
- // compares file contents by running fstat)
- $change->save();
- $this->p4->run('shelve', array('-c', $change->getId()));
-
- // create review from change if requested by the test
- $review = null;
- if (isset($changeData['createReview']) && $changeData['createReview']) {
- $review = Review::createFromChange($change, $this->p4)
- ->save()
- ->updateFromChange($change)
- ->save();
- }
-
- // execute on-before-submit callback if specified
- if (isset($changeData['onBeforeSubmit']) && is_callable($changeData['onBeforeSubmit'])) {
- call_user_func_array(
- $changeData['onBeforeSubmit'],
- array($change, $review)
- );
- }
-
- // submit the change (delete shelved files first)
- $this->p4->run('shelve', array('-d', '-c', $change->getId()));
- $exceptionCaught = null;
- try {
- $change->submit();
- } catch (\Exception $exceptionCaught) {
- }
-
- // verify expected error
- if (is_array($expectedError)) {
- // we were expecting an error, first verify that we indeed caught some exception
- $this->assertTrue($exceptionCaught instanceof \Exception);
-
- // verify exception class if specified
- if (isset($expectedError['exceptionClass'])) {
- $this->assertSame($expectedError['exceptionClass'], get_class($exceptionCaught));
- }
-
- // verify error message if specified
- if (isset($expectedError['message'])) {
- $this->assertTrue(
- strpos($exceptionCaught->getMessage(), $expectedError['message']) !== false,
- 'Unexpected error message: ' . $exceptionCaught->getMessage()
- );
- }
- } else {
- // no error was expected, verify
- $this->assertTrue(
- is_null($exceptionCaught),
- $exceptionCaught
- ? "Unexpected exception: " . $exceptionCaught->getMessage()
- : ''
- );
- }
- }
-
- /**
- * @dataProvider strictTriggerWithKtextFilesProvider
- */
- public function testStrictTriggerWithKtextFiles()
- {
- $this->markTestSkipped(
- 'Ktext files handling is not implemented in bash trigger script.'
- );
- }
-
- /**
- * Create test suite for testing strict and enforce triggers.
- *
- * @return array
- */
- public function triggerTestProvider()
- {
- $changeData1 = array(
- 'description' => 'test',
- 'files' => array(
- array('//depot/foo', 'abc'),
- array('//depot/a/foo', '123'),
- array('//depot/a/b/foo', 'klm'),
- )
- );
-
- $changeData2 = array(
- 'description' => 'test with ktext',
- 'files' => array(
- array('//depot/foo', 'abc', 'ktext'),
- array('//depot/a/foo', '123', 'ktext'),
- array('//depot/a/b/foo', 'klm'),
- )
- );
-
- // build test data array
- $tests = array();
- foreach (array($changeData1, $changeData2) as $changeData) {
- // add tests for change not under review (should reject)
- $expectedError = array(
- 'exceptionClass' => 'P4\Connection\Exception\CommandException',
- 'message' => 'Cannot find a Swarm review associated with this change'
- );
- foreach ($this->getTriggers('//depot/a/...') as $triggers) {
- foreach ($this->getVariants($changeData) as $variant) {
- $tests[] = array(
- $triggers,
- $variant + array('createReview' => false),
- $expectedError
- );
- }
- }
-
- // add tests for change not under review but with -r flag set on trigger (should accept)
- foreach ($this->getTriggers('//depot/a/...', null, array('-r')) as $triggers) {
- foreach ($this->getVariants($changeData) as $variant) {
- $tests[] = array(
- $triggers,
- $variant + array('createReview' => false),
- null
- );
- }
- }
-
- // add tests for change under un-approved review (should reject)
- $expectedError = array(
- 'exceptionClass' => 'P4\Connection\Exception\CommandException',
- 'message' => 'Swarm review 2 for this change (1) is not approved'
- );
- foreach ($this->getTriggers('//depot/a/...') as $triggers) {
- foreach ($this->getVariants($changeData) as $variant) {
- $tests[] = array(
- $triggers,
- $variant + array('createReview' => true),
- $expectedError
- );
- }
- }
-
- // add tests for change under approved review (should accept if unmodified files)
- foreach ($this->getTriggers('//depot/a/...') as $triggers) {
- $tests[] = array(
- $triggers,
- $changeData + array(
- 'createReview' => true,
- 'onBeforeSubmit' => function ($change, $review) {
- $review->setState(Review::STATE_APPROVED)->save();
- }
- ),
- null
- );
- }
-
- // add tests for change under approved review with modified files (should accept for enforce)
- foreach ($this->getVariants($changeData) as $variant) {
- $tests[] = array(
- current($this->getTriggers('//depot/a/...', array('enforce'))),
- $changeData + array(
- 'createReview' => true,
- 'onBeforeSubmit' => function ($change, $review) {
- $review->setState(Review::STATE_APPROVED)->save();
- $change->getFileObjects()->first()->setLocalContents('xyz');
- }
- ),
- null
- );
- }
-
- // add tests for change under approved review with modified files (should reject for strict)
- $expectedError = array(
- 'exceptionClass' => 'P4\Connection\Exception\CommandException',
- 'message' => 'The content of this change (1) does not match the content'
- . ' of the associated Swarm review (2)'
- );
- $variants = $this->getVariants(
- $changeData,
- function ($change, $review) {
- $review->setState(Review::STATE_APPROVED)->save();
- }
- );
- unset($variants['identical']);
- foreach ($this->getTriggers('//depot/a/...', array('strict', 'both')) as $triggers) {
- foreach ($variants as $variant) {
- $tests[] = array(
- $triggers,
- $variant + array('createReview' => true),
- $expectedError
- );
- }
- }
- }
-
- // add special case for '-g <GROUP>' flag (if user is a member of a GROUP then triggers are bypassed)
- // test both cases when change user is/is-not a member of a GROUP
- foreach ($this->getTriggers('//depot/a/...', null, array('-g foo')) as $triggers) {
- foreach ($this->getVariants($changeData) as $variant) {
- $tests[] = array(
- $triggers,
- $variant + array('createReview' => false),
- array(
- 'exceptionClass' => 'P4\Connection\Exception\CommandException',
- 'message' => 'Cannot find a Swarm review associated with this change'
- ),
- function ($p4, Change $change) {
- // create 'foo' group and put the change user as member
- $group = new Group($p4);
- $group->setId('foo')->addUser('foo')->save();
- }
- );
- $tests[] = array(
- $triggers,
- $variant + array('createReview' => false),
- null,
- function ($p4, Change $change) {
- // create 'foo' group and put the change user as member
- $group = new Group($p4);
- $group->setId('foo')->addUser($change->getUser())->save();
- }
- );
- }
- }
-
- return $tests;
- }
-
- /**
- * Data provider for testing strict trigger with ktext files handling
- * Returns array with following keys:
- * review_files - list of files (keyed with filespec) to create a review with
- * value is an array with 'type' (file type) and 'content' (local content)
- * submit_files - list of files (specified in a same fashion as review_files) to submit
- * files will be submitted under the review created with review_files
- * should_fail - boolean flag, true if the trigger is expected to prevent committing files
- * or false if trigger is expected to do nothing
- */
- public function strictTriggerWithKtextFilesProvider()
- {
- return array(
- 'ktext-no-keywords-unchanged' => array(
- 'review_files' => array(
- '//depot/foo' => array(
- 'type' => 'ktext',
- 'content' => 'test no keywords'
- ),
- '//depot/bar' => array(
- 'type' => 'text',
- 'content' => 'foo $Id$'
- ),
- ),
- 'submit_files' => array(
- '//depot/foo' => array(
- 'type' => 'ktext',
- 'content' => 'test no keywords'
- ),
- '//depot/bar' => array(
- 'type' => 'text',
- 'content' => 'foo $Id$'
- ),
- ),
- 'should_fail' => false
- ),
- 'ktext-no-keywords-changed' => array(
- 'review_files' => array(
- '//depot/foo' => array(
- 'type' => 'text',
- 'content' => 'abc'
- ),
- '//depot/bar' => array(
- 'type' => 'text+k',
- 'content' => '1 2 3'
- ),
- '//depot/baz' => array(
- 'type' => 'ktext',
- 'content' => 'no keywords'
- ),
- ),
- 'submit_files' => array(
- '//depot/foo' => array(
- 'type' => 'text',
- 'content' => 'abc'
- ),
- '//depot/bar' => array(
- 'type' => 'text+k',
- 'content' => '1 2 3'
- ),
- '//depot/baz' => array(
- 'type' => 'ktext',
- 'content' => 'no keywords changed'
- ),
- ),
- 'should_fail' => true
- ),
- 'ktext-with-keywords-unchanged' => array(
- 'review_files' => array(
- '//depot/foo' => array(
- 'type' => 'text',
- 'content' => '123'
- ),
- '//depot/bar' => array(
- 'type' => 'text',
- 'content' => 'a b c'
- ),
- '//depot/baz' => array(
- 'type' => 'text+k',
- 'content' => 'date $DateTime$ 1'
- ),
- ),
- 'submit_files' => array(
- '//depot/foo' => array(
- 'type' => 'text',
- 'content' => '123'
- ),
- '//depot/bar' => array(
- 'type' => 'text',
- 'content' => 'a b c'
- ),
- '//depot/baz' => array(
- 'type' => 'text+k',
- 'content' => 'date $DateTime$ 1'
- ),
- ),
- 'should_fail' => false
- ),
- 'ktext-with-keywords-changed' => array(
- 'review_files' => array(
- '//depot/foo' => array(
- 'type' => 'text',
- 'content' => '123'
- ),
- '//depot/bar' => array(
- 'type' => 'text',
- 'content' => 'a b c'
- ),
- '//depot/baz' => array(
- 'type' => 'text+k',
- 'content' => 'date $DateTime$ 1'
- ),
- ),
- 'submit_files' => array(
- '//depot/foo' => array(
- 'type' => 'text',
- 'content' => '123'
- ),
- '//depot/bar' => array(
- 'type' => 'text',
- 'content' => 'a b c'
- ),
- '//depot/baz' => array(
- 'type' => 'text+k',
- 'content' => 'date $DateTime$ 2'
- ),
- ),
- 'should_fail' => true
- ),
- 'ktext-missing-file' => array(
- 'review_files' => array(
- '//depot/foo' => array(
- 'type' => 'text',
- 'content' => '123'
- ),
- '//depot/bar' => array(
- 'type' => 'ktext',
- 'content' => 'a b c'
- ),
- ),
- 'submit_files' => array(
- '//depot/foo' => array(
- 'type' => 'text',
- 'content' => '123'
- ),
- ),
- 'should_fail' => true
- ),
- );
- }
-
- /**
- * Copy and tweak the trigger script.
- *
- * @param string $targetFilename filename for the local script copy
- * @param array $configPaths list with paths that will be inserted into the
- * script as locations for the config files
- * @param array $verifyLines list with strings keyed by line numbers
- * these lines will be verified in the original script
- * to ensure they contain given values
- * @param array $deleteLines list of line number to remove in the local copy
- * @param int $insertAfterLine line number where the new paths for config files will be inserted
- */
- protected function adjustTriggerScript(
- $targetFilename,
- array $configPaths,
- array $verifyLines,
- array $deleteLines,
- $insertAfterLine
- ) {
- // we just replace particular lines by their number
- // this will likely break if the script is modified, so before the replacement
- // we check some lines around to ensure they contain the correct data
- $this->verifyFileLines($this->getScriptPath(), $verifyLines);
-
- // copy the script and modify lines
- $source = fopen($this->getScriptPath(), 'r');
- $target = fopen($targetFilename, 'w');
- $lineNumber = 0;
- while (!feof($source)) {
- $lineNumber++;
- $line = fgets($source);
-
- // copy the line unless its a line for removal
- if (!in_array($lineNumber, $deleteLines)) {
- fwrite($target, $line);
- }
-
- // insert new line(s) with config path(s) if we are at the correct spot
- if ($lineNumber === $insertAfterLine) {
- foreach ($configPaths as $path) {
- fwrite($target, $path . PHP_EOL);
- }
- }
- }
-
- fclose($source);
- fclose($target);
-
- // ensure script is executable
- chmod($targetFilename, 0755);
- }
-
- /**
- * Helper method to verify that given lines in the given file contain
- * given values.
- *
- * @param string $filename filename to check for lines content
- * @param array $lines list of lines to check; each item
- * contains line number in key and expected content in value
- * (the line in the file must begin with this value, but might be longer)
- */
- protected function verifyFileLines($filename, array $lines)
- {
- $lineNumbersToCheck = array_keys($lines);
- $lineNumber = 0;
- $handle = fopen($filename, 'r');
- while (!feof($handle) && $lineNumber < max($lineNumbersToCheck)) {
- $lineNumber++;
- $line = fgets($handle);
-
- if (array_key_exists($lineNumber, $lines)) {
- $this->assertTrue(
- strpos($line, $lines[$lineNumber]) === 0,
- "Script line $lineNumber doesn't match the expected value."
- . "\nExpected: " . $lines[$lineNumber]
- . "\n Found: " . $line
- );
- }
- }
-
- fclose($handle);
- }
-
- /**
- * Start built-in web-server on a given port.
- *
- * @param string|int $port port to start web server at
- * @param string $logFile web-server log file (will log both stdout and stderr)
- * @return int pid of the web-server process
- */
- protected function startWebServer($port, $logFile)
- {
- // launch a web-server to monitor for requests from the trigger script
- $command = PHP_BINDIR . '/php -S localhost:' . $port . ' > ' . $logFile . ' 2>&1 & echo $!;';
- $pid = exec($command, $output);
-
- // sleep to give the web-server a chance to startup
- usleep(250000);
-
- return $pid;
- }
-
- /**
- * Helper method to create variant for given $changeData. Intended for data provider
- * for testing strict/enforce triggers.
- *
- * @param array $changeData change data
- * @param callable|null $onBeforeSubmit coptional - before change submit callback
- * @return array
- */
- protected function getVariants(array $changeData, $onBeforeSubmit = null)
- {
- $p4 = $this->p4;
- return array(
- // identical files
- 'identical' => $changeData + array(
- 'onBeforeSubmit' => function ($change, $review) use ($onBeforeSubmit) {
- if (is_callable($onBeforeSubmit)) {
- call_user_func_array($onBeforeSubmit, array($change, $review));
- }
- }
- ),
-
- // one file modified
- 'fileModified' => $changeData + array(
- 'onBeforeSubmit' => function ($change, $review) use ($onBeforeSubmit) {
- $change->getFileObjects()->first()->setLocalContents('xyz');
-
- if (is_callable($onBeforeSubmit)) {
- call_user_func_array($onBeforeSubmit, array($change, $review));
- }
- }
- ),
-
- // one file added
- 'fileAdded' => $changeData + array(
- 'onBeforeSubmit' => function ($change, $review) use ($onBeforeSubmit, $p4) {
- $file = new File($p4);
- $file->setFilespec('//depot/newfile')->open()->setLocalContents('xyz 123');
- $change->addFile($file);
-
- if (is_callable($onBeforeSubmit)) {
- call_user_func_array($onBeforeSubmit, array($change, $review));
- }
- }
- ),
-
- // one file removed
- 'fileRemoved' => $changeData + array(
- 'onBeforeSubmit' => function ($change, $review) use ($onBeforeSubmit) {
- $files = $change->getFiles();
- array_shift($files);
- $change->setFiles($files);
-
- if (is_callable($onBeforeSubmit)) {
- call_user_func_array($onBeforeSubmit, array($change, $review));
- }
- }
- ),
- );
- }
-
- /**
- * Helper method to prepare list of triggers. Intended for data provider for testing
- * strict/enforce triggers.
- *
- * @param string $path triggers path
- * @param array $types optional - list of triggers types, recognized values are:
- * 'strict' for strict trigger
- * 'enforce' for enforce trigger
- * 'both' for strict and enforce triggers
- * @param array $flags optional - additional flags for triggers
- * @return array
- */
- protected function getTriggers($path, array $types = null, $flags = array())
- {
- $types = is_array($types) ? $types : array('strict', 'enforce', 'both');
- $triggers = array();
-
- if (in_array('strict', $types)) {
- $triggers[] = array(
- array('strict', $path, $flags)
- );
- }
- if (in_array('enforce', $types)) {
- $triggers[] = array(
- array('enforce', $path, $flags)
- );
- }
- if (in_array('both', $types)) {
- $triggers[] = array(
- array('strict', $path, $flags),
- array('enforce', $path, $flags)
- );
- }
-
- return $triggers;
- }
-
- /**
- * Helper method to install triggers in Perforce.
- *
- * @param array $data data for triggers, each set is supposed to be an array with 3 values:
- * - type trigger pseudo-type (job, user, group etc. or enforce or strict)
- * - paths list of paths for the trigger
- * - params additional params to be appended to the trigger command
- * by default, '-v %change%', '-t <type>' and '-c <config_file>'
- * are automatically added
- */
- protected function installTriggers(array $data, $triggerScriptPath = null)
- {
- $triggerScriptPath = $triggerScriptPath ?: $this->getScriptPath();
- $triggers = Triggers::fetch($this->superP4);
- $lines = $triggers->getTriggers();
- $types = array(
- 'job' => 'form-commit',
- 'user' => 'form-commit',
- 'userdel' => 'form-delete',
- 'group' => 'form-commit',
- 'groupdel' => 'form-delete',
- 'changesave' => 'form-save',
- 'shelve' => 'shelve-commit',
- 'commit' => 'change-commit',
- 'enforce' => 'change-submit',
- 'strict' => 'change-content',
- );
-
- foreach ($data as $key => $triggerData) {
- if (!is_array($triggerData) || count($triggerData) !== 3) {
- throw new \Exception("Invalid triggers format.");
- }
-
- list($type, $paths, $params) = $triggerData;
-
- if (!in_array($type, array_keys($types))) {
- throw new \InvalidArgumentException(
- "Invalid trigger type: " . implode(', ', array_keys($types)) . " are accepted."
- );
- }
- if (!is_array($params)) {
- throw new \InvalidArgumentException("Invalid trigger params: expecting an array.");
- }
-
- // mix provided options with defaults
- $options = $params;
- $options[] = '-t ' . $type;
- $options[] = '-c %quote%' . $this->scriptConfigFile . '%quote%';
- $options[] = in_array($type, array('shelve', 'commit', 'enforce', 'strict'))
- ? '-v %change%'
- : '-v %formname%';
-
- foreach ((array) $paths as $path) {
- $lines[] = array(
- 'name' => 'test.' . $type . '.' . $key,
- 'type' => $types[$type],
- 'path' => $path,
- 'command' => '%quote%' . $triggerScriptPath . '%quote% ' . implode(' ', $options)
- );
- }
- }
-
- $triggers->setTriggers($lines)->save();
- }
-
- protected function getScriptPath()
- {
- return BASE_PATH . '/p4-bin/scripts/' . $this->scriptBasename;
- }
-
- protected function configureScript(array $config = array())
- {
- $this->scriptConfigFile = DATA_PATH . '/trigger-script.config';
-
- $config += array(
- 'ADMIN_USER' => '"tester"',
- 'ADMIN_TICKET_FILE' => '"' . DATA_PATH . '/p4tickets.txt"',
- 'P4_PORT' => "'" . $this->getP4Params('port') . "'"
- );
-
- $configLines = array_map(
- function ($lhs, $rhs) {
- return $lhs . '=' . $rhs;
- },
- array_keys($config),
- $config
- );
-
- file_put_contents($this->scriptConfigFile, implode("\n", $configLines));
- }
-
- protected function assertTaskQueued($logFile, $token)
- {
- usleep(250000);
- $logData = file_get_contents($logFile);
- $this->assertTrue(strpos($logData, '/queue/add/' . $token) !== false);
- file_put_contents($logFile, '');
- }
-
- protected function getExpectedUsage($help = false)
- {
- $scriptPath = $this->getScriptPath();
- $scriptName = basename($scriptPath);
-
- return <<<USAGE
- Usage: $scriptName -t <type> -v <value> \
- [-p <p4port>] [-r] [-g <group-to-exclude>] [-c <config file>]
- $scriptName -o
- -t: specify the Swarm trigger type (e.g. job, shelve, commit)
- -v: specify the ID value
- -p: specify optional (recommended) P4PORT, only intended for
- '-t enforce' or '-t strict'
- -r: when using '-t strict' or '-t enforce', only apply this check
- to changes that are in review.
- -g: specify optional group to exclude for '-t enforce' or
- '-t strict'; members of this group, or subgroups thereof will
- not be subject to these triggers
- -c: specify optional config file to source variables
- -o: convenience flag to output the trigger lines
-
- This script is meant to be called from a Perforce trigger. It should be placed
- on the Perforce Server machine and the following entries should be added using
- 'p4 triggers' (use the -o flag to this script to only output these lines):
-
- swarm.job form-commit job "%quote%$scriptPath%quote% -t job -v %formname%"
- swarm.user form-commit user "%quote%$scriptPath%quote% -t user -v %formname%"
- swarm.userdel form-delete user "%quote%$scriptPath%quote% -t userdel -v %formname%"
- swarm.group form-commit group "%quote%$scriptPath%quote% -t group -v %formname%"
- swarm.groupdel form-delete group "%quote%$scriptPath%quote% -t groupdel -v %formname%"
- swarm.changesave form-save change "%quote%$scriptPath%quote% -t changesave -v %formname%"
- swarm.shelve shelve-commit //... "%quote%$scriptPath%quote% -t shelve -v %change%"
- swarm.commit change-commit //... "%quote%$scriptPath%quote% -t commit -v %change%"
- #swarm.enforce.1 change-submit //DEPOT_PATH1/... "%quote%$scriptPath%quote% -t enforce -v %change% -p %serverport%"
- #swarm.enforce.2 change-submit //DEPOT_PATH2/... "%quote%$scriptPath%quote% -t enforce -v %change% -p %serverport%"
- #swarm.strict.1 change-content //DEPOT_PATH1/... "%quote%$scriptPath%quote% -t strict -v %change% -p %serverport%"
- #swarm.strict.2 change-content //DEPOT_PATH2/... "%quote%$scriptPath%quote% -t strict -v %change% -p %serverport%"
- Notes:
-
- * The use of '%quote%' is not supported on 2010.2 servers (they are harmless
- though); if you're using this version, ensure you don't have any spaces in the
- pathname to this script.
-
- * This script requires configuration to be set in an external configuration file
- or directly in the script itself, such as the Swarm host and token.
- By default, this script will source any of these config file:
- /etc/perforce/swarm-trigger.conf
- /opt/perforce/etc/swarm-trigger.conf
- swarm-trigger.conf (in the same directory as this script)
- Lastly, if -c <config file> is passed, that file will be sourced too.
-
- * For 'enforce' triggers (enforce that a change to be submitted is tied to an
- approved review), or 'strict' triggers (verify that the content of a change to
- be submitted matches the content of its associated approved review), uncomment
- the appropriate lines and replace DEPOT_PATH as appropriate. For additional
- paths to check, increment the trigger name suffix so that each trigger name is
- named uniquely.
-
- * For 'enforce' or 'strict' triggers, you can optionally specify a group whose
- members will not be subject to these triggers.
-
- * For 'enforce' or 'strict' triggers, if your Perforce Server is SSL-enabled,
- add the "ssl:" protocol prefix to "%serverport%".
-
- USAGE;
- }
- }