CRM-17860 - Replace CiviTestPdoUtils, CiviTestDB with CiviTester
authorTim Otten <totten@civicrm.org>
Sat, 30 Jan 2016 01:55:46 +0000 (17:55 -0800)
committerTim Otten <totten@civicrm.org>
Tue, 2 Feb 2016 07:33:51 +0000 (00:33 -0700)
CRM/Core/ClassLoader.php
tests/phpunit/CiviTest/CiviTestDB.php [deleted file]
tests/phpunit/CiviTest/CiviTestPdoUtils.php [deleted file]
tests/phpunit/CiviTest/CiviTester.php [new file with mode: 0644]
tests/phpunit/CiviTest/CiviUnitTestCase.php
tests/phpunit/CiviTest/bootstrap.php

index d78c070421bd4723fdd579ac1ac1ca34418d783c..c32ef990009c0ad34b9e27b7f2dd4af59e461ffe 100644 (file)
@@ -77,8 +77,7 @@ class CRM_Core_ClassLoader {
       'CiviMailUtils',
       'CiviReportTestCase',
       'CiviSeleniumTestCase',
-      'CiviTestDB',
-      'CiviTestPdoUtils',
+      'CiviTester',
       'CiviTestSuite',
       'CiviUnitTestCase',
       'Contact',
diff --git a/tests/phpunit/CiviTest/CiviTestDB.php b/tests/phpunit/CiviTest/CiviTestDB.php
deleted file mode 100644 (file)
index 5b688c9..0000000
+++ /dev/null
@@ -1,219 +0,0 @@
-<?php
-
-class CiviTestDB {
-
-  /**
-   * @var CiviTestPdoUtils
-   */
-  protected $pdoUtils;
-
-  /**
-   * CiviTestDB constructor.
-   * @param CiviTestPdoUtils $pdoUtils
-   */
-  public function __construct($pdoUtils) {
-    $this->pdoUtils = $pdoUtils;
-  }
-
-  /**
-   * Autocreate the schema.
-   *
-   * Check civitest_schema_rev. If it's out-of-date, drop the old schema and load the
-   * new one.
-   *
-   * @return mixed
-   */
-  public function updateSchema() {
-    $schemaFile = dirname(dirname(dirname(dirname(__FILE__)))) . "/sql/civicrm.mysql";
-    if (!is_file($schemaFile)) {
-      return $this->fatal("Failed to find schema: $schemaFile");
-    }
-
-    // $schemaFileRev = md5(@file_get_contents($schemaFile));
-    $schemaFileRev = filemtime($schemaFile) . ' ' . filectime($schemaFile);
-
-    $tables = $this->getCurrentTables('BASE TABLE');
-    $liveSchemaRev = '';
-    if (in_array('civitest_schema_rev', $tables)) {
-      $pdoStmt = $this->pdoUtils->pdo->query("SELECT schema_rev FROM {$this->pdoUtils->dbName}.civitest_schema_rev");
-      foreach ($pdoStmt as $row) {
-        $liveSchemaRev = $row['schema_rev'];
-      }
-    }
-
-    if ($liveSchemaRev === $schemaFileRev) {
-      return;
-    }
-
-    echo "Installing {$this->pdoUtils->dbName} schema\n";
-    $this->dropSchema();
-    if ($this->pdoUtils->do_query(@file_get_contents($schemaFile)) === FALSE) {
-      return $this->fatal("Cannot load $schemaFile. Aborting.");
-    }
-    $query = sprintf(
-      "USE {$this->pdoUtils->dbName};"
-      . "DROP TABLE IF EXISTS civitest_schema_rev;"
-      . "CREATE TABLE civitest_schema_rev (schema_rev VARCHAR(64));"
-      . "INSERT INTO civitest_schema_rev (schema_rev) VALUES (%s);",
-      $this->pdoUtils->pdo->quote($schemaFileRev)
-    );
-
-    if ($this->pdoUtils->do_query($query) === FALSE) {
-      return $this->fatal("Failed to flag schema version: $query");
-    }
-  }
-
-  public function dropSchema() {
-    $queries = array(
-      "USE {$this->pdoUtils->dbName};",
-      "SET foreign_key_checks = 0",
-      // SQL mode needs to be strict, that's our standard
-      "SET SQL_MODE='STRICT_ALL_TABLES';",
-      "SET global innodb_flush_log_at_trx_commit = 2;",
-    );
-
-    foreach ($this->getCurrentTables('VIEW') as $table) {
-      if (preg_match('/^(civicrm_|log_)/', $table)) {
-        $queries[] = "DROP VIEW $table";
-      }
-    }
-
-    foreach ($this->getCurrentTables('BASE TABLE') as $table) {
-      if (preg_match('/^(civicrm_|log_)/', $table)) {
-        $queries[] = "DROP TABLE $table";
-      }
-    }
-
-    $queries[] = "set global innodb_flush_log_at_trx_commit = 1;";
-    $queries[] = "SET foreign_key_checks = 1";
-
-    foreach ($queries as $query) {
-      if ($this->pdoUtils->do_query($query) === FALSE) {
-        return $this->fatal("dropSchema: Query failed: $query");
-      }
-    }
-  }
-
-  /**
-   * @return bool
-   */
-  public function populate() {
-    $pdoUtils = $this->pdoUtils;
-    $tables = $this->getCurrentTables('BASE TABLE');
-
-    $truncates = array();
-    $drops = array();
-    foreach ($tables as $table) {
-      // skip log tables
-      if (substr($table, 0, 4) == 'log_') {
-        continue;
-      }
-
-      // don't change list of installed extensions
-      if ($table == 'civicrm_extension') {
-        continue;
-      }
-
-      if (substr($table, 0, 14) == 'civicrm_value_') {
-        $drops[] = 'DROP TABLE ' . $table . ';';
-      }
-      elseif (substr($table, 0, 9) == 'civitest_') {
-        // ignore
-      }
-      else {
-        $truncates[] = 'TRUNCATE ' . $table . ';';
-      }
-    }
-
-    $dbName = $pdoUtils->dbName;
-    $queries = array(
-      "USE {$dbName};",
-      "SET foreign_key_checks = 0",
-      // SQL mode needs to be strict, that's our standard
-      "SET SQL_MODE='STRICT_ALL_TABLES';",
-      "SET global innodb_flush_log_at_trx_commit = 2;",
-    );
-    $queries = array_merge($queries, $truncates);
-    $queries = array_merge($queries, $drops);
-    foreach ($queries as $query) {
-      if ($pdoUtils->do_query($query) === FALSE) {
-        return $this->fatal("Query failed: $query");
-      }
-    }
-
-    //  initialize test database
-    $sql_file2 = dirname(dirname(dirname(dirname(__FILE__)))) . "/sql/civicrm_data.mysql";
-    $sql_file3 = dirname(dirname(dirname(dirname(__FILE__)))) . "/sql/test_data.mysql";
-    $sql_file4 = dirname(dirname(dirname(dirname(__FILE__)))) . "/sql/test_data_second_domain.mysql";
-
-    $query2 = file_get_contents($sql_file2);
-    $query3 = file_get_contents($sql_file3);
-    $query4 = file_get_contents($sql_file4);
-    if ($pdoUtils->do_query($query2) === FALSE) {
-      return $this->fatal("Cannot load civicrm_data.mysql. Aborting.");
-    }
-    if ($pdoUtils->do_query($query3) === FALSE) {
-      return $this->fatal("Cannot load test_data.mysql. Aborting.");
-    }
-    if ($pdoUtils->do_query($query4) === FALSE) {
-      return $this->fatal("Cannot load test_data.mysql. Aborting.");
-    }
-
-    // done with all the loading, get transactions back
-    if ($pdoUtils->do_query("set global innodb_flush_log_at_trx_commit = 1;") === FALSE) {
-      return $this->fatal("Cannot set global? Huh?");
-    }
-
-    if ($pdoUtils->do_query("SET foreign_key_checks = 1") === FALSE) {
-      return $this->fatal("Cannot get foreign keys back? Huh?");
-    }
-
-    unset($query, $query2, $query3);
-
-    // Rebuild triggers
-    civicrm_api('system', 'flush', array('version' => 3, 'triggers' => 1));
-
-    CRM_Core_BAO_ConfigSetting::setEnabledComponents(array(
-      'CiviEvent',
-      'CiviContribute',
-      'CiviMember',
-      'CiviMail',
-      'CiviReport',
-      'CiviPledge',
-    ));
-
-    return TRUE;
-  }
-
-  /**
-   * @param $message
-   * @return mixed
-   */
-  protected function fatal($message) {
-    echo "$message\n";
-    exit(1);
-  }
-
-  /**
-   * @param string $type
-   *   'BASE TABLE' or 'VIEW'.
-   * @return array
-   */
-  protected function getCurrentTables($type) {
-    $pdo = $this->pdoUtils->pdo;
-    // only consider real tables and not views
-    $query = sprintf(
-      "SELECT table_name FROM INFORMATION_SCHEMA.TABLES
-        WHERE TABLE_SCHEMA = %s AND TABLE_TYPE = %s",
-      $pdo->quote($this->pdoUtils->dbName),
-      $pdo->quote($type)
-    );
-    $tables = $pdo->query($query);
-    $result = array();
-    foreach ($tables as $table) {
-      $result[] = $table['table_name'];
-    }
-    return $result;
-  }
-
-}
diff --git a/tests/phpunit/CiviTest/CiviTestPdoUtils.php b/tests/phpunit/CiviTest/CiviTestPdoUtils.php
deleted file mode 100644 (file)
index 9dcbcbd..0000000
+++ /dev/null
@@ -1,152 +0,0 @@
-<?php
-// vim: set si ai expandtab tabstop=4 shiftwidth=4 softtabstop=4:
-
-/**
- *  File for the Utils class
- *
- *  (PHP 5)
- *
- * @author Walt Haas <walt@dharmatech.org> (801) 534-1262
- * @copyright Copyright CiviCRM LLC (C) 2009
- * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html
- *              GNU Affero General Public License version 3
- * @version   $Id: Utils.php 40328 2012-05-11 23:06:13Z allen $
- * @package   CiviCRM
- *
- *   This file is part of CiviCRM
- *
- *   CiviCRM is free software; you can redistribute it and/or
- *   modify it under the terms of the GNU Affero General Public License
- *   as published by the Free Software Foundation; either version 3 of
- *   the License, or (at your option) any later version.
- *
- *   CiviCRM is distributed in the hope that it will be useful,
- *   but WITHOUT ANY WARRANTY; without even the implied warranty of
- *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *   GNU Affero General Public License for more details.
- *
- *   You should have received a copy of the GNU Affero General Public
- *   License along with this program.  If not, see
- *   <http://www.gnu.org/licenses/>.
- */
-
-/**
- *  Utility functions
- * @package   CiviCRM
- */
-class CiviTestPdoUtils {
-
-  /**
-   *  PDO for the database.
-   * @var PDO
-   */
-  public $pdo;
-
-  public $dbName;
-
-  /**
-   *  Construct an object for this database.
-   */
-  public function __construct($dsn) {
-    require_once "DB.php";
-    $dsninfo = DB::parseDSN($dsn);
-    $host = $dsninfo['hostspec'];
-    $port = @$dsninfo['port'];
-    $user = $dsninfo['username'];
-    $pass = $dsninfo['password'];
-    $this->dbName = $dsninfo['database'];
-
-    try {
-      $this->pdo = new PDO("mysql:host={$host}" . ($port ? ";port=$port" : ""),
-        $user, $pass,
-        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);
-    }
-  }
-
-  /**
-   * Prepare and execute a query.
-   *
-   * If the query fails, output a diagnostic message
-   * @param string $query
-   *   Query to run
-   * @return bool
-   */
-  public function do_query($query) {
-    // echo "do_query($query)\n";
-    // $stmt = $this->pdo->query( $query, PDO::FETCH_ASSOC );
-    // echo "PDO returned";
-    // var_dump($stmt);
-    $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 = $this->pdo->query($query);
-        if ($this->pdo->errorCode() == 0) {
-          continue;
-        }
-        else {
-          var_dump($result);
-          var_dump($this->pdo->errorInfo());
-          // die( "Cannot execute $query: " . $this->pdo->errorInfo() );
-        }
-      }
-    }
-
-    /*******
-     * if ( $this->pdo->errorCode() == 0 ) {
-     * //echo "returning the PDOStmt\n";
-     * return $stmt;
-     * }
-     *
-     * //  operation failed, so output description of where and why
-     * $errorInfo = $this->pdo->errorInfo();
-     * echo "Oops, can't do query:\n    {$query}\n    in "
-     * . basename( __FILE__) . " line " . __LINE__.":\n    "
-     * . $errorInfo[0] . ": " . $errorInfo[2] . "\n    Call stack:\n";
-     * $backtrace = debug_backtrace();
-     * $dir_name  = dirname( __FILE__ );
-     * $cwd_len   = strlen( $dir_name ) + 1;
-     * foreach ($backtrace as $frame ) {
-     * echo "      ";
-     * if ( array_key_exists( 'class', $frame ) ) {
-     * echo " class {$frame['class']}";
-     * if ( array_key_exists( 'function', $frame ) ) {
-     * echo " method {$frame['function']}";
-     * }
-     * }
-     * else {
-     * if ( array_key_exists( 'function', $frame ) ) {
-     * echo " function {$frame['function']}";
-     * }
-     * }
-     * if ( array_key_exists( 'file', $frame ) ) {
-     * echo " file ". substr( $frame['file'], $cwd_len );
-     * }
-     * if ( array_key_exists( 'line', $frame ) ) {
-     * echo " line {$frame['line']}";
-     * }
-     * echo "\n";
-     * }
-     ******/
-    return TRUE;
-  }
-
-}
-// class Utils
-
-// -- set Emacs parameters --
-// Local variables:
-// mode: php;
-// tab-width: 4
-// c-basic-offset: 4
-// c-hanging-comment-ender-p: nil
-// indent-tabs-mode: nil
-// End:
diff --git a/tests/phpunit/CiviTest/CiviTester.php b/tests/phpunit/CiviTest/CiviTester.php
new file mode 100644 (file)
index 0000000..d5266a7
--- /dev/null
@@ -0,0 +1,542 @@
+<?php
+
+class CiviTester {
+
+  /**
+   * @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'];
+  }
+
+  /**
+   * Get the schema manager.
+   *
+   * @return \CiviTesterBuilder
+   *
+   * @code
+   * CiviTester::builder()->apply();
+   * CiviTester::builder()->sqlFile('ex.sql')->apply();
+   * @endCode
+   */
+  public static function builder() {
+    if (!isset(self::$singletons['builder'])) {
+      $civiRoot = dirname(dirname(dirname(dirname(__FILE__))));
+      self::$singletons['builder'] = new CiviTesterBuilder('CiviTesterSchema');
+      self::$singletons['builder']
+        ->callback(function ($ctx) {
+          $dbName = CiviTester::dsn('database');
+          echo "Installing {$dbName} schema\n";
+          CiviTester::schema()->dropAll();
+        }, 'msg-drop')
+        ->sqlFile($civiRoot . "/sql/civicrm.mysql")
+        ->callback(function ($ctx) {
+          CiviTester::data()->populate();
+        }, 'populate');
+    }
+    return self::$singletons['builder'];
+  }
+
+  /**
+   * @return \CiviTesterSchema
+   */
+  public static function schema() {
+    if (!isset(self::$singletons['schema'])) {
+      self::$singletons['schema'] = new CiviTesterSchema();
+    }
+    return self::$singletons['schema'];
+  }
+
+
+  /**
+   * @return \CiviTesterData
+   */
+  public static function data() {
+    if (!isset(self::$singletons['data'])) {
+      self::$singletons['data'] = new CiviTesterData('CiviTesterData');
+    }
+    return self::$singletons['data'];
+  }
+
+  /**
+   * Prepare and execute a batch of SQL statements.
+   *
+   * @param string $query
+   * @return bool
+   */
+  public static function execute($query) {
+    $pdo = CiviTester::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;
+  }
+
+}
+
+/**
+ * Class CiviTesterSchema
+ *
+ * Manage the entire database. This is useful for destroying or loading the schema.
+ */
+class CiviTesterSchema {
+
+  /**
+   * @param string $type
+   *   'BASE TABLE' or 'VIEW'.
+   * @return array
+   */
+  public function getTables($type) {
+    $pdo = CiviTester::pdo();
+    // only consider real tables and not views
+    $query = sprintf(
+      "SELECT table_name FROM INFORMATION_SCHEMA.TABLES
+        WHERE TABLE_SCHEMA = %s AND TABLE_TYPE = %s",
+      $pdo->quote(CiviTester::dsn('database')),
+      $pdo->quote($type)
+    );
+    $tables = $pdo->query($query);
+    $result = array();
+    foreach ($tables as $table) {
+      $result[] = $table['table_name'];
+    }
+    return $result;
+  }
+
+  public function setStrict($checks) {
+    $dbName = CiviTester::dsn('database');
+    if ($checks) {
+      $queries = array(
+        "USE {$dbName};",
+        "SET global innodb_flush_log_at_trx_commit = 1;",
+        "SET SQL_MODE='STRICT_ALL_TABLES';",
+        "SET foreign_key_checks = 1;",
+      );
+    }
+    else {
+      $queries = array(
+        "USE {$dbName};",
+        "SET foreign_key_checks = 0",
+        "SET SQL_MODE='STRICT_ALL_TABLES';",
+        "SET global innodb_flush_log_at_trx_commit = 2;",
+      );
+    }
+    foreach ($queries as $query) {
+      if (CiviTester::execute($query) === FALSE) {
+        throw new RuntimeException("Query failed: $query");
+      }
+    }
+    return $this;
+  }
+
+  public function dropAll() {
+    $queries = array();
+    foreach ($this->getTables('VIEW') as $table) {
+      if (preg_match('/^(civicrm_|log_)/', $table)) {
+        $queries[] = "DROP VIEW $table";
+      }
+    }
+
+    foreach ($this->getTables('BASE TABLE') as $table) {
+      if (preg_match('/^(civicrm_|log_)/', $table)) {
+        $queries[] = "DROP TABLE $table";
+      }
+    }
+
+    $this->setStrict(FALSE);
+    foreach ($queries as $query) {
+      if (CiviTester::execute($query) === FALSE) {
+        throw new RuntimeException("dropSchema: Query failed: $query");
+      }
+    }
+    $this->setStrict(TRUE);
+
+    return $this;
+  }
+
+  /**
+   * @return array
+   */
+  public function truncateAll() {
+    $tables = CiviTester::schema()->getTables('BASE TABLE');
+
+    $truncates = array();
+    $drops = array();
+    foreach ($tables as $table) {
+      // skip log tables
+      if (substr($table, 0, 4) == 'log_') {
+        continue;
+      }
+
+      // don't change list of installed extensions
+      if ($table == 'civicrm_extension') {
+        continue;
+      }
+
+      if (substr($table, 0, 14) == 'civicrm_value_') {
+        $drops[] = 'DROP TABLE ' . $table . ';';
+      }
+      elseif (substr($table, 0, 9) == 'civitest_') {
+        // ignore
+      }
+      else {
+        $truncates[] = 'TRUNCATE ' . $table . ';';
+      }
+    }
+
+    CiviTester::schema()->setStrict(FALSE);
+    $queries = array_merge($truncates, $drops);
+    foreach ($queries as $query) {
+      if (CiviTester::execute($query) === FALSE) {
+        throw new RuntimeException("Query failed: $query");
+      }
+    }
+    CiviTester::schema()->setStrict(TRUE);
+
+    return $this;
+  }
+
+}
+
+/**
+ * Class CiviTesterData
+ */
+class CiviTesterData {
+
+  /**
+   * @return bool
+   */
+  public function populate() {
+    CiviTester::schema()->truncateAll();
+
+    CiviTester::schema()->setStrict(FALSE);
+    //  initialize test database
+    $sql_file2 = dirname(dirname(dirname(dirname(__FILE__)))) . "/sql/civicrm_data.mysql";
+    $sql_file3 = dirname(dirname(dirname(dirname(__FILE__)))) . "/sql/test_data.mysql";
+    $sql_file4 = dirname(dirname(dirname(dirname(__FILE__)))) . "/sql/test_data_second_domain.mysql";
+
+    $query2 = file_get_contents($sql_file2);
+    $query3 = file_get_contents($sql_file3);
+    $query4 = file_get_contents($sql_file4);
+    if (CiviTester::execute($query2) === FALSE) {
+      throw new RuntimeException("Cannot load civicrm_data.mysql. Aborting.");
+    }
+    if (CiviTester::execute($query3) === FALSE) {
+      throw new RuntimeException("Cannot load test_data.mysql. Aborting.");
+    }
+    if (CiviTester::execute($query4) === FALSE) {
+      throw new RuntimeException("Cannot load test_data.mysql. Aborting.");
+    }
+
+    unset($query, $query2, $query3);
+
+    CiviTester::schema()->setStrict(TRUE);
+
+    // Rebuild triggers
+    civicrm_api('system', 'flush', array('version' => 3, 'triggers' => 1));
+
+    CRM_Core_BAO_ConfigSetting::setEnabledComponents(array(
+      'CiviEvent',
+      'CiviContribute',
+      'CiviMember',
+      'CiviMail',
+      'CiviReport',
+      'CiviPledge',
+    ));
+
+    return TRUE;
+  }
+
+}
+
+/**
+ * Class CiviTesterBuilder
+ *
+ * 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 CiviTesterBuilder {
+  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(CiviTesterStep $step) {
+    $this->targetSignature = NULL;
+    $this->steps[] = $step;
+    return $this;
+  }
+
+  public function callback($callback, $signature = NULL) {
+    return $this->addStep(new CiviTesterCallbackStep($callback, $signature));
+  }
+
+  public function sql($sql) {
+    return $this->addStep(new CiviTesterSqlStep($sql));
+  }
+
+  public function sqlFile($file) {
+    return $this->addStep(new CiviTesterSqlFileStep($file));
+  }
+
+  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 = CiviTester::pdo();
+    $pdoStmt = $pdo->query(sprintf(
+      "SELECT rev FROM %s.civitest_revs WHERE name = %s",
+      CiviTester::dsn('database'),
+      $pdo->quote($this->name)
+    ));
+    foreach ($pdoStmt as $row) {
+      $liveSchemaRev = $row['rev'];
+    }
+    return $liveSchemaRev;
+  }
+
+  /**
+   * @param $newSignature
+   */
+  protected function setSavedSignature($newSignature) {
+    $pdo = CiviTester::pdo();
+    $query = sprintf(
+      'INSERT INTO %s.civitest_revs (name,rev) VALUES (%s,%s) '
+      . 'ON DUPLICATE KEY UPDATE rev = %s;',
+      CiviTester::dsn('database'),
+      $pdo->quote($this->name),
+      $pdo->quote($newSignature),
+      $pdo->quote($newSignature)
+    );
+
+    if (CiviTester::execute($query) === FALSE) {
+      throw new RuntimeException("Failed to flag schema version: $query");
+    }
+  }
+
+  /**
+   * Determine if the schema is correct. If necessary, destroy and recreate.
+   *
+   * @param bool $force
+   * @return $this
+   */
+  public function apply($force = FALSE) {
+    $dbName = CiviTester::dsn('database');
+    $query = "USE {$dbName};"
+      . "CREATE TABLE IF NOT EXISTS civitest_revs (name VARCHAR(64) PRIMARY KEY, rev VARCHAR(64));";
+
+    if (CiviTester::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;
+  }
+
+}
+
+interface CiviTesterStep {
+  public function getSig();
+
+  public function isValid();
+
+  public function run($ctx);
+
+}
+
+class CiviTesterSqlFileStep implements CiviTesterStep {
+  private $file;
+
+  /**
+   * CiviTesterSqlFileStep constructor.
+   * @param $file
+   */
+  public function __construct($file) {
+    $this->file = $file;
+  }
+
+
+  public function getSig() {
+    return implode(' ', array(
+      $this->file,
+      filemtime($this->file),
+      filectime($this->file),
+    ));
+  }
+
+  public function isValid() {
+    return is_file($this->file) && is_readable($this->file);
+  }
+
+  public function run($ctx) {
+    /** @var $ctx CiviTesterBuilder */
+    if (CiviTester::execute(@file_get_contents($this->file)) === FALSE) {
+      throw new RuntimeException("Cannot load {$this->file}. Aborting.");
+    }
+  }
+
+}
+
+class CiviTesterSqlStep implements CiviTesterStep {
+  private $sql;
+
+  /**
+   * CiviTesterSqlFileStep constructor.
+   * @param $sql
+   */
+  public function __construct($sql) {
+    $this->sql = $sql;
+  }
+
+
+  public function getSig() {
+    return md5($this->sql);
+  }
+
+  public function isValid() {
+    return TRUE;
+  }
+
+  public function run($ctx) {
+    /** @var $ctx CiviTesterBuilder */
+    if (CiviTester::execute($this->sql) === FALSE) {
+      throw new RuntimeException("Cannot execute: {$this->sql}");
+    }
+  }
+
+}
+
+class CiviTesterCallbackStep implements CiviTesterStep {
+  private $callback;
+  private $sig;
+
+  /**
+   * CiviTesterCallbackStep constructor.
+   * @param $callback
+   * @param $sig
+   */
+  public function __construct($callback, $sig = NULL) {
+    $this->callback = $callback;
+    $this->sig = $sig === NULL ? md5(var_export($callback, 1)) : $sig;
+  }
+
+  public function getSig() {
+    return $this->sig;
+  }
+
+  public function isValid() {
+    return is_callable($this->callback);
+  }
+
+  public function run($ctx) {
+    call_user_func($this->callback, $ctx);
+  }
+
+}
index 365c9c7e9c64b83e7f1c37fd79496157ffab6e51..9cbb5d7a0788662299efd0cdfce0de8586ca53dd 100755 (executable)
@@ -89,11 +89,6 @@ class CiviUnitTestCase extends PHPUnit_Extensions_Database_TestCase {
    */
   protected $tempDirs;
 
-  /**
-   * @var CiviTestPdoUtils
-   */
-  public static $utils;
-
   /**
    * @var boolean populateOnce allows to skip db resets in setUp
    *
@@ -160,9 +155,6 @@ class CiviUnitTestCase extends PHPUnit_Extensions_Database_TestCase {
 
     self::$_dbName = self::getDBName();
 
-    //  create test database
-    self::$utils = new CiviTestPdoUtils(CIVICRM_DSN);
-
     // also load the class loader
     require_once 'CRM/Core/ClassLoader.php';
     CRM_Core_ClassLoader::singleton()->register();
@@ -228,7 +220,8 @@ class CiviUnitTestCase extends PHPUnit_Extensions_Database_TestCase {
 
       self::$dbInit = TRUE;
     }
-    return $this->createDefaultDBConnection(self::$utils->pdo, $dbName);
+
+    return $this->createDefaultDBConnection(CiviTester::pdo(), $dbName);
   }
 
   /**
@@ -260,27 +253,7 @@ class CiviUnitTestCase extends PHPUnit_Extensions_Database_TestCase {
     }
     self::$populateOnce = NULL;
 
-    $builder = new CiviTestDB(self::$utils);
-
-    static $isSchemaUpdated = FALSE;
-    if (!$isSchemaUpdated) {
-      $builder->updateSchema();
-      $isSchemaUpdated = TRUE;
-    }
-
-    $builder->populate();
-
-    // Rebuild triggers
-    civicrm_api('system', 'flush', array('version' => 3, 'triggers' => 1));
-
-    CRM_Core_BAO_ConfigSetting::setEnabledComponents(array(
-      'CiviEvent',
-      'CiviContribute',
-      'CiviMember',
-      'CiviMail',
-      'CiviReport',
-      'CiviPledge',
-    ));
+    CiviTester::data()->populate();
 
     return TRUE;
   }
index 84b52d71635d8ac90a21fc2b85fc4b7fffc80a0b..e04cd6a8471fb200e818a8cb99d214fbf6a2d4a7 100644 (file)
@@ -18,6 +18,10 @@ ini_set('memory_limit', '2G');
 define('CIVICRM_TEST', 1);
 eval(cv('php:boot --level=settings', 'phpcode'));
 
+if (CIVICRM_UF === 'UnitTests') {
+  CiviTester::builder()->apply();
+}
+
 // ------------------------------------------------------------------------------
 
 /**