[ Index ]

PHP Cross Reference of Moodle 1.9.3 [Build 15-Oct-2008]

title

Body

[close]

/lib/ -> simpletestlib.php (source)

   1  <?php // $Id$
   2  /**
   3   * Utility functions to make unit testing easier.
   4   * 
   5   * These functions, particularly the the database ones, are quick and
   6   * dirty methods for getting things done in test cases. None of these 
   7   * methods should be used outside test code.
   8   *
   9   * @copyright &copy; 2006 The Open University
  10   * @author T.J.Hunt@open.ac.uk
  11   * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
  12   * @version $Id$
  13   * @package SimpleTestEx
  14   */
  15  
  16  require_once(dirname(__FILE__) . '/../config.php');
  17  require_once($CFG->libdir . '/simpletestlib/simpletest.php');
  18  require_once($CFG->libdir . '/simpletestlib/unit_tester.php');
  19  require_once($CFG->libdir . '/simpletestlib/expectation.php');
  20  require_once($CFG->libdir . '/simpletestlib/reporter.php');
  21  require_once($CFG->libdir . '/simpletestlib/web_tester.php');
  22  require_once($CFG->libdir . '/simpletestlib/mock_objects.php');
  23  
  24  /**
  25   * Recursively visit all the files in the source tree. Calls the callback
  26   * function with the pathname of each file found. 
  27   * 
  28   * @param $path the folder to start searching from. 
  29   * @param $callback the function to call with the name of each file found.
  30   * @param $fileregexp a regexp used to filter the search (optional).
  31   * @param $exclude If true, pathnames that match the regexp will be ingored. If false, 
  32   *     only files that match the regexp will be included. (default false).
  33   * @param array $ignorefolders will not go into any of these folders (optional).
  34   */ 
  35  function recurseFolders($path, $callback, $fileregexp = '/.*/', $exclude = false, $ignorefolders = array()) {
  36      $files = scandir($path);
  37  
  38      foreach ($files as $file) {
  39          $filepath = $path .'/'. $file;
  40          if ($file == '.' || $file == '..') {
  41              continue;
  42          } else if (is_dir($filepath)) {
  43              if (!in_array($filepath, $ignorefolders)) {
  44                  recurseFolders($filepath, $callback, $fileregexp, $exclude, $ignorefolders);
  45              }
  46          } else if ($exclude xor preg_match($fileregexp, $filepath)) {
  47              call_user_func($callback, $filepath);
  48          }
  49      }
  50  }
  51  
  52  /**
  53   * An expectation for comparing strings ignoring whitespace.
  54   */
  55  class IgnoreWhitespaceExpectation extends SimpleExpectation {
  56      var $expect;
  57  
  58      function IgnoreWhitespaceExpectation($content, $message = '%s') {
  59          $this->SimpleExpectation($message);
  60          $this->expect=$this->normalise($content);
  61      }
  62  
  63      function test($ip) {
  64          return $this->normalise($ip)==$this->expect;
  65      }
  66  
  67      function normalise($text) {
  68          return preg_replace('/\s+/m',' ',trim($text));
  69      }
  70  
  71      function testMessage($ip) {
  72          return "Input string [$ip] doesn't match the required value.";
  73      }
  74  }
  75  
  76  /**
  77   * An Expectation that two arrays contain the same list of values.
  78   */
  79  class ArraysHaveSameValuesExpectation extends SimpleExpectation {
  80      var $expect;
  81  
  82      function ArraysHaveSameValuesExpectation($expected, $message = '%s') {
  83          $this->SimpleExpectation($message);
  84          if (!is_array($expected)) {
  85              trigger_error('Attempt to create an ArraysHaveSameValuesExpectation ' .
  86                      'with an expected value that is not an array.');
  87          }
  88          $this->expect = $this->normalise($expected);
  89      }
  90  
  91      function test($actual) {
  92          return $this->normalise($actual) == $this->expect;
  93      }
  94  
  95      function normalise($array) {
  96          sort($array);
  97          return $array;
  98      }
  99  
 100      function testMessage($actual) {
 101          return 'Array [' . implode(', ', $actual) .
 102                  '] does not contain the expected list of values [' . implode(', ', $this->expect) . '].';
 103      }
 104  }
 105  
 106  /**
 107   * An Expectation that compares to objects, and ensures that for every field in the
 108   * expected object, there is a key of the same name in the actual object, with
 109   * the same value. (The actual object may have other fields to, but we ignore them.)
 110   */
 111  class CheckSpecifiedFieldsExpectation extends SimpleExpectation {
 112      var $expect;
 113  
 114      function CheckSpecifiedFieldsExpectation($expected, $message = '%s') {
 115          $this->SimpleExpectation($message);
 116          if (!is_object($expected)) {
 117              trigger_error('Attempt to create a CheckSpecifiedFieldsExpectation ' .
 118                      'with an expected value that is not an object.');
 119          }
 120          $this->expect = $expected;
 121      }
 122  
 123      function test($actual) {
 124          foreach ($this->expect as $key => $value) {
 125              if (isset($value) && isset($actual->$key) && $actual->$key == $value) {
 126                  // OK
 127              } else if (is_null($value) && is_null($actual->$key)) {
 128                  // OK
 129              } else {
 130                  return false;
 131              }
 132          }
 133          return true;
 134      }
 135  
 136      function testMessage($actual) {
 137          $mismatches = array();
 138          foreach ($this->expect as $key => $value) {
 139              if (isset($value) && isset($actual->$key) && $actual->$key == $value) {
 140                  // OK
 141              } else if (is_null($value) && is_null($actual->$key)) {
 142                  // OK
 143              } else {
 144                  $mismatches[] = $key;
 145              }
 146          }
 147          return 'Actual object does not have all the same fields with the same values as the expected object (' .
 148                  implode(', ', $mismatches) . ').';
 149      }
 150  }
 151  
 152  /**
 153   * Given a table name, a two-dimensional array of data, and a database connection,
 154   * creates a table in the database. The array of data should look something like this.
 155   *
 156   * $testdata = array(
 157   *      array('id', 'username', 'firstname', 'lastname', 'email'),
 158   *      array(1,    'u1',       'user',      'one',      'u1@example.com'),
 159   *      array(2,    'u2',       'user',      'two',      'u2@example.com'),
 160   *      array(3,    'u3',       'user',      'three',    'u3@example.com'),
 161   *      array(4,    'u4',       'user',      'four',     'u4@example.com'),
 162   *      array(5,    'u5',       'user',      'five',     'u5@example.com'),
 163   *  );
 164   *
 165   * The first 'row' of the test data gives the column names. The type of each column
 166   * is set to either INT or VARCHAR($strlen), guessed by inspecting the first row of
 167   * data. Unless the col name is 'id' in which case the col type will be SERIAL.
 168   * The remaining 'rows' of the data array are values loaded into the table. All columns
 169   * are created with a default of 0xdefa or 'Default' as appropriate.
 170   * 
 171   * This function should not be used in real code. Only for testing and debugging.
 172   *
 173   * @param string $tablename the name of the table to create. E.g. 'mdl_unittest_user'.
 174   * @param array $data a two-dimensional array of data, in the format described above.
 175   * @param object $db an AdoDB database connection.
 176   * @param int $strlen the width to use for string fields.
 177   */
 178  function load_test_table($tablename, $data, $db = null, $strlen = 255) {
 179      $colnames = array_shift($data);
 180      $coldefs = array();
 181      foreach (array_combine($colnames, $data[0]) as $colname => $value) {
 182          if ($colname == 'id') {
 183              $type = 'SERIAL';
 184          } else if (is_int($value)) {
 185              $type = 'INTEGER DEFAULT 57082'; // 0xdefa
 186          } else {
 187              $type = "VARCHAR($strlen) DEFAULT 'Default'";
 188          }
 189          $coldefs[] = "$colname $type";
 190      }
 191      _private_execute_sql("CREATE TABLE $tablename (" . join(',', $coldefs) . ');', $db);
 192  
 193      array_unshift($data, $colnames);
 194      load_test_data($tablename, $data, $db);
 195  }
 196  
 197  /**
 198   * Given a table name, a two-dimensional array of data, and a database connection,
 199   * adds data to the database table. The array should have the same format as for
 200   * load_test_table(), with the first 'row' giving column names.
 201   * 
 202   * This function should not be used in real code. Only for testing and debugging.
 203   *
 204   * @param string $tablename the name of the table to populate. E.g. 'mdl_unittest_user'.
 205   * @param array $data a two-dimensional array of data, in the format described.
 206   * @param object $localdb an AdoDB database connection.
 207   */
 208  function load_test_data($tablename, $data, $localdb = null) {
 209      global $CFG;
 210  
 211      if (null == $localdb) {
 212          global $db;
 213          $localdb = $db;
 214      }    
 215      $colnames = array_shift($data);
 216      $idcol = array_search('id', $colnames);
 217      $maxid = -1;
 218      foreach ($data as $row) {
 219          _private_execute_sql($localdb->GetInsertSQL($tablename, array_combine($colnames, $row)), $localdb);
 220          if ($idcol !== false && $row[$idcol] > $maxid) {
 221              $maxid = $row[$idcol];
 222          }
 223      }
 224      if ($CFG->dbfamily == 'postgres' && $idcol !== false) {
 225          $maxid += 1;
 226          _private_execute_sql("ALTER SEQUENCE {$tablename}_id_seq RESTART WITH $maxid;", $localdb);
 227      }
 228  }
 229  
 230  /**
 231   * Make multiple tables that are the same as a real table but empty.
 232   * 
 233   * This function should not be used in real code. Only for testing and debugging.
 234   *
 235   * @param mixed $tablename Array of strings containing the names of the table to populate (without prefix).
 236   * @param string $realprefix the prefix used for real tables. E.g. 'mdl_'.
 237   * @param string $testprefix the prefix used for test tables. E.g. 'mdl_unittest_'.
 238   * @param object $db an AdoDB database connection.
 239   */
 240  function make_test_tables_like_real_one($tablenames, $realprefix, $testprefix, $db,$dropconstraints=false) {
 241      foreach($tablenames as $individual) {
 242          make_test_table_like_real_one($individual,$realprefix,$testprefix,$db,$dropconstraints);
 243      }
 244  }
 245  
 246  /**
 247   * Make a test table that has all the same columns as a real moodle table,
 248   * but which is empty.
 249   *
 250   * This function should not be used in real code. Only for testing and debugging.
 251   *
 252   * @param string $tablename Name of the table to populate. E.g. 'user'.
 253   * @param string $realprefix the prefix used for real tables. E.g. 'mdl_'.
 254   * @param string $testprefix the prefix used for test tables. E.g. 'mdl_unittest_'.
 255   * @param object $db an AdoDB database connection.
 256   */
 257  function make_test_table_like_real_one($tablename, $realprefix, $testprefix, $db, $dropconstraints=false) {
 258      _private_execute_sql("CREATE TABLE $testprefix$tablename (LIKE $realprefix$tablename INCLUDING DEFAULTS);", $db);
 259      if (_private_has_id_column($testprefix . $tablename, $db)) {
 260          _private_execute_sql("CREATE SEQUENCE $testprefix{$tablename}_id_seq;", $db);
 261          _private_execute_sql("ALTER TABLE $testprefix$tablename ALTER COLUMN id SET DEFAULT nextval('{$testprefix}{$tablename}_id_seq'::regclass);", $db);
 262          _private_execute_sql("ALTER TABLE $testprefix$tablename ADD PRIMARY KEY (id);", $db);
 263      }
 264      if($dropconstraints) {
 265          $cols=$db->MetaColumnNames($testprefix.$tablename);
 266          foreach($cols as $col) {
 267              $rs=_private_execute_sql(
 268                  "SELECT constraint_name FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE table_name='$testprefix$tablename'",$db);
 269              while(!$rs->EOF) {
 270                  $constraintname=$rs->fields['constraint_name'];
 271                  _private_execute_sql("ALTER TABLE $testprefix$tablename DROP CONSTRAINT $constraintname",$db);
 272                  $rs->MoveNext();
 273              }
 274  
 275              _private_execute_sql("ALTER TABLE $testprefix$tablename ALTER COLUMN $col DROP NOT NULL",$db);
 276          }
 277      }
 278  }
 279  
 280  /**
 281   * Drops a table from the database pointed to by the database connection.
 282   * This undoes the create performed by load_test_table().
 283   *
 284   * This function should not be used in real code. Only for testing and debugging.
 285   *
 286   * @param string $tablename the name of the table to populate. E.g. 'mdl_unittest_user'.
 287   * @param object $db an AdoDB database connection.
 288   * @param bool $cascade If true, also drop tables that depend on this one, e.g. through
 289   *      foreign key constraints.
 290   */
 291  function remove_test_table($tablename, $db, $cascade = false) {
 292      global $CFG;
 293      _private_execute_sql('DROP TABLE ' . $tablename . ($cascade ? ' CASCADE' : '') . ';', $db);
 294      
 295      if ($CFG->dbfamily == 'postgres') {
 296          $rs = $db->Execute("SELECT relname FROM pg_class WHERE relname = '{$tablename}_id_seq' AND relkind = 'S';");
 297          if ($rs && !rs_EOF($rs)) {
 298              _private_execute_sql("DROP SEQUENCE {$tablename}_id_seq;", $db);
 299          }
 300      }
 301  }
 302  
 303  /**
 304   * Drops all the tables with a particular prefix from the database pointed to by the database connection.
 305   * Useful for cleaning up after a unit test run has crashed leaving the DB full of junk.
 306   *
 307   * This function should not be used in real code. Only for testing and debugging.
 308   *
 309   * @param string $prefix the prfix of tables to drop 'mdl_unittest_'.
 310   * @param object $db an AdoDB database connection.
 311   */
 312  function wipe_tables($prefix, $db) {
 313      if (strpos($prefix, 'test') === false) {
 314          notice('The wipe_tables function should only be used to wipe test tables.');
 315          return;
 316      }
 317      $tables = $db->Metatables('TABLES', false, "$prefix%");
 318      foreach ($tables as $table) {
 319          _private_execute_sql("DROP TABLE $table CASCADE", $db);
 320      }
 321  }
 322  
 323  /**
 324   * Drops all the sequences with a particular prefix from the database pointed to by the database connection.
 325   * Useful for cleaning up after a unit test run has crashed leaving the DB full of junk.
 326   *
 327   * This function should not be used in real code. Only for testing and debugging.
 328   *
 329   * @param string $prefix the prfix of sequences to drop 'mdl_unittest_'.
 330   * @param object $db an AdoDB database connection.
 331   */
 332  function wipe_sequences($prefix, $db) {
 333      global $CFG;
 334  
 335      if ($CFG->dbfamily == 'postgres') {
 336          $sequences = $db->GetCol("SELECT relname FROM pg_class WHERE relname LIKE '$prefix%_id_seq' AND relkind = 'S';");
 337          if ($sequences) {
 338              foreach ($sequences as $sequence) {
 339                  _private_execute_sql("DROP SEQUENCE $sequence CASCADE", $db);
 340              }
 341          }
 342      }
 343  }
 344  
 345  function _private_has_id_column($table, $db) {
 346      return in_array('id', $db->MetaColumnNames($table));
 347  }
 348  
 349  function _private_execute_sql($sql, $localdb = null) {
 350  
 351      if (null == $localdb) {
 352          global $db;
 353          $localdb = $db;
 354      }
 355      if (!$rs = $localdb->Execute($sql)) {
 356          echo '<p>SQL ERROR: ', $localdb->ErrorMsg(), ". STATEMENT: $sql</p>";
 357      }
 358      return $rs;
 359  }
 360  
 361  /**
 362   * Base class for testcases that want a different DB prefix.
 363   * 
 364   * That is, when you need to load test data into the database for
 365   * unit testing, instead of messing with the real mdl_course table,
 366   * we will temporarily change $CFG->prefix from (say) mdl_ to mdl_unittest_
 367   * and create a table called mdl_unittest_course to hold the test data.
 368   */
 369  class prefix_changing_test_case extends UnitTestCase {
 370      var $old_prefix;
 371      
 372      function change_prefix() {
 373          global $CFG;
 374          $this->old_prefix = $CFG->prefix;
 375          $CFG->prefix = $CFG->prefix . 'unittest_';
 376      }
 377  
 378      function change_prefix_back() {
 379          global $CFG;
 380          $CFG->prefix = $this->old_prefix;
 381      }
 382  
 383      function setUp() {
 384          $this->change_prefix();
 385      }
 386  
 387      function tearDown() {
 388          $this->change_prefix_back();
 389      }
 390  }
 391  ?>


Generated: Wed Jan 14 11:33:29 2009 Cross-referenced by PHPXref 0.7