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