Merge branch 'MDL-35858-master' of git://github.com/FMCorz/moodle
[moodle.git] / lib / phpunit / classes / advanced_testcase.php
CommitLineData
7e7cfe7a
PS
1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Advanced test case.
19 *
20 * @package core
21 * @category phpunit
22 * @copyright 2012 Petr Skoda {@link http://skodak.org}
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25
26
27/**
28 * Advanced PHPUnit test case customised for Moodle.
29 *
30 * @package core
31 * @category phpunit
32 * @copyright 2012 Petr Skoda {@link http://skodak.org}
33 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34 */
35abstract class advanced_testcase extends PHPUnit_Framework_TestCase {
36 /** @var bool automatically reset everything? null means log changes */
37 private $resetAfterTest;
38
39 /** @var moodle_transaction */
40 private $testdbtransaction;
41
42 /**
43 * Constructs a test case with the given name.
44 *
45 * Note: use setUp() or setUpBeforeClass() in your test cases.
46 *
47 * @param string $name
48 * @param array $data
49 * @param string $dataName
50 */
51 final public function __construct($name = null, array $data = array(), $dataName = '') {
52 parent::__construct($name, $data, $dataName);
53
54 $this->setBackupGlobals(false);
55 $this->setBackupStaticAttributes(false);
56 $this->setRunTestInSeparateProcess(false);
57 }
58
59 /**
60 * Runs the bare test sequence.
61 * @return void
62 */
63 final public function runBare() {
64 global $DB;
65
66 if (phpunit_util::$lastdbwrites != $DB->perf_get_writes()) {
67 // this happens when previous test does not reset, we can not use transactions
68 $this->testdbtransaction = null;
69
70 } else if ($DB->get_dbfamily() === 'postgres' or $DB->get_dbfamily() === 'mssql') {
71 // database must allow rollback of DDL, so no mysql here
72 $this->testdbtransaction = $DB->start_delegated_transaction();
73 }
74
75 try {
76 parent::runBare();
77 // set DB reference in case somebody mocked it in test
78 $DB = phpunit_util::get_global_backup('DB');
ef5b5e05
PS
79
80 // Deal with any debugging messages.
81 phpunit_util::display_debugging_messages();
82 phpunit_util::reset_debugging();
83
7e7cfe7a
PS
84 } catch (Exception $e) {
85 // cleanup after failed expectation
86 phpunit_util::reset_all_data();
87 throw $e;
88 }
89
90 if (!$this->testdbtransaction or $this->testdbtransaction->is_disposed()) {
91 $this->testdbtransaction = null;
92 }
93
94 if ($this->resetAfterTest === true) {
95 if ($this->testdbtransaction) {
96 $DB->force_transaction_rollback();
97 phpunit_util::reset_all_database_sequences();
98 phpunit_util::$lastdbwrites = $DB->perf_get_writes(); // no db reset necessary
99 }
100 phpunit_util::reset_all_data();
101
102 } else if ($this->resetAfterTest === false) {
103 if ($this->testdbtransaction) {
104 $this->testdbtransaction->allow_commit();
105 }
106 // keep all data untouched for other tests
107
108 } else {
109 // reset but log what changed
110 if ($this->testdbtransaction) {
111 try {
112 $this->testdbtransaction->allow_commit();
113 } catch (dml_transaction_exception $e) {
114 phpunit_util::reset_all_data();
115 throw new coding_exception('Invalid transaction state detected in test '.$this->getName());
116 }
117 }
118 phpunit_util::reset_all_data(true);
119 }
120
121 // make sure test did not forget to close transaction
122 if ($DB->is_transaction_started()) {
123 phpunit_util::reset_all_data();
124 if ($this->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_PASSED
125 or $this->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_SKIPPED
126 or $this->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_INCOMPLETE) {
127 throw new coding_exception('Test '.$this->getName().' did not close database transaction');
128 }
129 }
130 }
131
132 /**
133 * Creates a new FlatXmlDataSet with the given $xmlFile. (absolute path.)
134 *
135 * @param string $xmlFile
136 * @return PHPUnit_Extensions_Database_DataSet_FlatXmlDataSet
137 */
138 protected function createFlatXMLDataSet($xmlFile) {
139 return new PHPUnit_Extensions_Database_DataSet_FlatXmlDataSet($xmlFile);
140 }
141
142 /**
143 * Creates a new XMLDataSet with the given $xmlFile. (absolute path.)
144 *
145 * @param string $xmlFile
146 * @return PHPUnit_Extensions_Database_DataSet_XmlDataSet
147 */
148 protected function createXMLDataSet($xmlFile) {
149 return new PHPUnit_Extensions_Database_DataSet_XmlDataSet($xmlFile);
150 }
151
152 /**
153 * Creates a new CsvDataSet from the given array of csv files. (absolute paths.)
154 *
155 * @param array $files array tablename=>cvsfile
156 * @param string $delimiter
157 * @param string $enclosure
158 * @param string $escape
159 * @return PHPUnit_Extensions_Database_DataSet_CsvDataSet
160 */
161 protected function createCsvDataSet($files, $delimiter = ',', $enclosure = '"', $escape = '"') {
162 $dataSet = new PHPUnit_Extensions_Database_DataSet_CsvDataSet($delimiter, $enclosure, $escape);
163 foreach($files as $table=>$file) {
164 $dataSet->addTable($table, $file);
165 }
166 return $dataSet;
167 }
168
169 /**
170 * Creates new ArrayDataSet from given array
171 *
172 * @param array $data array of tables, first row in each table is columns
173 * @return phpunit_ArrayDataSet
174 */
175 protected function createArrayDataSet(array $data) {
176 return new phpunit_ArrayDataSet($data);
177 }
178
179 /**
180 * Load date into moodle database tables from standard PHPUnit data set.
181 *
182 * Note: it is usually better to use data generators
183 *
184 * @param PHPUnit_Extensions_Database_DataSet_IDataSet $dataset
185 * @return void
186 */
187 protected function loadDataSet(PHPUnit_Extensions_Database_DataSet_IDataSet $dataset) {
188 global $DB;
189
190 $structure = phpunit_util::get_tablestructure();
191
192 foreach($dataset->getTableNames() as $tablename) {
193 $table = $dataset->getTable($tablename);
194 $metadata = $dataset->getTableMetaData($tablename);
195 $columns = $metadata->getColumns();
196
197 $doimport = false;
198 if (isset($structure[$tablename]['id']) and $structure[$tablename]['id']->auto_increment) {
199 $doimport = in_array('id', $columns);
200 }
201
202 for($r=0; $r<$table->getRowCount(); $r++) {
203 $record = $table->getRow($r);
204 if ($doimport) {
205 $DB->import_record($tablename, $record);
206 } else {
207 $DB->insert_record($tablename, $record);
208 }
209 }
210 if ($doimport) {
211 $DB->get_manager()->reset_sequence(new xmldb_table($tablename));
212 }
213 }
214 }
215
216 /**
217 * Call this method from test if you want to make sure that
218 * the resetting of database is done the slow way without transaction
219 * rollback.
220 *
221 * This is useful especially when testing stuff that is not compatible with transactions.
222 *
223 * @return void
224 */
225 public function preventResetByRollback() {
226 if ($this->testdbtransaction and !$this->testdbtransaction->is_disposed()) {
227 $this->testdbtransaction->allow_commit();
228 $this->testdbtransaction = null;
229 }
230 }
231
232 /**
233 * Reset everything after current test.
234 * @param bool $reset true means reset state back, false means keep all data for the next test,
235 * null means reset state and show warnings if anything changed
236 * @return void
237 */
238 public function resetAfterTest($reset = true) {
239 $this->resetAfterTest = $reset;
240 }
241
ef5b5e05
PS
242 /**
243 * Return debugging messages from the current test.
94c9db54 244 * @return array with instances having 'message', 'level' and 'stacktrace' property.
ef5b5e05
PS
245 */
246 public function getDebuggingMessages() {
247 return phpunit_util::get_debugging_messages();
248 }
249
250 /**
251 * Clear all previous debugging messages in current test.
ef5b5e05
PS
252 */
253 public function resetDebugging() {
94c9db54 254 phpunit_util::reset_debugging();
ef5b5e05
PS
255 }
256
257 /**
258 * Assert that exactly debugging was just called once.
259 *
260 * Discards the debugging message if successful.
261 *
262 * @param null|string $debugmessage null means any
263 * @param null|string $debuglevel null means any
264 * @param string $message
265 */
266 public function assertDebuggingCalled($debugmessage = null, $debuglevel = null, $message = '') {
267 $debugging = phpunit_util::get_debugging_messages();
268 $count = count($debugging);
269
270 if ($count == 0) {
271 if ($message === '') {
272 $message = 'Expectation failed, debugging() not triggered.';
273 }
274 $this->fail($message);
275 }
276 if ($count > 1) {
277 if ($message === '') {
278 $message = 'Expectation failed, debugging() triggered '.$count.' times.';
279 }
280 $this->fail($message);
281 }
282 $this->assertEquals(1, $count);
283
284 $debug = reset($debugging);
285 if ($debugmessage !== null) {
286 $this->assertSame($debugmessage, $debug->message, $message);
287 }
288 if ($debuglevel !== null) {
289 $this->assertSame($debuglevel, $debug->level, $message);
290 }
291
292 phpunit_util::reset_debugging();
293 }
294
94c9db54
PS
295 /**
296 * Call when no debugging() messages expected.
297 * @param string $message
298 */
ef5b5e05
PS
299 public function assertDebuggingNotCalled($message = '') {
300 $debugging = phpunit_util::get_debugging_messages();
301 $count = count($debugging);
302
303 if ($message === '') {
304 $message = 'Expectation failed, debugging() was triggered.';
305 }
306 $this->assertEquals(0, $count, $message);
307 }
308
7e7cfe7a
PS
309 /**
310 * Cleanup after all tests are executed.
311 *
312 * Note: do not forget to call this if overridden...
313 *
314 * @static
315 * @return void
316 */
317 public static function tearDownAfterClass() {
318 phpunit_util::reset_all_data();
319 }
320
321 /**
322 * Reset all database tables, restore global state and clear caches and optionally purge dataroot dir.
323 * @static
324 * @return void
325 */
326 public static function resetAllData() {
327 phpunit_util::reset_all_data();
328 }
329
330 /**
331 * Set current $USER, reset access cache.
332 * @static
d59ef9c5 333 * @param null|int|stdClass $user user record, null or 0 means non-logged-in, positive integer means userid
7e7cfe7a
PS
334 * @return void
335 */
336 public static function setUser($user = null) {
337 global $CFG, $DB;
338
339 if (is_object($user)) {
340 $user = clone($user);
341 } else if (!$user) {
342 $user = new stdClass();
343 $user->id = 0;
344 $user->mnethostid = $CFG->mnet_localhost_id;
345 } else {
346 $user = $DB->get_record('user', array('id'=>$user));
347 }
348 unset($user->description);
349 unset($user->access);
d59ef9c5 350 unset($user->preference);
7e7cfe7a
PS
351
352 session_set_user($user);
353 }
354
d59ef9c5
PS
355 /**
356 * Set current $USER to admin account, reset access cache.
357 * @static
358 * @return void
359 */
360 public static function setAdminUser() {
361 self::setUser(2);
362 }
363
364 /**
365 * Set current $USER to guest account, reset access cache.
366 * @static
367 * @return void
368 */
369 public static function setGuestUser() {
370 self::setUser(1);
371 }
372
7e7cfe7a
PS
373 /**
374 * Get data generator
375 * @static
376 * @return phpunit_data_generator
377 */
378 public static function getDataGenerator() {
379 return phpunit_util::get_data_generator();
380 }
381
382 /**
383 * Recursively visit all the files in the source tree. Calls the callback
384 * function with the pathname of each file found.
385 *
386 * @param string $path the folder to start searching from.
387 * @param string $callback the method of this class to call with the name of each file found.
388 * @param string $fileregexp a regexp used to filter the search (optional).
389 * @param bool $exclude If true, pathnames that match the regexp will be ignored. If false,
390 * only files that match the regexp will be included. (default false).
391 * @param array $ignorefolders will not go into any of these folders (optional).
392 * @return void
393 */
394 public function recurseFolders($path, $callback, $fileregexp = '/.*/', $exclude = false, $ignorefolders = array()) {
395 $files = scandir($path);
396
397 foreach ($files as $file) {
398 $filepath = $path .'/'. $file;
399 if (strpos($file, '.') === 0) {
400 /// Don't check hidden files.
401 continue;
402 } else if (is_dir($filepath)) {
403 if (!in_array($filepath, $ignorefolders)) {
404 $this->recurseFolders($filepath, $callback, $fileregexp, $exclude, $ignorefolders);
405 }
406 } else if ($exclude xor preg_match($fileregexp, $filepath)) {
407 $this->$callback($filepath);
408 }
409 }
410 }
411}