From: Tim Otten Date: Tue, 12 May 2015 04:11:39 +0000 (-0700) Subject: CRM-16387 - Civi\API\ExternalBatch - Add helper for testing concurrent cron jobs X-Git-Url: https://vcs.fsf.org/?a=commitdiff_plain;h=888dab1e9b678b690aa1558b6ce7746cedca33b4;p=civicrm-core.git CRM-16387 - Civi\API\ExternalBatch - Add helper for testing concurrent cron jobs --- diff --git a/Civi/API/ExternalBatch.php b/Civi/API/ExternalBatch.php new file mode 100644 index 0000000000..1ff9282d0a --- /dev/null +++ b/Civi/API/ExternalBatch.php @@ -0,0 +1,233 @@ + array $apiCall). + */ + protected $apiCalls; + + protected $defaultParams; + + protected $root; + + protected $settingsPath; + + protected $env = array(); + + /** + * @var array + * Array(int $idx => Process $process). + */ + protected $processes; + + /** + * @var array + * Array(int $idx => array $apiResult). + */ + protected $apiResults; + + /** + * @param array $defaultParams + * Default values to merge into any API calls. + */ + public function __construct($defaultParams = array()) { + global $civicrm_root; + $this->root = $civicrm_root; + $this->settingsPath = defined('CIVICRM_SETTINGS_PATH') ? CIVICRM_SETTINGS_PATH : NULL; + $this->defaultParams = $defaultParams; + } + + /** + * @param string $entity + * @param string $action + * @param array $params + * @return $this + */ + public function addCall($entity, $action, $params = array()) { + $params = array_merge($this->defaultParams, $params); + + $this->apiCalls[] = array( + 'entity' => $entity, + 'action' => $action, + 'params' => $params, + ); + return $this; + } + + /** + * @param array $env + * List of environment variables to add. + * @return static + */ + public function addEnv($env) { + $this->env = array_merge($this->env, $env); + return $this; + } + + /** + * Run all the API calls concurrently. + * + * @return static + * @throws \CRM_Core_Exception + */ + public function start() { + foreach ($this->apiCalls as $idx => $apiCall) { + $process = $this->createProcess($apiCall); + $process->start(); + $this->processes[$idx] = $process; + } + return $this; + } + + /** + * @return int + * The number of running processes. + */ + public function getRunningCount() { + $count = 0; + foreach ($this->processes as $process) { + if ($process->isRunning()) { + $count++; + } + } + return $count; + } + + public function wait() { + while (!empty($this->processes)) { + usleep(self::POLL_INTERVAL); + foreach (array_keys($this->processes) as $idx) { + /** @var Process $process */ + $process = $this->processes[$idx]; + if (!$process->isRunning()) { + $parsed = json_decode($process->getOutput(), TRUE); + if ($process->getExitCode() || $parsed === NULL) { + $this->apiResults[] = array( + 'is_error' => 1, + 'error_message' => 'External API returned malformed response.', + 'trace' => array( + 'code' => $process->getExitCode(), + 'stdout' => $process->getOutput(), + 'stderr' => $process->getErrorOutput(), + ), + ); + } + else { + $this->apiResults[] = $parsed; + } + unset($this->processes[$idx]); + } + } + } + return $this; + } + + /** + * @return array + */ + public function getResults() { + return $this->apiResults; + } + + /** + * @param int $idx + * @return array + */ + public function getResult($idx = 0) { + return $this->apiResults[$idx]; + } + + /** + * Determine if the local environment supports running API calls + * externally. + * + * @return bool + */ + public function isSupported() { + // If you try in Windows, feel free to change this... + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN' || !function_exists('proc_open')) { + return FALSE; + } + if (!file_exists($this->root . '/bin/cli.php') || !file_exists($this->settingsPath)) { + return FALSE; + } + return TRUE; + } + + /** + * @param array $apiCall + * Array with keys: entity, action, params. + * @return Process + * @throws \CRM_Core_Exception + */ + public function createProcess($apiCall) { + $parts = array(); + + $executableFinder = new PhpExecutableFinder(); + $php = $executableFinder->find(); + if (!$php) { + throw new \CRM_Core_Exception("Failed to locate PHP interpreter."); + } + $parts[] = $php; + + $parts[] = escapeshellarg($this->root . '/bin/cli.php'); + $parts[] = escapeshellarg("-e=" . $apiCall['entity']); + $parts[] = escapeshellarg("-a=" . $apiCall['action']); + $parts[] = "--json"; + $parts[] = escapeshellarg("-u=dummyuser"); + foreach ($apiCall['params'] as $key => $value) { + $parts[] = escapeshellarg("--$key=$value"); + } + $command = implode(" ", $parts); + + $env = array_merge($this->env, array( + 'CIVICRM_SETTINGS' => $this->settingsPath, + )); + return new Process($command, $this->root, $env); + } + + /** + * @return string + */ + public function getRoot() { + return $this->root; + } + + /** + * @param string $root + */ + public function setRoot($root) { + $this->root = $root; + } + + /** + * @return string + */ + public function getSettingsPath() { + return $this->settingsPath; + } + + /** + * @param string $settingsPath + */ + public function setSettingsPath($settingsPath) { + $this->settingsPath = $settingsPath; + } + +} diff --git a/tests/phpunit/CiviTest/CiviUnitTestCase.php b/tests/phpunit/CiviTest/CiviUnitTestCase.php index b896aac5bb..30a9eea6ac 100755 --- a/tests/phpunit/CiviTest/CiviUnitTestCase.php +++ b/tests/phpunit/CiviTest/CiviUnitTestCase.php @@ -908,6 +908,39 @@ class CiviUnitTestCase extends PHPUnit_Extensions_Database_TestCase { return civicrm_api($entity, $action, $params); } + /** + * Create a batch of external API calls which can + * be executed concurrently. + * + * @code + * $calls = $this->createExternalAPI() + * ->addCall('Contact', 'get', ...) + * ->addCall('Contact', 'get', ...) + * ... + * ->run() + * ->getResults(); + * @endcode + * + * @return \Civi\API\ExternalBatch + * @throws PHPUnit_Framework_SkippedTestError + */ + public function createExternalAPI() { + global $civicrm_root; + $defaultParams = array( + 'version' => $this->_apiversion, + 'debug' => 1, + ); + + $calls = new \Civi\API\ExternalBatch($defaultParams); + $calls->setSettingsPath("$civicrm_root/tests/phpunit/CiviTest/civicrm.settings.cli.php"); + + if (!$calls->isSupported()) { + $this->markTestSkipped('The test relies on Civi\API\ExternalBatch. This is unsupported in the local environment.'); + } + + return $calls; + } + /** * wrap api functions. * so we can ensure they succeed & throw exceptions without litterering the test with checks