CRM-17957 - Extension classloader - Reset whenever the mapper resets
[civicrm-core.git] / tests / phpunit / CiviTest / CiviTester.php
1 <?php
2
3 class CiviTester {
4
5 /**
6 * @var array
7 */
8 private static $singletons = array();
9
10 /**
11 * Get the data source used for testing.
12 *
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.
18 */
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);
23 }
24
25 if ($part === NULL) {
26 return self::$singletons['dsn'];
27 }
28
29 if (isset(self::$singletons['dsn'][$part])) {
30 return self::$singletons['dsn'][$part];
31 }
32
33 return NULL;
34 }
35
36 /**
37 * Get a connection to the test database.
38 *
39 * @return PDO
40 */
41 public static function pdo() {
42 if (!isset(self::$singletons['pdo'])) {
43 $dsninfo = self::dsn();
44 $host = $dsninfo['hostspec'];
45 $port = @$dsninfo['port'];
46 try {
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)
50 );
51 }
52 catch (PDOException$e) {
53 echo "Can't connect to MySQL server:" . PHP_EOL . $e->getMessage() . PHP_EOL;
54 exit(1);
55 }
56 }
57 return self::$singletons['pdo'];
58 }
59
60 /**
61 * Get the schema manager.
62 *
63 * @return \CiviTesterBuilder
64 *
65 * @code
66 * CiviTester::headless()->apply();
67 * CiviTester::headless()->sqlFile('ex.sql')->apply();
68 * @endCode
69 */
70 public static function headless() {
71 $civiRoot = dirname(dirname(dirname(dirname(__FILE__))));
72 $builder = new CiviTesterBuilder('CiviTesterSchema');
73 $builder
74 ->callback(function ($ctx) {
75 if (CIVICRM_UF !== 'UnitTests') {
76 throw new \RuntimeException("CiviTester::headless() requires CIVICRM_UF=UnitTests");
77 }
78 $dbName = CiviTester::dsn('database');
79 echo "Installing {$dbName} schema\n";
80 CiviTester::schema()->dropAll();
81 }, 'msg-drop')
82 ->sqlFile($civiRoot . "/sql/civicrm.mysql")
83 ->sql("DELETE FROM civicrm_extension")
84 ->callback(function ($ctx) {
85 CiviTester::data()->populate();
86 }, 'populate');
87 return $builder;
88 }
89
90 /**
91 * @return \CiviTesterSchema
92 */
93 public static function schema() {
94 if (!isset(self::$singletons['schema'])) {
95 self::$singletons['schema'] = new CiviTesterSchema();
96 }
97 return self::$singletons['schema'];
98 }
99
100
101 /**
102 * @return \CiviTesterData
103 */
104 public static function data() {
105 if (!isset(self::$singletons['data'])) {
106 self::$singletons['data'] = new CiviTesterData('CiviTesterData');
107 }
108 return self::$singletons['data'];
109 }
110
111 /**
112 * Prepare and execute a batch of SQL statements.
113 *
114 * @param string $query
115 * @return bool
116 */
117 public static function execute($query) {
118 $pdo = CiviTester::pdo();
119
120 $string = preg_replace("/^#[^\n]*$/m", "\n", $query);
121 $string = preg_replace("/^(--[^-]).*/m", "\n", $string);
122
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) {
129 continue;
130 }
131 else {
132 var_dump($result);
133 var_dump($pdo->errorInfo());
134 // die( "Cannot execute $query: " . $pdo->errorInfo() );
135 }
136 }
137 }
138 return TRUE;
139 }
140
141 }
142
143 /**
144 * Class CiviTesterSchema
145 *
146 * Manage the entire database. This is useful for destroying or loading the schema.
147 */
148 class CiviTesterSchema {
149
150 /**
151 * @param string $type
152 * 'BASE TABLE' or 'VIEW'.
153 * @return array
154 */
155 public function getTables($type) {
156 $pdo = CiviTester::pdo();
157 // only consider real tables and not views
158 $query = sprintf(
159 "SELECT table_name FROM INFORMATION_SCHEMA.TABLES
160 WHERE TABLE_SCHEMA = %s AND TABLE_TYPE = %s",
161 $pdo->quote(CiviTester::dsn('database')),
162 $pdo->quote($type)
163 );
164 $tables = $pdo->query($query);
165 $result = array();
166 foreach ($tables as $table) {
167 $result[] = $table['table_name'];
168 }
169 return $result;
170 }
171
172 public function setStrict($checks) {
173 $dbName = CiviTester::dsn('database');
174 if ($checks) {
175 $queries = array(
176 "USE {$dbName};",
177 "SET global innodb_flush_log_at_trx_commit = 1;",
178 "SET SQL_MODE='STRICT_ALL_TABLES';",
179 "SET foreign_key_checks = 1;",
180 );
181 }
182 else {
183 $queries = array(
184 "USE {$dbName};",
185 "SET foreign_key_checks = 0",
186 "SET SQL_MODE='STRICT_ALL_TABLES';",
187 "SET global innodb_flush_log_at_trx_commit = 2;",
188 );
189 }
190 foreach ($queries as $query) {
191 if (CiviTester::execute($query) === FALSE) {
192 throw new RuntimeException("Query failed: $query");
193 }
194 }
195 return $this;
196 }
197
198 public function dropAll() {
199 $queries = array();
200 foreach ($this->getTables('VIEW') as $table) {
201 if (preg_match('/^(civicrm_|log_)/', $table)) {
202 $queries[] = "DROP VIEW $table";
203 }
204 }
205
206 foreach ($this->getTables('BASE TABLE') as $table) {
207 if (preg_match('/^(civicrm_|log_)/', $table)) {
208 $queries[] = "DROP TABLE $table";
209 }
210 }
211
212 $this->setStrict(FALSE);
213 foreach ($queries as $query) {
214 if (CiviTester::execute($query) === FALSE) {
215 throw new RuntimeException("dropSchema: Query failed: $query");
216 }
217 }
218 $this->setStrict(TRUE);
219
220 return $this;
221 }
222
223 /**
224 * @return array
225 */
226 public function truncateAll() {
227 $tables = CiviTester::schema()->getTables('BASE TABLE');
228
229 $truncates = array();
230 $drops = array();
231 foreach ($tables as $table) {
232 // skip log tables
233 if (substr($table, 0, 4) == 'log_') {
234 continue;
235 }
236
237 // don't change list of installed extensions
238 if ($table == 'civicrm_extension') {
239 continue;
240 }
241
242 if (substr($table, 0, 14) == 'civicrm_value_') {
243 $drops[] = 'DROP TABLE ' . $table . ';';
244 }
245 elseif (substr($table, 0, 9) == 'civitest_') {
246 // ignore
247 }
248 else {
249 $truncates[] = 'TRUNCATE ' . $table . ';';
250 }
251 }
252
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");
258 }
259 }
260 CiviTester::schema()->setStrict(TRUE);
261
262 return $this;
263 }
264
265 }
266
267 /**
268 * Class CiviTesterData
269 */
270 class CiviTesterData {
271
272 /**
273 * @return bool
274 */
275 public function populate() {
276 CiviTester::schema()->truncateAll();
277
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";
283
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.");
289 }
290 if (CiviTester::execute($query3) === FALSE) {
291 throw new RuntimeException("Cannot load test_data.mysql. Aborting.");
292 }
293 if (CiviTester::execute($query4) === FALSE) {
294 throw new RuntimeException("Cannot load test_data.mysql. Aborting.");
295 }
296
297 unset($query, $query2, $query3);
298
299 CiviTester::schema()->setStrict(TRUE);
300
301 // Rebuild triggers
302 civicrm_api('system', 'flush', array('version' => 3, 'triggers' => 1));
303
304 CRM_Core_BAO_ConfigSetting::setEnabledComponents(array(
305 'CiviEvent',
306 'CiviContribute',
307 'CiviMember',
308 'CiviMail',
309 'CiviReport',
310 'CiviPledge',
311 ));
312
313 return TRUE;
314 }
315
316 }
317
318 /**
319 * Class CiviTesterBuilder
320 *
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.
325 */
326 class CiviTesterBuilder {
327 protected $name;
328
329 private $steps = array();
330
331 /**
332 * @var string|NULL
333 * A digest of the values in $steps.
334 */
335 private $targetSignature = NULL;
336
337 public function __construct($name) {
338 $this->name = $name;
339 }
340
341 public function addStep(CiviTesterStep $step) {
342 $this->targetSignature = NULL;
343 $this->steps[] = $step;
344 return $this;
345 }
346
347 public function callback($callback, $signature = NULL) {
348 return $this->addStep(new CiviTesterCallbackStep($callback, $signature));
349 }
350
351 public function sql($sql) {
352 return $this->addStep(new CiviTesterSqlStep($sql));
353 }
354
355 public function sqlFile($file) {
356 return $this->addStep(new CiviTesterSqlFileStep($file));
357 }
358
359 /**
360 * Require an extension (based on its name).
361 *
362 * @param string $name
363 * @return \CiviTesterBuilder
364 */
365 public function ext($name) {
366 return $this->addStep(new CiviTesterExtensionStep($name));
367 }
368
369 /**
370 * Require an extension (based on its directory).
371 *
372 * @param $dir
373 * @return \CiviTesterBuilder
374 * @throws \CRM_Extension_Exception_ParseException
375 */
376 public function extDir($dir) {
377 while ($dir && dirname($dir) !== $dir && !file_exists("$dir/info.xml")) {
378 $dir = dirname($dir);
379 }
380 if (file_exists("$dir/info.xml")) {
381 $info = CRM_Extension_Info::loadFromFile("$dir/info.xml");
382 $name = $info->key;
383 }
384 return $this->addStep(new CiviTesterExtensionStep($name));
385 }
386
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));
391 }
392 }
393 }
394
395 /**
396 * @return string
397 */
398 protected function getTargetSignature() {
399 if ($this->targetSignature === NULL) {
400 $buf = '';
401 foreach ($this->steps as $step) {
402 $buf .= $step->getSig();
403 }
404 $this->targetSignature = md5($buf);
405 }
406
407 return $this->targetSignature;
408 }
409
410 /**
411 * @return string
412 */
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)
420 ));
421 foreach ($pdoStmt as $row) {
422 $liveSchemaRev = $row['rev'];
423 }
424 return $liveSchemaRev;
425 }
426
427 /**
428 * @param $newSignature
429 */
430 protected function setSavedSignature($newSignature) {
431 $pdo = CiviTester::pdo();
432 $query = sprintf(
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)
439 );
440
441 if (CiviTester::execute($query) === FALSE) {
442 throw new RuntimeException("Failed to flag schema version: $query");
443 }
444 }
445
446 /**
447 * Determine if the schema is correct. If necessary, destroy and recreate.
448 *
449 * @param bool $force
450 * @return $this
451 */
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));";
456
457 if (CiviTester::execute($query) === FALSE) {
458 throw new RuntimeException("Failed to flag schema version: $query");
459 }
460
461 $this->assertValid();
462
463 if (!$force && $this->getSavedSignature() === $this->getTargetSignature()) {
464 return $this;
465 }
466 foreach ($this->steps as $step) {
467 $step->run($this);
468 }
469 $this->setSavedSignature($this->getTargetSignature());
470 return $this;
471 }
472
473 }
474
475 interface CiviTesterStep {
476 public function getSig();
477
478 public function isValid();
479
480 public function run($ctx);
481
482 }
483
484 class CiviTesterSqlFileStep implements CiviTesterStep {
485 private $file;
486
487 /**
488 * CiviTesterSqlFileStep constructor.
489 * @param $file
490 */
491 public function __construct($file) {
492 $this->file = $file;
493 }
494
495
496 public function getSig() {
497 return implode(' ', array(
498 $this->file,
499 filemtime($this->file),
500 filectime($this->file),
501 ));
502 }
503
504 public function isValid() {
505 return is_file($this->file) && is_readable($this->file);
506 }
507
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.");
512 }
513 }
514
515 }
516
517 class CiviTesterSqlStep implements CiviTesterStep {
518 private $sql;
519
520 /**
521 * CiviTesterSqlFileStep constructor.
522 * @param $sql
523 */
524 public function __construct($sql) {
525 $this->sql = $sql;
526 }
527
528
529 public function getSig() {
530 return md5($this->sql);
531 }
532
533 public function isValid() {
534 return TRUE;
535 }
536
537 public function run($ctx) {
538 /** @var $ctx CiviTesterBuilder */
539 if (CiviTester::execute($this->sql) === FALSE) {
540 throw new RuntimeException("Cannot execute: {$this->sql}");
541 }
542 }
543
544 }
545
546 class CiviTesterCallbackStep implements CiviTesterStep {
547 private $callback;
548 private $sig;
549
550 /**
551 * CiviTesterCallbackStep constructor.
552 * @param $callback
553 * @param $sig
554 */
555 public function __construct($callback, $sig = NULL) {
556 $this->callback = $callback;
557 $this->sig = $sig === NULL ? md5(var_export($callback, 1)) : $sig;
558 }
559
560 public function getSig() {
561 return $this->sig;
562 }
563
564 public function isValid() {
565 return is_callable($this->callback);
566 }
567
568 public function run($ctx) {
569 call_user_func($this->callback, $ctx);
570 }
571
572 }
573
574 class CiviTesterExtensionStep implements CiviTesterStep {
575 private $name;
576
577 /**
578 * CiviTesterExtensionStep constructor.
579 * @param $name
580 */
581 public function __construct($name) {
582 $this->name = $name;
583 }
584
585 public function getSig() {
586 return 'ext:' . $this->name;
587 }
588
589 public function isValid() {
590 return is_string($this->name);
591 }
592
593 public function run($ctx) {
594 CRM_Extension_System::singleton()->getManager()->install(array(
595 $this->name,
596 ));
597 }
598
599 }