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