First commit
This commit is contained in:
commit
c6e2478c40
13918 changed files with 2303184 additions and 0 deletions
208
sites/all/modules/civicrm/Civi/Test/CiviEnvBuilder.php
Normal file
208
sites/all/modules/civicrm/Civi/Test/CiviEnvBuilder.php
Normal file
|
@ -0,0 +1,208 @@
|
|||
<?php
|
||||
namespace Civi\Test;
|
||||
|
||||
use Civi\Test\CiviEnvBuilder\CallbackStep;
|
||||
use Civi\Test\CiviEnvBuilder\ExtensionsStep;
|
||||
use Civi\Test\CiviEnvBuilder\SqlFileStep;
|
||||
use Civi\Test\CiviEnvBuilder\SqlStep;
|
||||
use Civi\Test\CiviEnvBuilder\StepInterface;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Class CiviEnvBuilder
|
||||
*
|
||||
* Provides a fluent interface for tracking a set of steps.
|
||||
* By computing and storing a signature for the list steps, we can
|
||||
* determine whether to (a) do nothing with the list or (b)
|
||||
* reapply all the steps.
|
||||
*/
|
||||
class CiviEnvBuilder {
|
||||
protected $name;
|
||||
|
||||
private $steps = array();
|
||||
|
||||
/**
|
||||
* @var string|NULL
|
||||
* A digest of the values in $steps.
|
||||
*/
|
||||
private $targetSignature = NULL;
|
||||
|
||||
public function __construct($name) {
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function addStep(StepInterface $step) {
|
||||
$this->targetSignature = NULL;
|
||||
$this->steps[] = $step;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function callback($callback, $signature = NULL) {
|
||||
return $this->addStep(new CallbackStep($callback, $signature));
|
||||
}
|
||||
|
||||
public function sql($sql) {
|
||||
return $this->addStep(new SqlStep($sql));
|
||||
}
|
||||
|
||||
public function sqlFile($file) {
|
||||
return $this->addStep(new SqlFileStep($file));
|
||||
}
|
||||
|
||||
/**
|
||||
* Require that an extension be installed.
|
||||
*
|
||||
* @param string|array $names
|
||||
* One or more extension names. You may use a wildcard '*'.
|
||||
* @return CiviEnvBuilder
|
||||
*/
|
||||
public function install($names) {
|
||||
return $this->addStep(new ExtensionsStep('install', $names));
|
||||
}
|
||||
|
||||
/**
|
||||
* Require an extension be installed (identified by its directory).
|
||||
*
|
||||
* @param string $dir
|
||||
* The current test directory. We'll search for info.xml to
|
||||
* see what this extension is.
|
||||
* @return CiviEnvBuilder
|
||||
* @throws \CRM_Extension_Exception_ParseException
|
||||
*/
|
||||
public function installMe($dir) {
|
||||
return $this->addStep(new ExtensionsStep('install', $this->whoAmI($dir)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Require an extension be uninstalled.
|
||||
*
|
||||
* @param string|array $names
|
||||
* One or more extension names. You may use a wildcard '*'.
|
||||
* @return CiviEnvBuilder
|
||||
*/
|
||||
public function uninstall($names) {
|
||||
return $this->addStep(new ExtensionsStep('uninstall', $names));
|
||||
}
|
||||
|
||||
/**
|
||||
* Require an extension be uninstalled (identified by its directory).
|
||||
*
|
||||
* @param string $dir
|
||||
* The current test directory. We'll search for info.xml to
|
||||
* see what this extension is.
|
||||
* @return CiviEnvBuilder
|
||||
* @throws \CRM_Extension_Exception_ParseException
|
||||
*/
|
||||
public function uninstallMe($dir) {
|
||||
return $this->addStep(new ExtensionsStep('uninstall', $this->whoAmI($dir)));
|
||||
}
|
||||
|
||||
protected function assertValid() {
|
||||
foreach ($this->steps as $step) {
|
||||
if (!$step->isValid()) {
|
||||
throw new RuntimeException("Found invalid step: " . var_dump($step, 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getTargetSignature() {
|
||||
if ($this->targetSignature === NULL) {
|
||||
$buf = '';
|
||||
foreach ($this->steps as $step) {
|
||||
$buf .= $step->getSig();
|
||||
}
|
||||
$this->targetSignature = md5($buf);
|
||||
}
|
||||
|
||||
return $this->targetSignature;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getSavedSignature() {
|
||||
$liveSchemaRev = NULL;
|
||||
$pdo = \Civi\Test::pdo();
|
||||
$pdoStmt = $pdo->query(sprintf(
|
||||
"SELECT rev FROM %s.civitest_revs WHERE name = %s",
|
||||
\Civi\Test::dsn('database'),
|
||||
$pdo->quote($this->name)
|
||||
));
|
||||
foreach ($pdoStmt as $row) {
|
||||
$liveSchemaRev = $row['rev'];
|
||||
}
|
||||
return $liveSchemaRev;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $newSignature
|
||||
*/
|
||||
protected function setSavedSignature($newSignature) {
|
||||
$pdo = \Civi\Test::pdo();
|
||||
$query = sprintf(
|
||||
'INSERT INTO %s.civitest_revs (name,rev) VALUES (%s,%s) '
|
||||
. 'ON DUPLICATE KEY UPDATE rev = %s;',
|
||||
\Civi\Test::dsn('database'),
|
||||
$pdo->quote($this->name),
|
||||
$pdo->quote($newSignature),
|
||||
$pdo->quote($newSignature)
|
||||
);
|
||||
|
||||
if (\Civi\Test::execute($query) === FALSE) {
|
||||
throw new RuntimeException("Failed to flag schema version: $query");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if there's been a change in the preferred configuration.
|
||||
* If the preferred-configuration matches the last test, keep it. Otherwise,
|
||||
* destroy and recreate.
|
||||
*
|
||||
* @param bool $force
|
||||
* Forcibly execute the build, even if the configuration hasn't changed.
|
||||
* This will slow-down the tests, but it may be appropriate for some very sloppy
|
||||
* tests.
|
||||
* @return CiviEnvBuilder
|
||||
*/
|
||||
public function apply($force = FALSE) {
|
||||
$dbName = \Civi\Test::dsn('database');
|
||||
$query = "USE {$dbName};"
|
||||
. "CREATE TABLE IF NOT EXISTS civitest_revs (name VARCHAR(64) PRIMARY KEY, rev VARCHAR(64));";
|
||||
|
||||
if (\Civi\Test::execute($query) === FALSE) {
|
||||
throw new \RuntimeException("Failed to flag schema version: $query");
|
||||
}
|
||||
|
||||
$this->assertValid();
|
||||
|
||||
if (!$force && $this->getSavedSignature() === $this->getTargetSignature()) {
|
||||
return $this;
|
||||
}
|
||||
foreach ($this->steps as $step) {
|
||||
$step->run($this);
|
||||
}
|
||||
$this->setSavedSignature($this->getTargetSignature());
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $dir
|
||||
* @return null
|
||||
* @throws \CRM_Extension_Exception_ParseException
|
||||
*/
|
||||
protected function whoAmI($dir) {
|
||||
while ($dir && dirname($dir) !== $dir && !file_exists("$dir/info.xml")) {
|
||||
$dir = dirname($dir);
|
||||
}
|
||||
if (file_exists("$dir/info.xml")) {
|
||||
$info = \CRM_Extension_Info::loadFromFile("$dir/info.xml");
|
||||
$name = $info->key;
|
||||
return $name;
|
||||
}
|
||||
return $name;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
namespace Civi\Test\CiviEnvBuilder;
|
||||
class CallbackStep implements StepInterface {
|
||||
private $callback;
|
||||
private $sig;
|
||||
|
||||
/**
|
||||
* CallbackStep constructor.
|
||||
* @param $callback
|
||||
* @param $sig
|
||||
*/
|
||||
public function __construct($callback, $sig = NULL) {
|
||||
$this->callback = $callback;
|
||||
$this->sig = $sig === NULL ? md5(var_export($callback, 1)) : $sig;
|
||||
}
|
||||
|
||||
public function getSig() {
|
||||
return $this->sig;
|
||||
}
|
||||
|
||||
public function isValid() {
|
||||
return is_callable($this->callback);
|
||||
}
|
||||
|
||||
public function run($ctx) {
|
||||
call_user_func($this->callback, $ctx);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
namespace Civi\Test\CiviEnvBuilder;
|
||||
class ExtensionsStep implements StepInterface {
|
||||
private $action;
|
||||
private $names;
|
||||
|
||||
/**
|
||||
* ExtensionStep constructor.
|
||||
* @param string $action
|
||||
* Ex: 'install', 'uninstall'.
|
||||
* @param string|array $names
|
||||
*/
|
||||
public function __construct($action, $names) {
|
||||
$this->action = $action;
|
||||
$this->names = (array) $names;
|
||||
}
|
||||
|
||||
public function getSig() {
|
||||
return 'ext:' . implode(',', $this->names);
|
||||
}
|
||||
|
||||
public function isValid() {
|
||||
if (!in_array($this->action, array('install', 'uninstall'))) {
|
||||
return FALSE;
|
||||
}
|
||||
foreach ($this->names as $name) {
|
||||
if (!is_string($name)) {
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
public function run($ctx) {
|
||||
$allKeys = \CRM_Extension_System::singleton()->getFullContainer()->getKeys();
|
||||
$names = \CRM_Utils_String::filterByWildcards($this->names, $allKeys, TRUE);
|
||||
|
||||
$manager = \CRM_Extension_System::singleton()->getManager();
|
||||
switch ($this->action) {
|
||||
case 'install':
|
||||
$manager->install($names);
|
||||
break;
|
||||
|
||||
case 'uninstall':
|
||||
$manager->disable($names);
|
||||
$manager->uninstall($names);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
namespace Civi\Test\CiviEnvBuilder;
|
||||
|
||||
class SqlFileStep implements StepInterface {
|
||||
private $file;
|
||||
|
||||
/**
|
||||
* SqlFileStep constructor.
|
||||
* @param string $file
|
||||
*/
|
||||
public function __construct($file) {
|
||||
$this->file = $file;
|
||||
}
|
||||
|
||||
|
||||
public function getSig() {
|
||||
return implode(' ', array(
|
||||
$this->file,
|
||||
filemtime($this->file),
|
||||
filectime($this->file),
|
||||
));
|
||||
}
|
||||
|
||||
public function isValid() {
|
||||
return is_file($this->file) && is_readable($this->file);
|
||||
}
|
||||
|
||||
public function run($ctx) {
|
||||
/** @var $ctx \CiviEnvBuilder */
|
||||
if (\Civi\Test::execute(@file_get_contents($this->file)) === FALSE) {
|
||||
throw new \RuntimeException("Cannot load {$this->file}. Aborting.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
namespace Civi\Test\CiviEnvBuilder;
|
||||
class SqlStep implements StepInterface {
|
||||
private $sql;
|
||||
|
||||
/**
|
||||
* SqlFileStep constructor.
|
||||
* @param string $sql
|
||||
*/
|
||||
public function __construct($sql) {
|
||||
$this->sql = $sql;
|
||||
}
|
||||
|
||||
|
||||
public function getSig() {
|
||||
return md5($this->sql);
|
||||
}
|
||||
|
||||
public function isValid() {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
public function run($ctx) {
|
||||
/** @var $ctx \CiviEnvBuilder */
|
||||
if (\Civi\Test::execute($this->sql) === FALSE) {
|
||||
throw new \RuntimeException("Cannot execute: {$this->sql}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
namespace Civi\Test\CiviEnvBuilder;
|
||||
|
||||
interface StepInterface {
|
||||
public function getSig();
|
||||
|
||||
public function isValid();
|
||||
|
||||
public function run($ctx);
|
||||
|
||||
}
|
290
sites/all/modules/civicrm/Civi/Test/CiviTestListener.php
Normal file
290
sites/all/modules/civicrm/Civi/Test/CiviTestListener.php
Normal file
|
@ -0,0 +1,290 @@
|
|||
<?php
|
||||
|
||||
namespace Civi\Test;
|
||||
|
||||
/**
|
||||
* Class CiviTestListener
|
||||
* @package Civi\Test
|
||||
*
|
||||
* CiviTestListener participates in test-execution, looking for test-classes
|
||||
* which have certain tags. If the tags are found, the listener will perform
|
||||
* additional setup/teardown logic.
|
||||
*
|
||||
* @see EndToEndInterface
|
||||
* @see HeadlessInterface
|
||||
* @see HookInterface
|
||||
*/
|
||||
class CiviTestListener extends \PHPUnit_Framework_BaseTestListener {
|
||||
/**
|
||||
* @var \CRM_Core_TemporaryErrorScope
|
||||
*/
|
||||
private $errorScope;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
* Ex: $cache['Some_Test_Class']['civicrm_foobar'] = 'hook_civicrm_foobar';
|
||||
* Array(string $testClass => Array(string $hookName => string $methodName)).
|
||||
*/
|
||||
private $cache = array();
|
||||
|
||||
/**
|
||||
* @var \CRM_Core_Transaction|NULL
|
||||
*/
|
||||
private $tx;
|
||||
|
||||
public function startTestSuite(\PHPUnit_Framework_TestSuite $suite) {
|
||||
$byInterface = $this->indexTestsByInterface($suite->tests());
|
||||
$this->validateGroups($byInterface);
|
||||
$this->autoboot($byInterface);
|
||||
}
|
||||
|
||||
public function endTestSuite(\PHPUnit_Framework_TestSuite $suite) {
|
||||
$this->cache = array();
|
||||
}
|
||||
|
||||
public function startTest(\PHPUnit_Framework_Test $test) {
|
||||
if ($this->isCiviTest($test)) {
|
||||
error_reporting(E_ALL);
|
||||
$this->errorScope = \CRM_Core_TemporaryErrorScope::useException();
|
||||
}
|
||||
|
||||
if ($test instanceof HeadlessInterface) {
|
||||
$this->bootHeadless($test);
|
||||
}
|
||||
|
||||
if ($test instanceof HookInterface) {
|
||||
// Note: bootHeadless() indirectly resets any hooks, which means that hook_civicrm_config
|
||||
// is unsubscribable. However, after bootHeadless(), we're free to subscribe to hooks again.
|
||||
$this->registerHooks($test);
|
||||
}
|
||||
|
||||
if ($test instanceof TransactionalInterface) {
|
||||
$this->tx = new \CRM_Core_Transaction(TRUE);
|
||||
$this->tx->rollback();
|
||||
}
|
||||
else {
|
||||
$this->tx = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
public function endTest(\PHPUnit_Framework_Test $test, $time) {
|
||||
if ($test instanceof TransactionalInterface) {
|
||||
$this->tx->rollback()->commit();
|
||||
$this->tx = NULL;
|
||||
}
|
||||
if ($test instanceof HookInterface) {
|
||||
\CRM_Utils_Hook::singleton()->reset();
|
||||
}
|
||||
if ($this->isCiviTest($test)) {
|
||||
error_reporting(E_ALL & ~E_NOTICE);
|
||||
$this->errorScope = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param HeadlessInterface|\PHPUnit_Framework_Test $test
|
||||
*/
|
||||
protected function bootHeadless($test) {
|
||||
if (CIVICRM_UF !== 'UnitTests') {
|
||||
throw new \RuntimeException('HeadlessInterface requires CIVICRM_UF=UnitTests');
|
||||
}
|
||||
|
||||
// Hrm, this seems wrong. Shouldn't we be resetting the entire session?
|
||||
$session = \CRM_Core_Session::singleton();
|
||||
$session->set('userID', NULL);
|
||||
|
||||
$test->setUpHeadless();
|
||||
|
||||
\CRM_Utils_System::flushCache();
|
||||
\Civi::reset();
|
||||
\CRM_Core_Session::singleton()->set('userID', NULL);
|
||||
$config = \CRM_Core_Config::singleton(TRUE, TRUE); // ugh, performance
|
||||
|
||||
if (property_exists($config->userPermissionClass, 'permissions')) {
|
||||
$config->userPermissionClass->permissions = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Civi\Test\HookInterface $test
|
||||
* @return array
|
||||
* Array(string $hookName => string $methodName)).
|
||||
*/
|
||||
protected function findTestHooks(HookInterface $test) {
|
||||
$class = get_class($test);
|
||||
if (!isset($this->cache[$class])) {
|
||||
$funcs = array();
|
||||
foreach (get_class_methods($class) as $func) {
|
||||
if (preg_match('/^hook_/', $func)) {
|
||||
$funcs[substr($func, 5)] = $func;
|
||||
}
|
||||
}
|
||||
$this->cache[$class] = $funcs;
|
||||
}
|
||||
return $this->cache[$class];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \PHPUnit_Framework_Test $test
|
||||
* @return bool
|
||||
*/
|
||||
protected function isCiviTest(\PHPUnit_Framework_Test $test) {
|
||||
return $test instanceof HookInterface || $test instanceof HeadlessInterface;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find any hook functions in $test and register them.
|
||||
*
|
||||
* @param \Civi\Test\HookInterface $test
|
||||
*/
|
||||
protected function registerHooks(HookInterface $test) {
|
||||
if (CIVICRM_UF !== 'UnitTests') {
|
||||
// This is not ideal -- it's just a side-effect of how hooks and E2E tests work.
|
||||
// We can temporarily subscribe to hooks in-process, but for other processes, it gets messy.
|
||||
throw new \RuntimeException('CiviHookTestInterface requires CIVICRM_UF=UnitTests');
|
||||
}
|
||||
\CRM_Utils_Hook::singleton()->reset();
|
||||
/** @var \CRM_Utils_Hook_UnitTests $hooks */
|
||||
$hooks = \CRM_Utils_Hook::singleton();
|
||||
foreach ($this->findTestHooks($test) as $hook => $func) {
|
||||
$hooks->setHook($hook, array($test, $func));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The first time we come across HeadlessInterface or EndToEndInterface, we'll
|
||||
* try to autoboot.
|
||||
*
|
||||
* Once the system is booted, there's nothing we can do -- we're stuck with that
|
||||
* environment. (Thank you, prolific define()s!) If there's a conflict between a
|
||||
* test-class and the active boot-level, then we'll have to bail.
|
||||
*
|
||||
* @param array $byInterface
|
||||
* List of test classes, keyed by major interface (HeadlessInterface vs EndToEndInterface).
|
||||
*/
|
||||
protected function autoboot($byInterface) {
|
||||
if (defined('CIVICRM_UF')) {
|
||||
// OK, nothing we can do. System has booted already.
|
||||
}
|
||||
elseif (!empty($byInterface['HeadlessInterface'])) {
|
||||
putenv('CIVICRM_UF=UnitTests');
|
||||
eval($this->cv('php:boot --level=full', 'phpcode'));
|
||||
}
|
||||
elseif (!empty($byInterface['EndToEndInterface'])) {
|
||||
putenv('CIVICRM_UF=');
|
||||
eval($this->cv('php:boot --level=full', 'phpcode'));
|
||||
}
|
||||
|
||||
$blurb = "Tip: Run the headless tests and end-to-end tests separately, e.g.\n"
|
||||
. " $ phpunit4 --group headless\n"
|
||||
. " $ phpunit4 --group e2e \n";
|
||||
|
||||
if (!empty($byInterface['HeadlessInterface']) && CIVICRM_UF !== 'UnitTests') {
|
||||
$testNames = implode(', ', array_keys($byInterface['HeadlessInterface']));
|
||||
throw new \RuntimeException("Suite includes headless tests ($testNames) which require CIVICRM_UF=UnitTests.\n\n$blurb");
|
||||
}
|
||||
if (!empty($byInterface['EndToEndInterface']) && CIVICRM_UF === 'UnitTests') {
|
||||
$testNames = implode(', ', array_keys($byInterface['EndToEndInterface']));
|
||||
throw new \RuntimeException("Suite includes end-to-end tests ($testNames) which do not support CIVICRM_UF=UnitTests.\n\n$blurb");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the "cv" command.
|
||||
*
|
||||
* This duplicates the standalone `cv()` wrapper that is recommended in bootstrap.php.
|
||||
* This duplication is necessary because `cv()` is optional, and downstream implementers
|
||||
* may alter, rename, or omit the wrapper, and (by virtue of its role in bootstrap) there
|
||||
* it is impossible to define it centrally.
|
||||
*
|
||||
* @param string $cmd
|
||||
* The rest of the command to send.
|
||||
* @param string $decode
|
||||
* Ex: 'json' or 'phpcode'.
|
||||
* @return string
|
||||
* Response output (if the command executed normally).
|
||||
* @throws \RuntimeException
|
||||
* If the command terminates abnormally.
|
||||
*/
|
||||
protected function cv($cmd, $decode = 'json') {
|
||||
$cmd = 'cv ' . $cmd;
|
||||
$descriptorSpec = array(0 => array("pipe", "r"), 1 => array("pipe", "w"), 2 => STDERR);
|
||||
$oldOutput = getenv('CV_OUTPUT');
|
||||
putenv("CV_OUTPUT=json");
|
||||
$process = proc_open($cmd, $descriptorSpec, $pipes, __DIR__);
|
||||
putenv("CV_OUTPUT=$oldOutput");
|
||||
fclose($pipes[0]);
|
||||
$result = stream_get_contents($pipes[1]);
|
||||
fclose($pipes[1]);
|
||||
if (proc_close($process) !== 0) {
|
||||
throw new \RuntimeException("Command failed ($cmd):\n$result");
|
||||
}
|
||||
switch ($decode) {
|
||||
case 'raw':
|
||||
return $result;
|
||||
|
||||
case 'phpcode':
|
||||
// If the last output is /*PHPCODE*/, then we managed to complete execution.
|
||||
if (substr(trim($result), 0, 12) !== "/*BEGINPHP*/" || substr(trim($result), -10) !== "/*ENDPHP*/") {
|
||||
throw new \RuntimeException("Command failed ($cmd):\n$result");
|
||||
}
|
||||
return $result;
|
||||
|
||||
case 'json':
|
||||
return json_decode($result, 1);
|
||||
|
||||
default:
|
||||
throw new \RuntimeException("Bad decoder format ($decode)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $tests
|
||||
* @return array
|
||||
*/
|
||||
protected function indexTestsByInterface($tests) {
|
||||
$byInterface = array('HeadlessInterface' => array(), 'EndToEndInterface' => array());
|
||||
foreach ($tests as $test) {
|
||||
/** @var \PHPUnit_Framework_Test $test */
|
||||
if ($test instanceof HeadlessInterface) {
|
||||
$byInterface['HeadlessInterface'][get_class($test)] = 1;
|
||||
}
|
||||
if ($test instanceof EndToEndInterface) {
|
||||
$byInterface['EndToEndInterface'][get_class($test)] = 1;
|
||||
}
|
||||
}
|
||||
return $byInterface;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that any tests have sensible groups, e.g.
|
||||
*
|
||||
* `HeadlessInterface` ==> `group headless`
|
||||
* `EndToEndInterface` ==> `group e2e`
|
||||
*
|
||||
* @param array $byInterface
|
||||
*/
|
||||
protected function validateGroups($byInterface) {
|
||||
foreach ($byInterface['HeadlessInterface'] as $className => $nonce) {
|
||||
$clazz = new \ReflectionClass($className);
|
||||
$docComment = str_replace("\r\n", "\n", $clazz->getDocComment());
|
||||
if (strpos($docComment, "@group headless\n") === FALSE) {
|
||||
echo "WARNING: Class $className implements HeadlessInterface. It should declare \"@group headless\".\n";
|
||||
}
|
||||
if (strpos($docComment, "@group e2e\n") !== FALSE) {
|
||||
echo "WARNING: Class $className implements HeadlessInterface. It should not declare \"@group e2e\".\n";
|
||||
}
|
||||
}
|
||||
foreach ($byInterface['EndToEndInterface'] as $className => $nonce) {
|
||||
$clazz = new \ReflectionClass($className);
|
||||
$docComment = str_replace("\r\n", "\n", $clazz->getDocComment());
|
||||
if (strpos($docComment, "@group e2e\n") === FALSE) {
|
||||
echo "WARNING: Class $className implements EndToEndInterface. It should declare \"@group e2e\".\n";
|
||||
}
|
||||
if (strpos($docComment, "@group headless\n") !== FALSE) {
|
||||
echo "WARNING: Class $className implements EndToEndInterface. It should not declare \"@group headless\".\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
52
sites/all/modules/civicrm/Civi/Test/Data.php
Normal file
52
sites/all/modules/civicrm/Civi/Test/Data.php
Normal file
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
namespace Civi\Test;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Class Data
|
||||
*/
|
||||
class Data {
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function populate() {
|
||||
\Civi\Test::schema()->truncateAll();
|
||||
|
||||
\Civi\Test::schema()->setStrict(FALSE);
|
||||
$sqlDir = dirname(dirname(__DIR__)) . "/sql";
|
||||
|
||||
$query2 = file_get_contents("$sqlDir/civicrm_data.mysql");
|
||||
$query3 = file_get_contents("$sqlDir/test_data.mysql");
|
||||
$query4 = file_get_contents("$sqlDir/test_data_second_domain.mysql");
|
||||
if (\Civi\Test::execute($query2) === FALSE) {
|
||||
throw new RuntimeException("Cannot load civicrm_data.mysql. Aborting.");
|
||||
}
|
||||
if (\Civi\Test::execute($query3) === FALSE) {
|
||||
throw new RuntimeException("Cannot load test_data.mysql. Aborting.");
|
||||
}
|
||||
if (\Civi\Test::execute($query4) === FALSE) {
|
||||
throw new RuntimeException("Cannot load test_data.mysql. Aborting.");
|
||||
}
|
||||
|
||||
unset($query, $query2, $query3);
|
||||
|
||||
\Civi\Test::schema()->setStrict(TRUE);
|
||||
|
||||
// Rebuild triggers
|
||||
civicrm_api('system', 'flush', array('version' => 3, 'triggers' => 1));
|
||||
|
||||
\CRM_Core_BAO_ConfigSetting::setEnabledComponents(array(
|
||||
'CiviEvent',
|
||||
'CiviContribute',
|
||||
'CiviMember',
|
||||
'CiviMail',
|
||||
'CiviReport',
|
||||
'CiviPledge',
|
||||
));
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
}
|
28
sites/all/modules/civicrm/Civi/Test/EndToEndInterface.php
Normal file
28
sites/all/modules/civicrm/Civi/Test/EndToEndInterface.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
namespace Civi\Test;
|
||||
|
||||
/**
|
||||
* Interface EndToEndInterface
|
||||
* @package Civi\Test
|
||||
*
|
||||
* To run your test against a live, CMS-integrated database, flag it with the the
|
||||
* EndToEndInterface.
|
||||
*
|
||||
* Note: The global variable $_CV should be pre-populated with some interesting data:
|
||||
*
|
||||
* - $_CV['CMS_URL']
|
||||
* - $_CV['ADMIN_USER']
|
||||
* - $_CV['ADMIN_PASS']
|
||||
* - $_CV['ADMIN_EMAIL']
|
||||
* - $_CV['DEMO_USER']
|
||||
* - $_CV['DEMO_PASS']
|
||||
* - $_CV['DEMO_EMAIL']
|
||||
*
|
||||
* Alternatively, if you wish to run a test in a headless environment,
|
||||
* flag it with HeadlessInterface.
|
||||
*
|
||||
* @see HeadlessInterface
|
||||
*/
|
||||
interface EndToEndInterface {
|
||||
|
||||
}
|
40
sites/all/modules/civicrm/Civi/Test/HeadlessInterface.php
Normal file
40
sites/all/modules/civicrm/Civi/Test/HeadlessInterface.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace Civi\Test;
|
||||
|
||||
/**
|
||||
* Interface HeadlessInterface
|
||||
* @package Civi\Test
|
||||
*
|
||||
* To run your test against a fake, headless database, flag it with the
|
||||
* HeadlessInterface. CiviTestListener will automatically boot Civi.
|
||||
*
|
||||
* Alternatively, if you wish to run a test in a live (CMS-enabled) environment,
|
||||
* flag it with EndToEndInterface.
|
||||
*
|
||||
* You may mix-in additional features for headless tests:
|
||||
* - HookInterface: Auto-register any functions named "hook_civicrm_foo()".
|
||||
* - TransactionalInterface: Wrap all work in a transaction, and rollback at the end.
|
||||
*
|
||||
* @see EndToEndInterface
|
||||
* @see HookInterface
|
||||
* @see TransactionalInterface
|
||||
*/
|
||||
interface HeadlessInterface {
|
||||
|
||||
/**
|
||||
* The setupHeadless function runs at the start of each test case, right before
|
||||
* the headless environment reboots.
|
||||
*
|
||||
* It should perform any necessary steps required for putting the database
|
||||
* in a consistent baseline -- such as loading schema and extensions.
|
||||
*
|
||||
* The utility `\Civi\Test::headless()` provides a number of helper functions
|
||||
* for managing this setup, and it includes optimizations to avoid redundant
|
||||
* setup work.
|
||||
*
|
||||
* @see \Civi\Test
|
||||
*/
|
||||
public function setUpHeadless();
|
||||
|
||||
}
|
30
sites/all/modules/civicrm/Civi/Test/HookInterface.php
Normal file
30
sites/all/modules/civicrm/Civi/Test/HookInterface.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace Civi\Test;
|
||||
|
||||
/**
|
||||
* Interface HookInterface
|
||||
* @package Civi\Test
|
||||
*
|
||||
* This interface allows you to subscribe to hooks as part of the test.
|
||||
* Simply create an eponymous hook function (e.g. `hook_civicrm_post()`).
|
||||
*
|
||||
* @code
|
||||
* class MyTest extends \PHPUnit_Framework_TestCase implements \Civi\Test\HookInterface {
|
||||
* public function hook_civicrm_post($op, $objectName, $objectId, &$objectRef) {
|
||||
* echo "Running hook_civicrm_post\n";
|
||||
* }
|
||||
* }
|
||||
* @endCode
|
||||
*
|
||||
* At time of writing, there are a few limitations in how HookInterface is handled
|
||||
* by CiviTestListener:
|
||||
*
|
||||
* - The test must execute in-process (aka HeadlessInterface; aka CIVICRM_UF==UnitTests).
|
||||
* End-to-end tests (multi-process tests) are not supported.
|
||||
* - Early bootstrap hooks (e.g. hook_civicrm_config) are not supported.
|
||||
*
|
||||
* @see CiviTestListener
|
||||
*/
|
||||
interface HookInterface {
|
||||
}
|
128
sites/all/modules/civicrm/Civi/Test/Schema.php
Normal file
128
sites/all/modules/civicrm/Civi/Test/Schema.php
Normal file
|
@ -0,0 +1,128 @@
|
|||
<?php
|
||||
namespace Civi\Test;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Class Schema
|
||||
*
|
||||
* Manage the entire database. This is useful for destroying or loading the schema.
|
||||
*/
|
||||
class Schema {
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* 'BASE TABLE' or 'VIEW'.
|
||||
* @return array
|
||||
*/
|
||||
public function getTables($type) {
|
||||
$pdo = \Civi\Test::pdo();
|
||||
// only consider real tables and not views
|
||||
$query = sprintf(
|
||||
"SELECT table_name FROM INFORMATION_SCHEMA.TABLES
|
||||
WHERE TABLE_SCHEMA = %s AND TABLE_TYPE = %s",
|
||||
$pdo->quote(\Civi\Test::dsn('database')),
|
||||
$pdo->quote($type)
|
||||
);
|
||||
$tables = $pdo->query($query);
|
||||
$result = array();
|
||||
foreach ($tables as $table) {
|
||||
$result[] = $table['table_name'];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function setStrict($checks) {
|
||||
$dbName = \Civi\Test::dsn('database');
|
||||
if ($checks) {
|
||||
$queries = array(
|
||||
"USE {$dbName};",
|
||||
"SET global innodb_flush_log_at_trx_commit = 1;",
|
||||
"SET SQL_MODE='STRICT_ALL_TABLES';",
|
||||
"SET foreign_key_checks = 1;",
|
||||
);
|
||||
}
|
||||
else {
|
||||
$queries = array(
|
||||
"USE {$dbName};",
|
||||
"SET foreign_key_checks = 0",
|
||||
"SET SQL_MODE='STRICT_ALL_TABLES';",
|
||||
"SET global innodb_flush_log_at_trx_commit = 2;",
|
||||
);
|
||||
}
|
||||
foreach ($queries as $query) {
|
||||
if (\Civi\Test::execute($query) === FALSE) {
|
||||
throw new RuntimeException("Query failed: $query");
|
||||
}
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function dropAll() {
|
||||
$queries = array();
|
||||
foreach ($this->getTables('VIEW') as $table) {
|
||||
if (preg_match('/^(civicrm_|log_)/', $table)) {
|
||||
$queries[] = "DROP VIEW $table";
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->getTables('BASE TABLE') as $table) {
|
||||
if (preg_match('/^(civicrm_|log_)/', $table)) {
|
||||
$queries[] = "DROP TABLE $table";
|
||||
}
|
||||
}
|
||||
|
||||
$this->setStrict(FALSE);
|
||||
foreach ($queries as $query) {
|
||||
if (\Civi\Test::execute($query) === FALSE) {
|
||||
throw new RuntimeException("dropSchema: Query failed: $query");
|
||||
}
|
||||
}
|
||||
$this->setStrict(TRUE);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Schema
|
||||
*/
|
||||
public function truncateAll() {
|
||||
$tables = \Civi\Test::schema()->getTables('BASE TABLE');
|
||||
|
||||
$truncates = array();
|
||||
$drops = array();
|
||||
foreach ($tables as $table) {
|
||||
// skip log tables
|
||||
if (substr($table, 0, 4) == 'log_') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// don't change list of installed extensions
|
||||
if ($table == 'civicrm_extension') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (substr($table, 0, 14) == 'civicrm_value_') {
|
||||
$drops[] = 'DROP TABLE ' . $table . ';';
|
||||
}
|
||||
elseif (substr($table, 0, 9) == 'civitest_') {
|
||||
// ignore
|
||||
}
|
||||
else {
|
||||
$truncates[] = 'TRUNCATE ' . $table . ';';
|
||||
}
|
||||
}
|
||||
|
||||
\Civi\Test::schema()->setStrict(FALSE);
|
||||
$queries = array_merge($truncates, $drops);
|
||||
foreach ($queries as $query) {
|
||||
if (\Civi\Test::execute($query) === FALSE) {
|
||||
throw new RuntimeException("Query failed: $query");
|
||||
}
|
||||
}
|
||||
\Civi\Test::schema()->setStrict(TRUE);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace Civi\Test;
|
||||
|
||||
/**
|
||||
* Interface HeadlessInterface
|
||||
* @package Civi\Test
|
||||
*
|
||||
* Mark a test with TransactionalInterface to instruct CiviTestListener to wrap
|
||||
* each test in a transaction (and rollback).
|
||||
*
|
||||
* Note: At time of writing, CiviTestListener only supports using TransactionalInterface if
|
||||
* the test is in-process and runs with CIVICRM_UF==UnitTests.
|
||||
*
|
||||
* For end-to-end testing, it is expected that the CMS will not participate in the transaction,
|
||||
* so the transaction mechanism will not work.
|
||||
*
|
||||
* @see HeadlessInterface
|
||||
*/
|
||||
interface TransactionalInterface {
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue