MDL-43736 Events: event->contextinstanceid should be used where possible
[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
2a67e105
PS
42 /** @var int timestamp used for current time asserts */
43 private $currenttimestart;
44
7e7cfe7a
PS
45 /**
46 * Constructs a test case with the given name.
47 *
48 * Note: use setUp() or setUpBeforeClass() in your test cases.
49 *
50 * @param string $name
51 * @param array $data
52 * @param string $dataName
53 */
54 final public function __construct($name = null, array $data = array(), $dataName = '') {
55 parent::__construct($name, $data, $dataName);
56
57 $this->setBackupGlobals(false);
58 $this->setBackupStaticAttributes(false);
59 $this->setRunTestInSeparateProcess(false);
60 }
61
62 /**
63 * Runs the bare test sequence.
64 * @return void
65 */
66 final public function runBare() {
67 global $DB;
68
69 if (phpunit_util::$lastdbwrites != $DB->perf_get_writes()) {
70 // this happens when previous test does not reset, we can not use transactions
71 $this->testdbtransaction = null;
72
73 } else if ($DB->get_dbfamily() === 'postgres' or $DB->get_dbfamily() === 'mssql') {
74 // database must allow rollback of DDL, so no mysql here
75 $this->testdbtransaction = $DB->start_delegated_transaction();
76 }
77
78 try {
2a67e105 79 $this->setCurrentTimeStart();
7e7cfe7a
PS
80 parent::runBare();
81 // set DB reference in case somebody mocked it in test
82 $DB = phpunit_util::get_global_backup('DB');
ef5b5e05
PS
83
84 // Deal with any debugging messages.
85 phpunit_util::display_debugging_messages();
86 phpunit_util::reset_debugging();
87
7e7cfe7a
PS
88 } catch (Exception $e) {
89 // cleanup after failed expectation
90 phpunit_util::reset_all_data();
91 throw $e;
92 }
93
94 if (!$this->testdbtransaction or $this->testdbtransaction->is_disposed()) {
95 $this->testdbtransaction = null;
96 }
97
98 if ($this->resetAfterTest === true) {
99 if ($this->testdbtransaction) {
100 $DB->force_transaction_rollback();
101 phpunit_util::reset_all_database_sequences();
102 phpunit_util::$lastdbwrites = $DB->perf_get_writes(); // no db reset necessary
103 }
71fc5003 104 phpunit_util::reset_all_data(null);
7e7cfe7a
PS
105
106 } else if ($this->resetAfterTest === false) {
107 if ($this->testdbtransaction) {
108 $this->testdbtransaction->allow_commit();
109 }
110 // keep all data untouched for other tests
111
112 } else {
113 // reset but log what changed
114 if ($this->testdbtransaction) {
115 try {
116 $this->testdbtransaction->allow_commit();
117 } catch (dml_transaction_exception $e) {
118 phpunit_util::reset_all_data();
119 throw new coding_exception('Invalid transaction state detected in test '.$this->getName());
120 }
121 }
122 phpunit_util::reset_all_data(true);
123 }
124
125 // make sure test did not forget to close transaction
126 if ($DB->is_transaction_started()) {
127 phpunit_util::reset_all_data();
128 if ($this->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_PASSED
129 or $this->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_SKIPPED
130 or $this->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_INCOMPLETE) {
131 throw new coding_exception('Test '.$this->getName().' did not close database transaction');
132 }
133 }
134 }
135
136 /**
137 * Creates a new FlatXmlDataSet with the given $xmlFile. (absolute path.)
138 *
139 * @param string $xmlFile
140 * @return PHPUnit_Extensions_Database_DataSet_FlatXmlDataSet
141 */
142 protected function createFlatXMLDataSet($xmlFile) {
143 return new PHPUnit_Extensions_Database_DataSet_FlatXmlDataSet($xmlFile);
144 }
145
146 /**
147 * Creates a new XMLDataSet with the given $xmlFile. (absolute path.)
148 *
149 * @param string $xmlFile
150 * @return PHPUnit_Extensions_Database_DataSet_XmlDataSet
151 */
152 protected function createXMLDataSet($xmlFile) {
153 return new PHPUnit_Extensions_Database_DataSet_XmlDataSet($xmlFile);
154 }
155
156 /**
157 * Creates a new CsvDataSet from the given array of csv files. (absolute paths.)
158 *
159 * @param array $files array tablename=>cvsfile
160 * @param string $delimiter
161 * @param string $enclosure
162 * @param string $escape
163 * @return PHPUnit_Extensions_Database_DataSet_CsvDataSet
164 */
165 protected function createCsvDataSet($files, $delimiter = ',', $enclosure = '"', $escape = '"') {
166 $dataSet = new PHPUnit_Extensions_Database_DataSet_CsvDataSet($delimiter, $enclosure, $escape);
167 foreach($files as $table=>$file) {
168 $dataSet->addTable($table, $file);
169 }
170 return $dataSet;
171 }
172
173 /**
174 * Creates new ArrayDataSet from given array
175 *
176 * @param array $data array of tables, first row in each table is columns
177 * @return phpunit_ArrayDataSet
178 */
179 protected function createArrayDataSet(array $data) {
180 return new phpunit_ArrayDataSet($data);
181 }
182
183 /**
184 * Load date into moodle database tables from standard PHPUnit data set.
185 *
186 * Note: it is usually better to use data generators
187 *
188 * @param PHPUnit_Extensions_Database_DataSet_IDataSet $dataset
189 * @return void
190 */
191 protected function loadDataSet(PHPUnit_Extensions_Database_DataSet_IDataSet $dataset) {
192 global $DB;
193
194 $structure = phpunit_util::get_tablestructure();
195
196 foreach($dataset->getTableNames() as $tablename) {
197 $table = $dataset->getTable($tablename);
198 $metadata = $dataset->getTableMetaData($tablename);
199 $columns = $metadata->getColumns();
200
201 $doimport = false;
202 if (isset($structure[$tablename]['id']) and $structure[$tablename]['id']->auto_increment) {
203 $doimport = in_array('id', $columns);
204 }
205
206 for($r=0; $r<$table->getRowCount(); $r++) {
207 $record = $table->getRow($r);
208 if ($doimport) {
209 $DB->import_record($tablename, $record);
210 } else {
211 $DB->insert_record($tablename, $record);
212 }
213 }
214 if ($doimport) {
215 $DB->get_manager()->reset_sequence(new xmldb_table($tablename));
216 }
217 }
218 }
219
220 /**
221 * Call this method from test if you want to make sure that
222 * the resetting of database is done the slow way without transaction
223 * rollback.
224 *
225 * This is useful especially when testing stuff that is not compatible with transactions.
226 *
227 * @return void
228 */
229 public function preventResetByRollback() {
230 if ($this->testdbtransaction and !$this->testdbtransaction->is_disposed()) {
231 $this->testdbtransaction->allow_commit();
232 $this->testdbtransaction = null;
233 }
234 }
235
236 /**
237 * Reset everything after current test.
238 * @param bool $reset true means reset state back, false means keep all data for the next test,
239 * null means reset state and show warnings if anything changed
240 * @return void
241 */
242 public function resetAfterTest($reset = true) {
243 $this->resetAfterTest = $reset;
244 }
245
ef5b5e05
PS
246 /**
247 * Return debugging messages from the current test.
94c9db54 248 * @return array with instances having 'message', 'level' and 'stacktrace' property.
ef5b5e05
PS
249 */
250 public function getDebuggingMessages() {
251 return phpunit_util::get_debugging_messages();
252 }
253
254 /**
96f81ea3
PS
255 * Clear all previous debugging messages in current test
256 * and revert to default DEVELOPER_DEBUG level.
ef5b5e05
PS
257 */
258 public function resetDebugging() {
94c9db54 259 phpunit_util::reset_debugging();
ef5b5e05
PS
260 }
261
262 /**
263 * Assert that exactly debugging was just called once.
264 *
265 * Discards the debugging message if successful.
266 *
267 * @param null|string $debugmessage null means any
268 * @param null|string $debuglevel null means any
269 * @param string $message
270 */
271 public function assertDebuggingCalled($debugmessage = null, $debuglevel = null, $message = '') {
272 $debugging = phpunit_util::get_debugging_messages();
273 $count = count($debugging);
274
275 if ($count == 0) {
276 if ($message === '') {
277 $message = 'Expectation failed, debugging() not triggered.';
278 }
279 $this->fail($message);
280 }
281 if ($count > 1) {
282 if ($message === '') {
283 $message = 'Expectation failed, debugging() triggered '.$count.' times.';
284 }
285 $this->fail($message);
286 }
287 $this->assertEquals(1, $count);
288
289 $debug = reset($debugging);
290 if ($debugmessage !== null) {
291 $this->assertSame($debugmessage, $debug->message, $message);
292 }
293 if ($debuglevel !== null) {
294 $this->assertSame($debuglevel, $debug->level, $message);
295 }
296
297 phpunit_util::reset_debugging();
298 }
299
94c9db54
PS
300 /**
301 * Call when no debugging() messages expected.
302 * @param string $message
303 */
ef5b5e05
PS
304 public function assertDebuggingNotCalled($message = '') {
305 $debugging = phpunit_util::get_debugging_messages();
306 $count = count($debugging);
307
308 if ($message === '') {
309 $message = 'Expectation failed, debugging() was triggered.';
310 }
311 $this->assertEquals(0, $count, $message);
312 }
313
d6277b0c
FM
314 /**
315 * Assert that an event legacy data is equal to the expected value.
316 *
317 * @param mixed $expected expected data.
318 * @param \core\event\base $event the event object.
319 * @param string $message
320 * @return void
321 */
322 public function assertEventLegacyData($expected, \core\event\base $event, $message = '') {
323 $legacydata = phpunit_event_mock::testable_get_legacy_eventdata($event);
324 if ($message === '') {
325 $message = 'Event legacy data does not match expected value.';
326 }
327 $this->assertEquals($expected, $legacydata, $message);
328 }
329
330 /**
331 * Assert that an event legacy log data is equal to the expected value.
332 *
333 * @param mixed $expected expected data.
334 * @param \core\event\base $event the event object.
335 * @param string $message
336 * @return void
337 */
338 public function assertEventLegacyLogData($expected, \core\event\base $event, $message = '') {
339 $legacydata = phpunit_event_mock::testable_get_legacy_logdata($event);
340 if ($message === '') {
341 $message = 'Event legacy log data does not match expected value.';
342 }
343 $this->assertEquals($expected, $legacydata, $message);
344 }
345
2a67e105
PS
346 /**
347 * Stores current time as the base for assertTimeCurrent().
348 *
349 * Note: this is called automatically before calling individual test methods.
350 * @return int current time
351 */
352 public function setCurrentTimeStart() {
353 $this->currenttimestart = time();
354 return $this->currenttimestart;
355 }
356
357 /**
358 * Assert that: start < $time < time()
359 * @param int $time
360 * @param string $message
361 * @return void
362 */
363 public function assertTimeCurrent($time, $message = '') {
364 $msg = ($message === '') ? 'Time is lower that allowed start value' : $message;
365 $this->assertGreaterThanOrEqual($this->currenttimestart, $time, $msg);
366 $msg = ($message === '') ? 'Time is in the future' : $message;
367 $this->assertLessThanOrEqual(time(), $time, $msg);
368 }
d6277b0c 369
4c9e03f0
PS
370 /**
371 * Starts message redirection.
372 *
373 * You can verify if messages were sent or not by inspecting the messages
374 * array in the returned messaging sink instance. The redirection
375 * can be stopped by calling $sink->close();
376 *
377 * @return phpunit_message_sink
378 */
379 public function redirectMessages() {
380 return phpunit_util::start_message_redirection();
381 }
382
1aba6b2b
AN
383 /**
384 * Starts email redirection.
385 *
386 * You can verify if email were sent or not by inspecting the email
387 * array in the returned phpmailer sink instance. The redirection
388 * can be stopped by calling $sink->close();
389 *
390 * @return phpunit_message_sink
391 */
392 public function redirectEmails() {
393 return phpunit_util::start_phpmailer_redirection();
394 }
395
62401e8f
PS
396 /**
397 * Starts event redirection.
398 *
399 * You can verify if events were triggered or not by inspecting the events
400 * array in the returned event sink instance. The redirection
401 * can be stopped by calling $sink->close();
402 *
403 * @return phpunit_event_sink
404 */
405 public function redirectEvents() {
406 return phpunit_util::start_event_redirection();
407 }
408
7e7cfe7a
PS
409 /**
410 * Cleanup after all tests are executed.
411 *
412 * Note: do not forget to call this if overridden...
413 *
414 * @static
415 * @return void
416 */
417 public static function tearDownAfterClass() {
418 phpunit_util::reset_all_data();
419 }
420
421 /**
422 * Reset all database tables, restore global state and clear caches and optionally purge dataroot dir.
423 * @static
424 * @return void
425 */
426 public static function resetAllData() {
427 phpunit_util::reset_all_data();
428 }
429
430 /**
431 * Set current $USER, reset access cache.
432 * @static
d59ef9c5 433 * @param null|int|stdClass $user user record, null or 0 means non-logged-in, positive integer means userid
7e7cfe7a
PS
434 * @return void
435 */
436 public static function setUser($user = null) {
437 global $CFG, $DB;
438
439 if (is_object($user)) {
440 $user = clone($user);
441 } else if (!$user) {
442 $user = new stdClass();
443 $user->id = 0;
444 $user->mnethostid = $CFG->mnet_localhost_id;
445 } else {
446 $user = $DB->get_record('user', array('id'=>$user));
447 }
448 unset($user->description);
449 unset($user->access);
d59ef9c5 450 unset($user->preference);
7e7cfe7a 451
d79d5ac2 452 \core\session\manager::set_user($user);
7e7cfe7a
PS
453 }
454
d59ef9c5
PS
455 /**
456 * Set current $USER to admin account, reset access cache.
457 * @static
458 * @return void
459 */
460 public static function setAdminUser() {
461 self::setUser(2);
462 }
463
464 /**
465 * Set current $USER to guest account, reset access cache.
466 * @static
467 * @return void
468 */
469 public static function setGuestUser() {
470 self::setUser(1);
471 }
472
7e7cfe7a
PS
473 /**
474 * Get data generator
475 * @static
5c3c2c81 476 * @return testing_data_generator
7e7cfe7a
PS
477 */
478 public static function getDataGenerator() {
479 return phpunit_util::get_data_generator();
480 }
481
a9d2f1b4
PS
482 /**
483 * Returns UTL of the external test file.
484 *
485 * The result depends on the value of following constants:
486 * - TEST_EXTERNAL_FILES_HTTP_URL
487 * - TEST_EXTERNAL_FILES_HTTPS_URL
488 *
489 * They should point to standard external test files repository,
490 * it defaults to 'http://download.moodle.org/unittest'.
491 *
492 * False value means skip tests that require external files.
493 *
494 * @param string $path
495 * @param bool $https true if https required
496 * @return string url
497 */
498 public function getExternalTestFileUrl($path, $https = false) {
499 $path = ltrim($path, '/');
500 if ($path) {
501 $path = '/'.$path;
502 }
503 if ($https) {
504 if (defined('TEST_EXTERNAL_FILES_HTTPS_URL')) {
505 if (!TEST_EXTERNAL_FILES_HTTPS_URL) {
506 $this->markTestSkipped('Tests using external https test files are disabled');
507 }
508 return TEST_EXTERNAL_FILES_HTTPS_URL.$path;
509 }
510 return 'https://download.moodle.org/unittest'.$path;
511 }
512
513 if (defined('TEST_EXTERNAL_FILES_HTTP_URL')) {
514 if (!TEST_EXTERNAL_FILES_HTTP_URL) {
515 $this->markTestSkipped('Tests using external http test files are disabled');
516 }
517 return TEST_EXTERNAL_FILES_HTTP_URL.$path;
518 }
519 return 'http://download.moodle.org/unittest'.$path;
520 }
521
7e7cfe7a
PS
522 /**
523 * Recursively visit all the files in the source tree. Calls the callback
524 * function with the pathname of each file found.
525 *
526 * @param string $path the folder to start searching from.
527 * @param string $callback the method of this class to call with the name of each file found.
528 * @param string $fileregexp a regexp used to filter the search (optional).
529 * @param bool $exclude If true, pathnames that match the regexp will be ignored. If false,
530 * only files that match the regexp will be included. (default false).
531 * @param array $ignorefolders will not go into any of these folders (optional).
532 * @return void
533 */
534 public function recurseFolders($path, $callback, $fileregexp = '/.*/', $exclude = false, $ignorefolders = array()) {
535 $files = scandir($path);
536
537 foreach ($files as $file) {
538 $filepath = $path .'/'. $file;
539 if (strpos($file, '.') === 0) {
540 /// Don't check hidden files.
541 continue;
542 } else if (is_dir($filepath)) {
543 if (!in_array($filepath, $ignorefolders)) {
544 $this->recurseFolders($filepath, $callback, $fileregexp, $exclude, $ignorefolders);
545 }
546 } else if ($exclude xor preg_match($fileregexp, $filepath)) {
547 $this->$callback($filepath);
548 }
549 }
550 }
551}