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