Merge branch 'MDL-52284-master' of git://github.com/marinaglancy/moodle
[moodle.git] / lib / phpunit / classes / advanced_testcase.php
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/>.
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  */
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  */
35 abstract class advanced_testcase extends base_testcase {
36     /** @var bool automatically reset everything? null means log changes */
37     private $resetAfterTest;
39     /** @var moodle_transaction */
40     private $testdbtransaction;
42     /** @var int timestamp used for current time asserts */
43     private $currenttimestart;
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);
57         $this->setBackupGlobals(false);
58         $this->setBackupStaticAttributes(false);
59         $this->setRunTestInSeparateProcess(false);
60     }
62     /**
63      * Runs the bare test sequence.
64      * @return void
65      */
66     final public function runBare() {
67         global $DB;
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;
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         }
78         try {
79             $this->setCurrentTimeStart();
80             parent::runBare();
81             // set DB reference in case somebody mocked it in test
82             $DB = phpunit_util::get_global_backup('DB');
84             // Deal with any debugging messages.
85             $debugerror = phpunit_util::display_debugging_messages();
86             $this->resetDebugging();
87             if ($debugerror) {
88                 trigger_error('Unexpected debugging() call detected.', E_USER_NOTICE);
89             }
91         } catch (Exception $ex) {
92             $e = $ex;
93         } catch (Throwable $ex) {
94             // Engine errors in PHP7 throw exceptions of type Throwable (this "catch" will be ignored in PHP5).
95             $e = $ex;
96         }
98         if (isset($e)) {
99             // cleanup after failed expectation
100             self::resetAllData();
101             throw $e;
102         }
104         if (!$this->testdbtransaction or $this->testdbtransaction->is_disposed()) {
105             $this->testdbtransaction = null;
106         }
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             }
114             self::resetAllData(null);
116         } else if ($this->resetAfterTest === false) {
117             if ($this->testdbtransaction) {
118                 $this->testdbtransaction->allow_commit();
119             }
120             // keep all data untouched for other tests
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) {
128                     self::resetAllData();
129                     throw new coding_exception('Invalid transaction state detected in test '.$this->getName());
130                 }
131             }
132             self::resetAllData(true);
133         }
135         // make sure test did not forget to close transaction
136         if ($DB->is_transaction_started()) {
137             self::resetAllData();
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     }
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     }
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     }
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     }
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     }
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;
204         $structure = phpunit_util::get_tablestructure();
206         foreach($dataset->getTableNames() as $tablename) {
207             $table = $dataset->getTable($tablename);
208             $metadata = $dataset->getTableMetaData($tablename);
209             $columns = $metadata->getColumns();
211             $doimport = false;
212             if (isset($structure[$tablename]['id']) and $structure[$tablename]['id']->auto_increment) {
213                 $doimport = in_array('id', $columns);
214             }
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     }
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     }
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     }
256     /**
257      * Return debugging messages from the current test.
258      * @return array with instances having 'message', 'level' and 'stacktrace' property.
259      */
260     public function getDebuggingMessages() {
261         return phpunit_util::get_debugging_messages();
262     }
264     /**
265      * Clear all previous debugging messages in current test
266      * and revert to default DEVELOPER_DEBUG level.
267      */
268     public function resetDebugging() {
269         phpunit_util::reset_debugging();
270     }
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 = '') {
282         $debugging = $this->getDebuggingMessages();
283         $count = count($debugging);
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);
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         }
307         $this->resetDebugging();
308     }
310     /**
311      * Call when no debugging() messages expected.
312      * @param string $message
313      */
314     public function assertDebuggingNotCalled($message = '') {
315         $debugging = $this->getDebuggingMessages();
316         $count = count($debugging);
318         if ($message === '') {
319             $message = 'Expectation failed, debugging() was triggered.';
320         }
321         $this->assertEquals(0, $count, $message);
322     }
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     }
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     }
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         }
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);
380         // Restore event->context.
381         phpunit_event_mock::testable_set_event_context($event, $eventcontext);
382     }
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     }
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     }
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     }
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     }
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     }
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() {
456         self::resetAllData();
457     }
460     /**
461      * Reset all database tables, restore global state and clear caches and optionally purge dataroot dir.
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
467      * @return void
468      */
469     public static function resetAllData($detectchanges = false) {
470         phpunit_util::reset_all_data($detectchanges);
471     }
473     /**
474      * Set current $USER, reset access cache.
475      * @static
476      * @param null|int|stdClass $user user record, null or 0 means non-logged-in, positive integer means userid
477      * @return void
478      */
479     public static function setUser($user = null) {
480         global $CFG, $DB;
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);
493         unset($user->preference);
495         \core\session\manager::set_user($user);
496     }
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     }
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     }
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     }
529     /**
530      * Get data generator
531      * @static
532      * @return testing_data_generator
533      */
534     public static function getDataGenerator() {
535         return phpunit_util::get_data_generator();
536     }
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         }
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     }
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);
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     }