8 private static $singletons = array();
11 * Get the data source used for testing.
13 * @param string|NULL $part
14 * One of NULL, 'hostspec', 'port', 'username', 'password', 'database'.
15 * @return string|array|NULL
16 * If $part is omitted, return full DSN array.
17 * If $part is a string, return that part of the DSN.
19 public static function dsn($part = NULL) {
20 if (!isset(self
::$singletons['dsn'])) {
21 require_once "DB.php";
22 self
::$singletons['dsn'] = DB
::parseDSN(CIVICRM_DSN
);
26 return self
::$singletons['dsn'];
29 if (isset(self
::$singletons['dsn'][$part])) {
30 return self
::$singletons['dsn'][$part];
37 * Get a connection to the test database.
41 public static function pdo() {
42 if (!isset(self
::$singletons['pdo'])) {
43 $dsninfo = self
::dsn();
44 $host = $dsninfo['hostspec'];
45 $port = @$dsninfo['port'];
47 self
::$singletons['pdo'] = new PDO("mysql:host={$host}" . ($port ?
";port=$port" : ""),
48 $dsninfo['username'], $dsninfo['password'],
49 array(PDO
::MYSQL_ATTR_USE_BUFFERED_QUERY
=> TRUE)
52 catch (PDOException
$e) {
53 echo "Can't connect to MySQL server:" . PHP_EOL
. $e->getMessage() . PHP_EOL
;
57 return self
::$singletons['pdo'];
61 * Get the schema manager.
63 * @return \CiviTesterBuilder
66 * CiviTester::headless()->apply();
67 * CiviTester::headless()->sqlFile('ex.sql')->apply();
70 public static function headless() {
71 $civiRoot = dirname(dirname(dirname(dirname(__FILE__
))));
72 $builder = new CiviTesterBuilder('CiviTesterSchema');
74 ->callback(function ($ctx) {
75 if (CIVICRM_UF
!== 'UnitTests') {
76 throw new \
RuntimeException("CiviTester::headless() requires CIVICRM_UF=UnitTests");
78 $dbName = CiviTester
::dsn('database');
79 echo "Installing {$dbName} schema\n";
80 CiviTester
::schema()->dropAll();
82 ->sqlFile($civiRoot . "/sql/civicrm.mysql")
83 ->sql("DELETE FROM civicrm_extension")
84 ->callback(function ($ctx) {
85 CiviTester
::data()->populate();
91 * @return \CiviTesterSchema
93 public static function schema() {
94 if (!isset(self
::$singletons['schema'])) {
95 self
::$singletons['schema'] = new CiviTesterSchema();
97 return self
::$singletons['schema'];
102 * @return \CiviTesterData
104 public static function data() {
105 if (!isset(self
::$singletons['data'])) {
106 self
::$singletons['data'] = new CiviTesterData('CiviTesterData');
108 return self
::$singletons['data'];
112 * Prepare and execute a batch of SQL statements.
114 * @param string $query
117 public static function execute($query) {
118 $pdo = CiviTester
::pdo();
120 $string = preg_replace("/^#[^\n]*$/m", "\n", $query);
121 $string = preg_replace("/^(--[^-]).*/m", "\n", $string);
123 $queries = preg_split('/;\s*$/m', $string);
124 foreach ($queries as $query) {
125 $query = trim($query);
126 if (!empty($query)) {
127 $result = $pdo->query($query);
128 if ($pdo->errorCode() == 0) {
133 var_dump($pdo->errorInfo());
134 // die( "Cannot execute $query: " . $pdo->errorInfo() );
144 * Class CiviTesterSchema
146 * Manage the entire database. This is useful for destroying or loading the schema.
148 class CiviTesterSchema
{
151 * @param string $type
152 * 'BASE TABLE' or 'VIEW'.
155 public function getTables($type) {
156 $pdo = CiviTester
::pdo();
157 // only consider real tables and not views
159 "SELECT table_name FROM INFORMATION_SCHEMA.TABLES
160 WHERE TABLE_SCHEMA = %s AND TABLE_TYPE = %s",
161 $pdo->quote(CiviTester
::dsn('database')),
164 $tables = $pdo->query($query);
166 foreach ($tables as $table) {
167 $result[] = $table['table_name'];
172 public function setStrict($checks) {
173 $dbName = CiviTester
::dsn('database');
177 "SET global innodb_flush_log_at_trx_commit = 1;",
178 "SET SQL_MODE='STRICT_ALL_TABLES';",
179 "SET foreign_key_checks = 1;",
185 "SET foreign_key_checks = 0",
186 "SET SQL_MODE='STRICT_ALL_TABLES';",
187 "SET global innodb_flush_log_at_trx_commit = 2;",
190 foreach ($queries as $query) {
191 if (CiviTester
::execute($query) === FALSE) {
192 throw new RuntimeException("Query failed: $query");
198 public function dropAll() {
200 foreach ($this->getTables('VIEW') as $table) {
201 if (preg_match('/^(civicrm_|log_)/', $table)) {
202 $queries[] = "DROP VIEW $table";
206 foreach ($this->getTables('BASE TABLE') as $table) {
207 if (preg_match('/^(civicrm_|log_)/', $table)) {
208 $queries[] = "DROP TABLE $table";
212 $this->setStrict(FALSE);
213 foreach ($queries as $query) {
214 if (CiviTester
::execute($query) === FALSE) {
215 throw new RuntimeException("dropSchema: Query failed: $query");
218 $this->setStrict(TRUE);
226 public function truncateAll() {
227 $tables = CiviTester
::schema()->getTables('BASE TABLE');
229 $truncates = array();
231 foreach ($tables as $table) {
233 if (substr($table, 0, 4) == 'log_') {
237 // don't change list of installed extensions
238 if ($table == 'civicrm_extension') {
242 if (substr($table, 0, 14) == 'civicrm_value_') {
243 $drops[] = 'DROP TABLE ' . $table . ';';
245 elseif (substr($table, 0, 9) == 'civitest_') {
249 $truncates[] = 'TRUNCATE ' . $table . ';';
253 CiviTester
::schema()->setStrict(FALSE);
254 $queries = array_merge($truncates, $drops);
255 foreach ($queries as $query) {
256 if (CiviTester
::execute($query) === FALSE) {
257 throw new RuntimeException("Query failed: $query");
260 CiviTester
::schema()->setStrict(TRUE);
268 * Class CiviTesterData
270 class CiviTesterData
{
275 public function populate() {
276 CiviTester
::schema()->truncateAll();
278 CiviTester
::schema()->setStrict(FALSE);
279 // initialize test database
280 $sql_file2 = dirname(dirname(dirname(dirname(__FILE__
)))) . "/sql/civicrm_data.mysql";
281 $sql_file3 = dirname(dirname(dirname(dirname(__FILE__
)))) . "/sql/test_data.mysql";
282 $sql_file4 = dirname(dirname(dirname(dirname(__FILE__
)))) . "/sql/test_data_second_domain.mysql";
284 $query2 = file_get_contents($sql_file2);
285 $query3 = file_get_contents($sql_file3);
286 $query4 = file_get_contents($sql_file4);
287 if (CiviTester
::execute($query2) === FALSE) {
288 throw new RuntimeException("Cannot load civicrm_data.mysql. Aborting.");
290 if (CiviTester
::execute($query3) === FALSE) {
291 throw new RuntimeException("Cannot load test_data.mysql. Aborting.");
293 if (CiviTester
::execute($query4) === FALSE) {
294 throw new RuntimeException("Cannot load test_data.mysql. Aborting.");
297 unset($query, $query2, $query3);
299 CiviTester
::schema()->setStrict(TRUE);
302 civicrm_api('system', 'flush', array('version' => 3, 'triggers' => 1));
304 CRM_Core_BAO_ConfigSetting
::setEnabledComponents(array(
319 * Class CiviTesterBuilder
321 * Provides a fluent interface for tracking a set of steps.
322 * By computing and storing a signature for the list steps, we can
323 * determine whether to (a) do nothing with the list or (b)
324 * reapply all the steps.
326 class CiviTesterBuilder
{
329 private $steps = array();
333 * A digest of the values in $steps.
335 private $targetSignature = NULL;
337 public function __construct($name) {
341 public function addStep(CiviTesterStep
$step) {
342 $this->targetSignature
= NULL;
343 $this->steps
[] = $step;
347 public function callback($callback, $signature = NULL) {
348 return $this->addStep(new CiviTesterCallbackStep($callback, $signature));
351 public function sql($sql) {
352 return $this->addStep(new CiviTesterSqlStep($sql));
355 public function sqlFile($file) {
356 return $this->addStep(new CiviTesterSqlFileStep($file));
360 * Require an extension (based on its name).
362 * @param string $name
363 * @return \CiviTesterBuilder
365 public function ext($name) {
366 return $this->addStep(new CiviTesterExtensionStep($name));
370 * Require an extension (based on its directory).
373 * @return \CiviTesterBuilder
374 * @throws \CRM_Extension_Exception_ParseException
376 public function extDir($dir) {
377 while ($dir && dirname($dir) !== $dir && !file_exists("$dir/info.xml")) {
378 $dir = dirname($dir);
380 if (file_exists("$dir/info.xml")) {
381 $info = CRM_Extension_Info
::loadFromFile("$dir/info.xml");
384 return $this->addStep(new CiviTesterExtensionStep($name));
387 protected function assertValid() {
388 foreach ($this->steps
as $step) {
389 if (!$step->isValid()) {
390 throw new RuntimeException("Found invalid step: " . var_dump($step, 1));
398 protected function getTargetSignature() {
399 if ($this->targetSignature
=== NULL) {
401 foreach ($this->steps
as $step) {
402 $buf .= $step->getSig();
404 $this->targetSignature
= md5($buf);
407 return $this->targetSignature
;
413 protected function getSavedSignature() {
414 $liveSchemaRev = NULL;
415 $pdo = CiviTester
::pdo();
416 $pdoStmt = $pdo->query(sprintf(
417 "SELECT rev FROM %s.civitest_revs WHERE name = %s",
418 CiviTester
::dsn('database'),
419 $pdo->quote($this->name
)
421 foreach ($pdoStmt as $row) {
422 $liveSchemaRev = $row['rev'];
424 return $liveSchemaRev;
428 * @param $newSignature
430 protected function setSavedSignature($newSignature) {
431 $pdo = CiviTester
::pdo();
433 'INSERT INTO %s.civitest_revs (name,rev) VALUES (%s,%s) '
434 . 'ON DUPLICATE KEY UPDATE rev = %s;',
435 CiviTester
::dsn('database'),
436 $pdo->quote($this->name
),
437 $pdo->quote($newSignature),
438 $pdo->quote($newSignature)
441 if (CiviTester
::execute($query) === FALSE) {
442 throw new RuntimeException("Failed to flag schema version: $query");
447 * Determine if the schema is correct. If necessary, destroy and recreate.
452 public function apply($force = FALSE) {
453 $dbName = CiviTester
::dsn('database');
454 $query = "USE {$dbName};"
455 . "CREATE TABLE IF NOT EXISTS civitest_revs (name VARCHAR(64) PRIMARY KEY, rev VARCHAR(64));";
457 if (CiviTester
::execute($query) === FALSE) {
458 throw new RuntimeException("Failed to flag schema version: $query");
461 $this->assertValid();
463 if (!$force && $this->getSavedSignature() === $this->getTargetSignature()) {
466 foreach ($this->steps
as $step) {
469 $this->setSavedSignature($this->getTargetSignature());
475 interface CiviTesterStep
{
476 public function getSig();
478 public function isValid();
480 public function run($ctx);
484 class CiviTesterSqlFileStep
implements CiviTesterStep
{
488 * CiviTesterSqlFileStep constructor.
491 public function __construct($file) {
496 public function getSig() {
497 return implode(' ', array(
499 filemtime($this->file
),
500 filectime($this->file
),
504 public function isValid() {
505 return is_file($this->file
) && is_readable($this->file
);
508 public function run($ctx) {
509 /** @var $ctx CiviTesterBuilder */
510 if (CiviTester
::execute(@file_get_contents
($this->file
)) === FALSE) {
511 throw new RuntimeException("Cannot load {$this->file}. Aborting.");
517 class CiviTesterSqlStep
implements CiviTesterStep
{
521 * CiviTesterSqlFileStep constructor.
524 public function __construct($sql) {
529 public function getSig() {
530 return md5($this->sql
);
533 public function isValid() {
537 public function run($ctx) {
538 /** @var $ctx CiviTesterBuilder */
539 if (CiviTester
::execute($this->sql
) === FALSE) {
540 throw new RuntimeException("Cannot execute: {$this->sql}");
546 class CiviTesterCallbackStep
implements CiviTesterStep
{
551 * CiviTesterCallbackStep constructor.
555 public function __construct($callback, $sig = NULL) {
556 $this->callback
= $callback;
557 $this->sig
= $sig === NULL ?
md5(var_export($callback, 1)) : $sig;
560 public function getSig() {
564 public function isValid() {
565 return is_callable($this->callback
);
568 public function run($ctx) {
569 call_user_func($this->callback
, $ctx);
574 class CiviTesterExtensionStep
implements CiviTesterStep
{
578 * CiviTesterExtensionStep constructor.
581 public function __construct($name) {
585 public function getSig() {
586 return 'ext:' . $this->name
;
589 public function isValid() {
590 return is_string($this->name
);
593 public function run($ctx) {
594 CRM_Extension_System
::singleton()->getManager()->install(array(