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::builder()->apply();
67 * CiviTester::builder()->sqlFile('ex.sql')->apply();
70 public static function builder() {
71 if (!isset(self
::$singletons['builder'])) {
72 $civiRoot = dirname(dirname(dirname(dirname(__FILE__
))));
73 self
::$singletons['builder'] = new CiviTesterBuilder('CiviTesterSchema');
74 self
::$singletons['builder']
75 ->callback(function ($ctx) {
76 $dbName = CiviTester
::dsn('database');
77 echo "Installing {$dbName} schema\n";
78 CiviTester
::schema()->dropAll();
80 ->sqlFile($civiRoot . "/sql/civicrm.mysql")
81 ->callback(function ($ctx) {
82 CiviTester
::data()->populate();
85 return self
::$singletons['builder'];
89 * @return \CiviTesterSchema
91 public static function schema() {
92 if (!isset(self
::$singletons['schema'])) {
93 self
::$singletons['schema'] = new CiviTesterSchema();
95 return self
::$singletons['schema'];
100 * @return \CiviTesterData
102 public static function data() {
103 if (!isset(self
::$singletons['data'])) {
104 self
::$singletons['data'] = new CiviTesterData('CiviTesterData');
106 return self
::$singletons['data'];
110 * Prepare and execute a batch of SQL statements.
112 * @param string $query
115 public static function execute($query) {
116 $pdo = CiviTester
::pdo();
118 $string = preg_replace("/^#[^\n]*$/m", "\n", $query);
119 $string = preg_replace("/^(--[^-]).*/m", "\n", $string);
121 $queries = preg_split('/;\s*$/m', $string);
122 foreach ($queries as $query) {
123 $query = trim($query);
124 if (!empty($query)) {
125 $result = $pdo->query($query);
126 if ($pdo->errorCode() == 0) {
131 var_dump($pdo->errorInfo());
132 // die( "Cannot execute $query: " . $pdo->errorInfo() );
142 * Class CiviTesterSchema
144 * Manage the entire database. This is useful for destroying or loading the schema.
146 class CiviTesterSchema
{
149 * @param string $type
150 * 'BASE TABLE' or 'VIEW'.
153 public function getTables($type) {
154 $pdo = CiviTester
::pdo();
155 // only consider real tables and not views
157 "SELECT table_name FROM INFORMATION_SCHEMA.TABLES
158 WHERE TABLE_SCHEMA = %s AND TABLE_TYPE = %s",
159 $pdo->quote(CiviTester
::dsn('database')),
162 $tables = $pdo->query($query);
164 foreach ($tables as $table) {
165 $result[] = $table['table_name'];
170 public function setStrict($checks) {
171 $dbName = CiviTester
::dsn('database');
175 "SET global innodb_flush_log_at_trx_commit = 1;",
176 "SET SQL_MODE='STRICT_ALL_TABLES';",
177 "SET foreign_key_checks = 1;",
183 "SET foreign_key_checks = 0",
184 "SET SQL_MODE='STRICT_ALL_TABLES';",
185 "SET global innodb_flush_log_at_trx_commit = 2;",
188 foreach ($queries as $query) {
189 if (CiviTester
::execute($query) === FALSE) {
190 throw new RuntimeException("Query failed: $query");
196 public function dropAll() {
198 foreach ($this->getTables('VIEW') as $table) {
199 if (preg_match('/^(civicrm_|log_)/', $table)) {
200 $queries[] = "DROP VIEW $table";
204 foreach ($this->getTables('BASE TABLE') as $table) {
205 if (preg_match('/^(civicrm_|log_)/', $table)) {
206 $queries[] = "DROP TABLE $table";
210 $this->setStrict(FALSE);
211 foreach ($queries as $query) {
212 if (CiviTester
::execute($query) === FALSE) {
213 throw new RuntimeException("dropSchema: Query failed: $query");
216 $this->setStrict(TRUE);
224 public function truncateAll() {
225 $tables = CiviTester
::schema()->getTables('BASE TABLE');
227 $truncates = array();
229 foreach ($tables as $table) {
231 if (substr($table, 0, 4) == 'log_') {
235 // don't change list of installed extensions
236 if ($table == 'civicrm_extension') {
240 if (substr($table, 0, 14) == 'civicrm_value_') {
241 $drops[] = 'DROP TABLE ' . $table . ';';
243 elseif (substr($table, 0, 9) == 'civitest_') {
247 $truncates[] = 'TRUNCATE ' . $table . ';';
251 CiviTester
::schema()->setStrict(FALSE);
252 $queries = array_merge($truncates, $drops);
253 foreach ($queries as $query) {
254 if (CiviTester
::execute($query) === FALSE) {
255 throw new RuntimeException("Query failed: $query");
258 CiviTester
::schema()->setStrict(TRUE);
266 * Class CiviTesterData
268 class CiviTesterData
{
273 public function populate() {
274 CiviTester
::schema()->truncateAll();
276 CiviTester
::schema()->setStrict(FALSE);
277 // initialize test database
278 $sql_file2 = dirname(dirname(dirname(dirname(__FILE__
)))) . "/sql/civicrm_data.mysql";
279 $sql_file3 = dirname(dirname(dirname(dirname(__FILE__
)))) . "/sql/test_data.mysql";
280 $sql_file4 = dirname(dirname(dirname(dirname(__FILE__
)))) . "/sql/test_data_second_domain.mysql";
282 $query2 = file_get_contents($sql_file2);
283 $query3 = file_get_contents($sql_file3);
284 $query4 = file_get_contents($sql_file4);
285 if (CiviTester
::execute($query2) === FALSE) {
286 throw new RuntimeException("Cannot load civicrm_data.mysql. Aborting.");
288 if (CiviTester
::execute($query3) === FALSE) {
289 throw new RuntimeException("Cannot load test_data.mysql. Aborting.");
291 if (CiviTester
::execute($query4) === FALSE) {
292 throw new RuntimeException("Cannot load test_data.mysql. Aborting.");
295 unset($query, $query2, $query3);
297 CiviTester
::schema()->setStrict(TRUE);
300 civicrm_api('system', 'flush', array('version' => 3, 'triggers' => 1));
302 CRM_Core_BAO_ConfigSetting
::setEnabledComponents(array(
317 * Class CiviTesterBuilder
319 * Provides a fluent interface for tracking a set of steps.
320 * By computing and storing a signature for the list steps, we can
321 * determine whether to (a) do nothing with the list or (b)
322 * reapply all the steps.
324 class CiviTesterBuilder
{
327 private $steps = array();
331 * A digest of the values in $steps.
333 private $targetSignature = NULL;
335 public function __construct($name) {
339 public function addStep(CiviTesterStep
$step) {
340 $this->targetSignature
= NULL;
341 $this->steps
[] = $step;
345 public function callback($callback, $signature = NULL) {
346 return $this->addStep(new CiviTesterCallbackStep($callback, $signature));
349 public function sql($sql) {
350 return $this->addStep(new CiviTesterSqlStep($sql));
353 public function sqlFile($file) {
354 return $this->addStep(new CiviTesterSqlFileStep($file));
357 protected function assertValid() {
358 foreach ($this->steps
as $step) {
359 if (!$step->isValid()) {
360 throw new RuntimeException("Found invalid step: " . var_dump($step, 1));
368 protected function getTargetSignature() {
369 if ($this->targetSignature
=== NULL) {
371 foreach ($this->steps
as $step) {
372 $buf .= $step->getSig();
374 $this->targetSignature
= md5($buf);
377 return $this->targetSignature
;
383 protected function getSavedSignature() {
384 $liveSchemaRev = NULL;
385 $pdo = CiviTester
::pdo();
386 $pdoStmt = $pdo->query(sprintf(
387 "SELECT rev FROM %s.civitest_revs WHERE name = %s",
388 CiviTester
::dsn('database'),
389 $pdo->quote($this->name
)
391 foreach ($pdoStmt as $row) {
392 $liveSchemaRev = $row['rev'];
394 return $liveSchemaRev;
398 * @param $newSignature
400 protected function setSavedSignature($newSignature) {
401 $pdo = CiviTester
::pdo();
403 'INSERT INTO %s.civitest_revs (name,rev) VALUES (%s,%s) '
404 . 'ON DUPLICATE KEY UPDATE rev = %s;',
405 CiviTester
::dsn('database'),
406 $pdo->quote($this->name
),
407 $pdo->quote($newSignature),
408 $pdo->quote($newSignature)
411 if (CiviTester
::execute($query) === FALSE) {
412 throw new RuntimeException("Failed to flag schema version: $query");
417 * Determine if the schema is correct. If necessary, destroy and recreate.
422 public function apply($force = FALSE) {
423 $dbName = CiviTester
::dsn('database');
424 $query = "USE {$dbName};"
425 . "CREATE TABLE IF NOT EXISTS civitest_revs (name VARCHAR(64) PRIMARY KEY, rev VARCHAR(64));";
427 if (CiviTester
::execute($query) === FALSE) {
428 throw new RuntimeException("Failed to flag schema version: $query");
431 $this->assertValid();
433 if (!$force && $this->getSavedSignature() === $this->getTargetSignature()) {
436 foreach ($this->steps
as $step) {
439 $this->setSavedSignature($this->getTargetSignature());
445 interface CiviTesterStep
{
446 public function getSig();
448 public function isValid();
450 public function run($ctx);
454 class CiviTesterSqlFileStep
implements CiviTesterStep
{
458 * CiviTesterSqlFileStep constructor.
461 public function __construct($file) {
466 public function getSig() {
467 return implode(' ', array(
469 filemtime($this->file
),
470 filectime($this->file
),
474 public function isValid() {
475 return is_file($this->file
) && is_readable($this->file
);
478 public function run($ctx) {
479 /** @var $ctx CiviTesterBuilder */
480 if (CiviTester
::execute(@file_get_contents
($this->file
)) === FALSE) {
481 throw new RuntimeException("Cannot load {$this->file}. Aborting.");
487 class CiviTesterSqlStep
implements CiviTesterStep
{
491 * CiviTesterSqlFileStep constructor.
494 public function __construct($sql) {
499 public function getSig() {
500 return md5($this->sql
);
503 public function isValid() {
507 public function run($ctx) {
508 /** @var $ctx CiviTesterBuilder */
509 if (CiviTester
::execute($this->sql
) === FALSE) {
510 throw new RuntimeException("Cannot execute: {$this->sql}");
516 class CiviTesterCallbackStep
implements CiviTesterStep
{
521 * CiviTesterCallbackStep constructor.
525 public function __construct($callback, $sig = NULL) {
526 $this->callback
= $callback;
527 $this->sig
= $sig === NULL ?
md5(var_export($callback, 1)) : $sig;
530 public function getSig() {
534 public function isValid() {
535 return is_callable($this->callback
);
538 public function run($ctx) {
539 call_user_func($this->callback
, $ctx);