Merge pull request #7737 from totten/master-cv-env
[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::builder()->apply();
67 * CiviTester::builder()->sqlFile('ex.sql')->apply();
68 * @endCode
69 */
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();
79 }, 'msg-drop')
80 ->sqlFile($civiRoot . "/sql/civicrm.mysql")
81 ->callback(function ($ctx) {
82 CiviTester::data()->populate();
83 }, 'populate');
84 }
85 return self::$singletons['builder'];
86 }
87
88 /**
89 * @return \CiviTesterSchema
90 */
91 public static function schema() {
92 if (!isset(self::$singletons['schema'])) {
93 self::$singletons['schema'] = new CiviTesterSchema();
94 }
95 return self::$singletons['schema'];
96 }
97
98
99 /**
100 * @return \CiviTesterData
101 */
102 public static function data() {
103 if (!isset(self::$singletons['data'])) {
104 self::$singletons['data'] = new CiviTesterData('CiviTesterData');
105 }
106 return self::$singletons['data'];
107 }
108
109 /**
110 * Prepare and execute a batch of SQL statements.
111 *
112 * @param string $query
113 * @return bool
114 */
115 public static function execute($query) {
116 $pdo = CiviTester::pdo();
117
118 $string = preg_replace("/^#[^\n]*$/m", "\n", $query);
119 $string = preg_replace("/^(--[^-]).*/m", "\n", $string);
120
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) {
127 continue;
128 }
129 else {
130 var_dump($result);
131 var_dump($pdo->errorInfo());
132 // die( "Cannot execute $query: " . $pdo->errorInfo() );
133 }
134 }
135 }
136 return TRUE;
137 }
138
139 }
140
141 /**
142 * Class CiviTesterSchema
143 *
144 * Manage the entire database. This is useful for destroying or loading the schema.
145 */
146 class CiviTesterSchema {
147
148 /**
149 * @param string $type
150 * 'BASE TABLE' or 'VIEW'.
151 * @return array
152 */
153 public function getTables($type) {
154 $pdo = CiviTester::pdo();
155 // only consider real tables and not views
156 $query = sprintf(
157 "SELECT table_name FROM INFORMATION_SCHEMA.TABLES
158 WHERE TABLE_SCHEMA = %s AND TABLE_TYPE = %s",
159 $pdo->quote(CiviTester::dsn('database')),
160 $pdo->quote($type)
161 );
162 $tables = $pdo->query($query);
163 $result = array();
164 foreach ($tables as $table) {
165 $result[] = $table['table_name'];
166 }
167 return $result;
168 }
169
170 public function setStrict($checks) {
171 $dbName = CiviTester::dsn('database');
172 if ($checks) {
173 $queries = array(
174 "USE {$dbName};",
175 "SET global innodb_flush_log_at_trx_commit = 1;",
176 "SET SQL_MODE='STRICT_ALL_TABLES';",
177 "SET foreign_key_checks = 1;",
178 );
179 }
180 else {
181 $queries = array(
182 "USE {$dbName};",
183 "SET foreign_key_checks = 0",
184 "SET SQL_MODE='STRICT_ALL_TABLES';",
185 "SET global innodb_flush_log_at_trx_commit = 2;",
186 );
187 }
188 foreach ($queries as $query) {
189 if (CiviTester::execute($query) === FALSE) {
190 throw new RuntimeException("Query failed: $query");
191 }
192 }
193 return $this;
194 }
195
196 public function dropAll() {
197 $queries = array();
198 foreach ($this->getTables('VIEW') as $table) {
199 if (preg_match('/^(civicrm_|log_)/', $table)) {
200 $queries[] = "DROP VIEW $table";
201 }
202 }
203
204 foreach ($this->getTables('BASE TABLE') as $table) {
205 if (preg_match('/^(civicrm_|log_)/', $table)) {
206 $queries[] = "DROP TABLE $table";
207 }
208 }
209
210 $this->setStrict(FALSE);
211 foreach ($queries as $query) {
212 if (CiviTester::execute($query) === FALSE) {
213 throw new RuntimeException("dropSchema: Query failed: $query");
214 }
215 }
216 $this->setStrict(TRUE);
217
218 return $this;
219 }
220
221 /**
222 * @return array
223 */
224 public function truncateAll() {
225 $tables = CiviTester::schema()->getTables('BASE TABLE');
226
227 $truncates = array();
228 $drops = array();
229 foreach ($tables as $table) {
230 // skip log tables
231 if (substr($table, 0, 4) == 'log_') {
232 continue;
233 }
234
235 // don't change list of installed extensions
236 if ($table == 'civicrm_extension') {
237 continue;
238 }
239
240 if (substr($table, 0, 14) == 'civicrm_value_') {
241 $drops[] = 'DROP TABLE ' . $table . ';';
242 }
243 elseif (substr($table, 0, 9) == 'civitest_') {
244 // ignore
245 }
246 else {
247 $truncates[] = 'TRUNCATE ' . $table . ';';
248 }
249 }
250
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");
256 }
257 }
258 CiviTester::schema()->setStrict(TRUE);
259
260 return $this;
261 }
262
263 }
264
265 /**
266 * Class CiviTesterData
267 */
268 class CiviTesterData {
269
270 /**
271 * @return bool
272 */
273 public function populate() {
274 CiviTester::schema()->truncateAll();
275
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";
281
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.");
287 }
288 if (CiviTester::execute($query3) === FALSE) {
289 throw new RuntimeException("Cannot load test_data.mysql. Aborting.");
290 }
291 if (CiviTester::execute($query4) === FALSE) {
292 throw new RuntimeException("Cannot load test_data.mysql. Aborting.");
293 }
294
295 unset($query, $query2, $query3);
296
297 CiviTester::schema()->setStrict(TRUE);
298
299 // Rebuild triggers
300 civicrm_api('system', 'flush', array('version' => 3, 'triggers' => 1));
301
302 CRM_Core_BAO_ConfigSetting::setEnabledComponents(array(
303 'CiviEvent',
304 'CiviContribute',
305 'CiviMember',
306 'CiviMail',
307 'CiviReport',
308 'CiviPledge',
309 ));
310
311 return TRUE;
312 }
313
314 }
315
316 /**
317 * Class CiviTesterBuilder
318 *
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.
323 */
324 class CiviTesterBuilder {
325 protected $name;
326
327 private $steps = array();
328
329 /**
330 * @var string|NULL
331 * A digest of the values in $steps.
332 */
333 private $targetSignature = NULL;
334
335 public function __construct($name) {
336 $this->name = $name;
337 }
338
339 public function addStep(CiviTesterStep $step) {
340 $this->targetSignature = NULL;
341 $this->steps[] = $step;
342 return $this;
343 }
344
345 public function callback($callback, $signature = NULL) {
346 return $this->addStep(new CiviTesterCallbackStep($callback, $signature));
347 }
348
349 public function sql($sql) {
350 return $this->addStep(new CiviTesterSqlStep($sql));
351 }
352
353 public function sqlFile($file) {
354 return $this->addStep(new CiviTesterSqlFileStep($file));
355 }
356
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));
361 }
362 }
363 }
364
365 /**
366 * @return string
367 */
368 protected function getTargetSignature() {
369 if ($this->targetSignature === NULL) {
370 $buf = '';
371 foreach ($this->steps as $step) {
372 $buf .= $step->getSig();
373 }
374 $this->targetSignature = md5($buf);
375 }
376
377 return $this->targetSignature;
378 }
379
380 /**
381 * @return string
382 */
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)
390 ));
391 foreach ($pdoStmt as $row) {
392 $liveSchemaRev = $row['rev'];
393 }
394 return $liveSchemaRev;
395 }
396
397 /**
398 * @param $newSignature
399 */
400 protected function setSavedSignature($newSignature) {
401 $pdo = CiviTester::pdo();
402 $query = sprintf(
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)
409 );
410
411 if (CiviTester::execute($query) === FALSE) {
412 throw new RuntimeException("Failed to flag schema version: $query");
413 }
414 }
415
416 /**
417 * Determine if the schema is correct. If necessary, destroy and recreate.
418 *
419 * @param bool $force
420 * @return $this
421 */
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));";
426
427 if (CiviTester::execute($query) === FALSE) {
428 throw new RuntimeException("Failed to flag schema version: $query");
429 }
430
431 $this->assertValid();
432
433 if (!$force && $this->getSavedSignature() === $this->getTargetSignature()) {
434 return $this;
435 }
436 foreach ($this->steps as $step) {
437 $step->run($this);
438 }
439 $this->setSavedSignature($this->getTargetSignature());
440 return $this;
441 }
442
443 }
444
445 interface CiviTesterStep {
446 public function getSig();
447
448 public function isValid();
449
450 public function run($ctx);
451
452 }
453
454 class CiviTesterSqlFileStep implements CiviTesterStep {
455 private $file;
456
457 /**
458 * CiviTesterSqlFileStep constructor.
459 * @param $file
460 */
461 public function __construct($file) {
462 $this->file = $file;
463 }
464
465
466 public function getSig() {
467 return implode(' ', array(
468 $this->file,
469 filemtime($this->file),
470 filectime($this->file),
471 ));
472 }
473
474 public function isValid() {
475 return is_file($this->file) && is_readable($this->file);
476 }
477
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.");
482 }
483 }
484
485 }
486
487 class CiviTesterSqlStep implements CiviTesterStep {
488 private $sql;
489
490 /**
491 * CiviTesterSqlFileStep constructor.
492 * @param $sql
493 */
494 public function __construct($sql) {
495 $this->sql = $sql;
496 }
497
498
499 public function getSig() {
500 return md5($this->sql);
501 }
502
503 public function isValid() {
504 return TRUE;
505 }
506
507 public function run($ctx) {
508 /** @var $ctx CiviTesterBuilder */
509 if (CiviTester::execute($this->sql) === FALSE) {
510 throw new RuntimeException("Cannot execute: {$this->sql}");
511 }
512 }
513
514 }
515
516 class CiviTesterCallbackStep implements CiviTesterStep {
517 private $callback;
518 private $sig;
519
520 /**
521 * CiviTesterCallbackStep constructor.
522 * @param $callback
523 * @param $sig
524 */
525 public function __construct($callback, $sig = NULL) {
526 $this->callback = $callback;
527 $this->sig = $sig === NULL ? md5(var_export($callback, 1)) : $sig;
528 }
529
530 public function getSig() {
531 return $this->sig;
532 }
533
534 public function isValid() {
535 return is_callable($this->callback);
536 }
537
538 public function run($ctx) {
539 call_user_func($this->callback, $ctx);
540 }
541
542 }