MDL-26502 check browser - fix for Symbian (backported)
[moodle.git] / lib / simpletestlib.php
blob913b4c9ecf14d667a1eaeec0cbe5857d058ce624
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.
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
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');
24 /**
25 * Recursively visit all the files in the source tree. Calls the callback
26 * function with the pathname of each file found.
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);
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);
46 } else if ($exclude xor preg_match($fileregexp, $filepath)) {
47 call_user_func($callback, $filepath);
52 /**
53 * An expectation for comparing strings ignoring whitespace.
55 class IgnoreWhitespaceExpectation extends SimpleExpectation {
56 var $expect;
58 function IgnoreWhitespaceExpectation($content, $message = '%s') {
59 $this->SimpleExpectation($message);
60 $this->expect=$this->normalise($content);
63 function test($ip) {
64 return $this->normalise($ip)==$this->expect;
67 function normalise($text) {
68 return preg_replace('/\s+/m',' ',trim($text));
71 function testMessage($ip) {
72 return "Input string [$ip] doesn't match the required value.";
76 /**
77 * An Expectation that two arrays contain the same list of values.
79 class ArraysHaveSameValuesExpectation extends SimpleExpectation {
80 var $expect;
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.');
88 $this->expect = $this->normalise($expected);
91 function test($actual) {
92 return $this->normalise($actual) == $this->expect;
95 function normalise($array) {
96 sort($array);
97 return $array;
100 function testMessage($actual) {
101 return 'Array [' . implode(', ', $actual) .
102 '] does not contain the expected list of values [' . implode(', ', $this->expect) . '].';
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.)
111 class CheckSpecifiedFieldsExpectation extends SimpleExpectation {
112 var $expect;
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.');
120 $this->expect = $expected;
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;
133 return true;
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;
147 return 'Actual object does not have all the same fields with the same values as the expected object (' .
148 implode(', ', $mismatches) . ').';
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.
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 * );
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.
171 * This function should not be used in real code. Only for testing and debugging.
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.
178 function load_test_table($tablename, $data, $db = null, $strlen = 255) {
179 global $CFG;
180 $colnames = array_shift($data);
181 $coldefs = array();
182 foreach (array_combine($colnames, $data[0]) as $colname => $value) {
183 if ($colname == 'id') {
184 switch ($CFG->dbfamily) {
185 case 'mssql':
186 $type = 'INTEGER IDENTITY(1,1)';
187 break;
188 case 'oracle':
189 $type = 'INTEGER';
190 break;
191 default:
192 $type = 'SERIAL';
194 } else if (is_int($value)) {
195 $type = 'INTEGER DEFAULT 57082'; // 0xdefa
196 if ($CFG->dbfamily == 'mssql') {
197 $type = 'INTEGER NULL DEFAULT 57082';
199 } else {
200 $type = "VARCHAR($strlen) DEFAULT 'Default'";
201 if ($CFG->dbfamily == 'mssql') {
202 $type = "VARCHAR($strlen) NULL DEFAULT 'Default'";
203 } else if ($CFG->dbfamily == 'oracle') {
204 $type = "VARCHAR2($strlen) DEFAULT 'Default'";
207 $coldefs[] = "$colname $type";
209 _private_execute_sql("CREATE TABLE $tablename (" . join(',', $coldefs) . ');', $db);
211 if ($CFG->dbfamily == 'oracle') {
212 $sql = "CREATE SEQUENCE {$tablename}_id_seq;";
213 _private_execute_sql($sql, $db);
214 $sql = "CREATE OR REPLACE TRIGGER {$tablename}_id_trg BEFORE INSERT ON $tablename FOR EACH ROW BEGIN IF :new.id IS NULL THEN SELECT {$tablename}_ID_SEQ.nextval INTO :new.id FROM dual; END IF; END; ";
215 _private_execute_sql($sql, $db);
218 array_unshift($data, $colnames);
219 load_test_data($tablename, $data, $db);
223 * Given a table name, a two-dimensional array of data, and a database connection,
224 * adds data to the database table. The array should have the same format as for
225 * load_test_table(), with the first 'row' giving column names.
227 * This function should not be used in real code. Only for testing and debugging.
229 * @param string $tablename the name of the table to populate. E.g. 'mdl_unittest_user'.
230 * @param array $data a two-dimensional array of data, in the format described.
231 * @param object $localdb an AdoDB database connection.
233 function load_test_data($tablename, $data, $localdb = null) {
234 global $CFG;
236 if (null == $localdb) {
237 global $db;
238 $localdb = $db;
240 $colnames = array_shift($data);
241 $idcol = array_search('id', $colnames);
242 $maxid = -1;
243 foreach ($data as $row) {
244 $savedcolnames = $colnames;
245 $savedrow = $row;
246 unset($colnames[0]);
247 unset($row[0]);
248 _private_execute_sql($localdb->GetInsertSQL($tablename, array_combine($colnames, $row)), $localdb);
249 $colnames = $savedcolnames;
250 $row = $savedrow;
251 if ($idcol !== false && $row[$idcol] > $maxid) {
252 $maxid = $row[$idcol];
255 if ($CFG->dbfamily == 'postgres' && $idcol !== false) {
256 $maxid += 1;
257 _private_execute_sql("ALTER SEQUENCE {$tablename}_id_seq RESTART WITH $maxid;", $localdb);
262 * Make multiple tables that are the same as a real table but empty.
264 * This function should not be used in real code. Only for testing and debugging.
266 * @param mixed $tablename Array of strings containing the names of the table to populate (without prefix).
267 * @param string $realprefix the prefix used for real tables. E.g. 'mdl_'.
268 * @param string $testprefix the prefix used for test tables. E.g. 'mdl_unittest_'.
269 * @param object $db an AdoDB database connection.
271 function make_test_tables_like_real_one($tablenames, $realprefix, $testprefix, $db,$dropconstraints=false) {
272 foreach($tablenames as $individual) {
273 make_test_table_like_real_one($individual,$realprefix,$testprefix,$db,$dropconstraints);
278 * Make a test table that has all the same columns as a real moodle table,
279 * but which is empty.
281 * This function should not be used in real code. Only for testing and debugging.
283 * @param string $tablename Name of the table to populate. E.g. 'user'.
284 * @param string $realprefix the prefix used for real tables. E.g. 'mdl_'.
285 * @param string $testprefix the prefix used for test tables. E.g. 'mdl_unittest_'.
286 * @param object $db an AdoDB database connection.
288 function make_test_table_like_real_one($tablename, $realprefix, $testprefix, $db, $dropconstraints=false) {
289 _private_execute_sql("CREATE TABLE $testprefix$tablename (LIKE $realprefix$tablename INCLUDING DEFAULTS);", $db);
290 if (_private_has_id_column($testprefix . $tablename, $db)) {
291 _private_execute_sql("CREATE SEQUENCE $testprefix{$tablename}_id_seq;", $db);
292 _private_execute_sql("ALTER TABLE $testprefix$tablename ALTER COLUMN id SET DEFAULT nextval('{$testprefix}{$tablename}_id_seq'::regclass);", $db);
293 _private_execute_sql("ALTER TABLE $testprefix$tablename ADD PRIMARY KEY (id);", $db);
295 if($dropconstraints) {
296 $cols=$db->MetaColumnNames($testprefix.$tablename);
297 foreach($cols as $col) {
298 $rs=_private_execute_sql(
299 "SELECT constraint_name FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE table_name='$testprefix$tablename'",$db);
300 while(!$rs->EOF) {
301 $constraintname=$rs->fields['constraint_name'];
302 _private_execute_sql("ALTER TABLE $testprefix$tablename DROP CONSTRAINT $constraintname",$db);
303 $rs->MoveNext();
306 _private_execute_sql("ALTER TABLE $testprefix$tablename ALTER COLUMN $col DROP NOT NULL",$db);
312 * Drops a table from the database pointed to by the database connection.
313 * This undoes the create performed by load_test_table().
315 * This function should not be used in real code. Only for testing and debugging.
317 * @param string $tablename the name of the table to populate. E.g. 'mdl_unittest_user'.
318 * @param object $db an AdoDB database connection.
319 * @param bool $cascade If true, also drop tables that depend on this one, e.g. through
320 * foreign key constraints.
322 function remove_test_table($tablename, $db, $cascade = false) {
323 global $CFG;
324 _private_execute_sql('DROP TABLE ' . $tablename . ($cascade ? ' CASCADE' : '') . ';', $db);
326 if ($CFG->dbfamily == 'postgres') {
327 $rs = $db->Execute("SELECT relname FROM pg_class WHERE relname = '{$tablename}_id_seq' AND relkind = 'S';");
328 if ($rs && !rs_EOF($rs)) {
329 _private_execute_sql("DROP SEQUENCE {$tablename}_id_seq;", $db);
333 if ($CFG->dbfamily == 'oracle') {
334 _private_execute_sql("DROP SEQUENCE {$tablename}_id_seq;", $db);
339 * Drops all the tables with a particular prefix from the database pointed to by the database connection.
340 * Useful for cleaning up after a unit test run has crashed leaving the DB full of junk.
342 * This function should not be used in real code. Only for testing and debugging.
344 * @param string $prefix the prfix of tables to drop 'mdl_unittest_'.
345 * @param object $db an AdoDB database connection.
347 function wipe_tables($prefix, $db) {
348 if (strpos($prefix, 'test') === false) {
349 notice('The wipe_tables function should only be used to wipe test tables.');
350 return;
352 $tables = $db->Metatables('TABLES', false, "$prefix%");
353 foreach ($tables as $table) {
354 _private_execute_sql("DROP TABLE $table CASCADE", $db);
359 * Drops all the sequences with a particular prefix from the database pointed to by the database connection.
360 * Useful for cleaning up after a unit test run has crashed leaving the DB full of junk.
362 * This function should not be used in real code. Only for testing and debugging.
364 * @param string $prefix the prfix of sequences to drop 'mdl_unittest_'.
365 * @param object $db an AdoDB database connection.
367 function wipe_sequences($prefix, $db) {
368 global $CFG;
370 if ($CFG->dbfamily == 'postgres') {
371 $sequences = $db->GetCol("SELECT relname FROM pg_class WHERE relname LIKE '$prefix%_id_seq' AND relkind = 'S';");
372 if ($sequences) {
373 foreach ($sequences as $sequence) {
374 _private_execute_sql("DROP SEQUENCE $sequence CASCADE", $db);
380 function _private_has_id_column($table, $db) {
381 return in_array('id', $db->MetaColumnNames($table));
384 function _private_execute_sql($sql, $localdb = null) {
386 global $CFG;
388 if (null == $localdb) {
389 global $db;
390 $localdb = $db;
392 if ($CFG->dbfamily == 'oracle') {
393 $sql = trim($sql, ';');
395 if (!$rs = $localdb->Execute($sql)) {
396 echo '<p>SQL ERROR: ', $localdb->ErrorMsg(), ". STATEMENT: $sql</p>";
398 return $rs;
402 * Base class for testcases that want a different DB prefix.
404 * That is, when you need to load test data into the database for
405 * unit testing, instead of messing with the real mdl_course table,
406 * we will temporarily change $CFG->prefix from (say) mdl_ to mdl_unittest_
407 * and create a table called mdl_unittest_course to hold the test data.
409 class prefix_changing_test_case extends UnitTestCase {
410 var $old_prefix;
412 function change_prefix() {
413 global $CFG;
414 $this->old_prefix = $CFG->prefix;
415 $CFG->prefix = $CFG->prefix . 'unittest_';
418 function change_prefix_back() {
419 global $CFG;
420 $CFG->prefix = $this->old_prefix;
423 function setUp() {
424 $this->change_prefix();
427 function tearDown() {
428 $this->change_prefix_back();