Skip to content

Commit

Permalink
Civi\Test - Backport helper test classes from 4.7.
Browse files Browse the repository at this point in the history
Note: These classes are not actually referenced by anything in core v4.6;
however, we provide so that extensions can use them.
  • Loading branch information
totten committed Jan 18, 2017
1 parent 92a49a3 commit c829601
Show file tree
Hide file tree
Showing 14 changed files with 1,125 additions and 0 deletions.
171 changes: 171 additions & 0 deletions Civi/Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<?php
namespace Civi;

use PDO;
use PDOException;

/**
* Class Test
*
* A facade for managing the test environment.
*/
class Test {

/**
* @var array
*/
private static $singletons = array();

/**
* Get the data source used for testing.
*
* @param string|NULL $part
* One of NULL, 'hostspec', 'port', 'username', 'password', 'database'.
* @return string|array|NULL
* If $part is omitted, return full DSN array.
* If $part is a string, return that part of the DSN.
*/
public static function dsn($part = NULL) {
if (!isset(self::$singletons['dsn'])) {
require_once "DB.php";
self::$singletons['dsn'] = \DB::parseDSN(CIVICRM_DSN);
}

if ($part === NULL) {
return self::$singletons['dsn'];
}

if (isset(self::$singletons['dsn'][$part])) {
return self::$singletons['dsn'][$part];
}

return NULL;
}

/**
* Get a connection to the test database.
*
* @return PDO
*/
public static function pdo() {
if (!isset(self::$singletons['pdo'])) {
$dsninfo = self::dsn();
$host = $dsninfo['hostspec'];
$port = @$dsninfo['port'];
try {
self::$singletons['pdo'] = new PDO("mysql:host={$host}" . ($port ? ";port=$port" : ""),
$dsninfo['username'], $dsninfo['password'],
array(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => TRUE)
);
}
catch (PDOException $e) {
echo "Can't connect to MySQL server:" . PHP_EOL . $e->getMessage() . PHP_EOL;
exit(1);
}
}
return self::$singletons['pdo'];
}

/**
* Create a builder for the headless environment.
*
* @return \Civi\Test\CiviEnvBuilder
*
* @code
* \Civi\Test::headless()->apply();
* \Civi\Test::headless()->sqlFile('ex.sql')->apply();
* @endCode
*/
public static function headless() {
$civiRoot = dirname(__DIR__);
$builder = new \Civi\Test\CiviEnvBuilder('CiviEnvBuilder');
$builder
->callback(function ($ctx) {
if (CIVICRM_UF !== 'UnitTests') {
throw new \RuntimeException("\\Civi\\Test::headless() requires CIVICRM_UF=UnitTests");
}
$dbName = \Civi\Test::dsn('database');
echo "Installing {$dbName} schema\n";
\Civi\Test::schema()->dropAll();
}, 'headless-drop')
->sqlFile($civiRoot . "/sql/civicrm.mysql")
->sql("DELETE FROM civicrm_extension")
->callback(function ($ctx) {
\Civi\Test::data()->populate();
}, 'populate');
return $builder;
}

/**
* Create a builder for end-to-end testing on the live environment.
*
* @return \Civi\Test\CiviEnvBuilder
*
* @code
* \Civi\Test::e2e()->apply();
* \Civi\Test::e2e()->install('foo.bar')->apply();
* @endCode
*/
public static function e2e() {
$builder = new \Civi\Test\CiviEnvBuilder('CiviEnvBuilder');
$builder
->callback(function ($ctx) {
if (CIVICRM_UF === 'UnitTests') {
throw new \RuntimeException("\\Civi\\Test::e2e() requires a real CMS. Found CIVICRM_UF=UnitTests.");
}
}, 'e2e-check');
return $builder;
}

/**
* @return \Civi\Test\Schema
*/
public static function schema() {
if (!isset(self::$singletons['schema'])) {
self::$singletons['schema'] = new \Civi\Test\Schema();
}
return self::$singletons['schema'];
}


/**
* @return \Civi\Test\Data
*/
public static function data() {
if (!isset(self::$singletons['data'])) {
self::$singletons['data'] = new \Civi\Test\Data('CiviTesterData');
}
return self::$singletons['data'];
}

/**
* Prepare and execute a batch of SQL statements.
*
* @param string $query
* @return bool
*/
public static function execute($query) {
$pdo = \Civi\Test::pdo();

$string = preg_replace("/^#[^\n]*$/m", "\n", $query);
$string = preg_replace("/^(--[^-]).*/m", "\n", $string);

$queries = preg_split('/;\s*$/m', $string);
foreach ($queries as $query) {
$query = trim($query);
if (!empty($query)) {
$result = $pdo->query($query);
if ($pdo->errorCode() == 0) {
continue;
}
else {
var_dump($result);
var_dump($pdo->errorInfo());
// die( "Cannot execute $query: " . $pdo->errorInfo() );
}
}
}
return TRUE;
}

}
208 changes: 208 additions & 0 deletions Civi/Test/CiviEnvBuilder.php
Original file line number Diff line number Diff line change
@@ -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;
}

}
Loading

0 comments on commit c829601

Please sign in to comment.