MDL-66568 behat: Support building of themes for behat
[moodle.git] / lib / behat / classes / util.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  * Utils for behat-related stuff
19  *
20  * @package    core
21  * @category   test
22  * @copyright  2012 David MonllaĆ³
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
28 require_once(__DIR__ . '/../lib.php');
29 require_once(__DIR__ . '/../../testing/classes/util.php');
30 require_once(__DIR__ . '/behat_command.php');
31 require_once(__DIR__ . '/behat_config_manager.php');
33 require_once(__DIR__ . '/../../filelib.php');
34 require_once(__DIR__ . '/../../clilib.php');
35 require_once(__DIR__ . '/../../csslib.php');
37 use Behat\Mink\Session;
38 use Behat\Mink\Exception\ExpectationException;
40 /**
41  * Init/reset utilities for Behat database and dataroot
42  *
43  * @package   core
44  * @category  test
45  * @copyright 2013 David MonllaĆ³
46  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
47  */
48 class behat_util extends testing_util {
50     /**
51      * The behat test site fullname and shortname.
52      */
53     const BEHATSITENAME = "Acceptance test site";
55     /**
56      * @var array Files to skip when resetting dataroot folder
57      */
58     protected static $datarootskiponreset = array('.', '..', 'behat', 'behattestdir.txt');
60     /**
61      * @var array Files to skip when dropping dataroot folder
62      */
63     protected static $datarootskipondrop = array('.', '..', 'lock');
65     /**
66      * Installs a site using $CFG->dataroot and $CFG->prefix
67      * @throws coding_exception
68      * @return void
69      */
70     public static function install_site() {
71         global $DB, $CFG;
72         require_once($CFG->dirroot.'/user/lib.php');
73         if (!defined('BEHAT_UTIL')) {
74             throw new coding_exception('This method can be only used by Behat CLI tool');
75         }
77         $tables = $DB->get_tables(false);
78         if (!empty($tables)) {
79             behat_error(BEHAT_EXITCODE_INSTALLED);
80         }
82         // New dataroot.
83         self::reset_dataroot();
85         $options = array();
86         $options['adminuser'] = 'admin';
87         $options['adminpass'] = 'admin';
88         $options['fullname'] = self::BEHATSITENAME;
89         $options['shortname'] = self::BEHATSITENAME;
91         install_cli_database($options, false);
93         // We need to keep the installed dataroot filedir files.
94         // So each time we reset the dataroot before running a test, the default files are still installed.
95         self::save_original_data_files();
97         $frontpagesummary = new admin_setting_special_frontpagedesc();
98         $frontpagesummary->write_setting(self::BEHATSITENAME);
100         // Update admin user info.
101         $user = $DB->get_record('user', array('username' => 'admin'));
102         $user->email = 'moodle@example.com';
103         $user->firstname = 'Admin';
104         $user->lastname = 'User';
105         $user->city = 'Perth';
106         $user->country = 'AU';
107         user_update_user($user, false);
109         // Disable email message processor.
110         $DB->set_field('message_processors', 'enabled', '0', array('name' => 'email'));
112         // Sets maximum debug level.
113         set_config('debug', DEBUG_DEVELOPER);
114         set_config('debugdisplay', 1);
116         // Disable some settings that are not wanted on test sites.
117         set_config('noemailever', 1);
119         // Enable web cron.
120         set_config('cronclionly', 0);
122         // Set editor autosave to high value, so as to avoid unwanted ajax.
123         set_config('autosavefrequency', '604800', 'editor_atto');
125         // Set noreplyaddress to an example domain, as it should be valid email address and test site can be a localhost.
126         set_config('noreplyaddress', 'noreply@example.com');
128         // Keeps the current version of database and dataroot.
129         self::store_versions_hash();
131         // Stores the database contents for fast reset.
132         self::store_database_state();
133     }
135     /**
136      * Build theme CSS.
137      */
138     public static function build_themes() {
139         global $CFG;
140         require_once("{$CFG->libdir}/outputlib.php");
142         $themenames = array_keys(\core_component::get_plugin_list('theme'));
144         // Load the theme configs.
145         $themeconfigs = array_map(function($themename) {
146             return \theme_config::load($themename);
147         }, $themenames);
149         // Build the list of themes and cache them in local cache.
150         $themes = theme_build_css_for_themes($themeconfigs, ['ltr'], true);
152         $framework = self::get_framework();
153         $storageroot = self::get_dataroot() . "/{$framework}/themedata";
155         foreach ($themes as $themename => $themedata) {
156             $dirname = "{$storageroot}/{$themename}";
157             check_dir_exists($dirname);
158             foreach ($themedata as $direction => $css) {
159                 file_put_contents("{$dirname}/{$direction}.css", $css);
160             }
161         }
162     }
164     /**
165      * Drops dataroot and remove test database tables
166      * @throws coding_exception
167      * @return void
168      */
169     public static function drop_site() {
171         if (!defined('BEHAT_UTIL')) {
172             throw new coding_exception('This method can be only used by Behat CLI tool');
173         }
175         self::reset_dataroot();
176         self::drop_database(true);
177         self::drop_dataroot();
178     }
180     /**
181      * Delete files and directories under dataroot.
182      */
183     public static function drop_dataroot() {
184         global $CFG;
186         // As behat directory is now created under default $CFG->behat_dataroot_parent, so remove the whole dir.
187         if ($CFG->behat_dataroot !== $CFG->behat_dataroot_parent) {
188             remove_dir($CFG->behat_dataroot, false);
189         } else {
190             // It should never come here.
191             throw new moodle_exception("Behat dataroot should not be same as parent behat data root.");
192         }
193     }
195     /**
196      * Checks if $CFG->behat_wwwroot is available and using same versions for cli and web.
197      *
198      * @return void
199      */
200     public static function check_server_status() {
201         global $CFG;
203         $url = $CFG->behat_wwwroot . '/admin/tool/behat/tests/behat/fixtures/environment.php';
205         // Get web versions used by behat site.
206         $ch = curl_init($url);
207         curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
208         $result = curl_exec($ch);
209         $statuscode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
210         curl_close($ch);
212         if ($statuscode !== 200 || empty($result) || (!$result = json_decode($result, true))) {
214             behat_error (BEHAT_EXITCODE_REQUIREMENT, $CFG->behat_wwwroot . ' is not available, ensure you specified ' .
215                 'correct url and that the server is set up and started.' . PHP_EOL . ' More info in ' .
216                 behat_command::DOCS_URL . PHP_EOL);
217         }
219         // Check if cli version is same as web version.
220         $clienv = self::get_environment();
221         if ($result != $clienv) {
222             $output = 'Differences detected between cli and webserver...'.PHP_EOL;
223             foreach ($result as $key => $version) {
224                 if ($clienv[$key] != $version) {
225                     $output .= ' ' . $key . ': ' . PHP_EOL;
226                     $output .= ' - web server: ' . $version . PHP_EOL;
227                     $output .= ' - cli: ' . $clienv[$key] . PHP_EOL;
228                 }
229             }
230             echo $output;
231             ob_flush();
232         }
233     }
235     /**
236      * Checks whether the test database and dataroot is ready
237      * Stops execution if something went wrong
238      * @throws coding_exception
239      * @return void
240      */
241     protected static function test_environment_problem() {
242         global $CFG, $DB;
244         if (!defined('BEHAT_UTIL')) {
245             throw new coding_exception('This method can be only used by Behat CLI tool');
246         }
248         if (!self::is_test_site()) {
249             behat_error(1, 'This is not a behat test site!');
250         }
252         $tables = $DB->get_tables(false);
253         if (empty($tables)) {
254             behat_error(BEHAT_EXITCODE_INSTALL, '');
255         }
257         if (!self::is_test_data_updated()) {
258             behat_error(BEHAT_EXITCODE_REINSTALL, 'The test environment was initialised for a different version');
259         }
260     }
262     /**
263      * Enables test mode
264      *
265      * It uses CFG->behat_dataroot
266      *
267      * Starts the test mode checking the composer installation and
268      * the test environment and updating the available
269      * features and steps definitions.
270      *
271      * Stores a file in dataroot/behat to allow Moodle to switch
272      * to the test environment when using cli-server.
273      * @param bool $themesuitewithallfeatures List themes to include core features.
274      * @param string $tags comma separated tag, which will be given preference while distributing features in parallel run.
275      * @param int $parallelruns number of parallel runs.
276      * @param int $run current run.
277      * @throws coding_exception
278      * @return void
279      */
280     public static function start_test_mode($themesuitewithallfeatures = false, $tags = '', $parallelruns = 0, $run = 0) {
281         global $CFG;
283         if (!defined('BEHAT_UTIL')) {
284             throw new coding_exception('This method can be only used by Behat CLI tool');
285         }
287         // Checks the behat set up and the PHP version.
288         if ($errorcode = behat_command::behat_setup_problem()) {
289             exit($errorcode);
290         }
292         // Check that test environment is correctly set up.
293         self::test_environment_problem();
295         // Updates all the Moodle features and steps definitions.
296         behat_config_manager::update_config_file('', true, $tags, $themesuitewithallfeatures, $parallelruns, $run);
298         if (self::is_test_mode_enabled()) {
299             return;
300         }
302         $contents = '$CFG->behat_wwwroot, $CFG->behat_prefix and $CFG->behat_dataroot' .
303             ' are currently used as $CFG->wwwroot, $CFG->prefix and $CFG->dataroot';
304         $filepath = self::get_test_file_path();
305         if (!file_put_contents($filepath, $contents)) {
306             behat_error(BEHAT_EXITCODE_PERMISSIONS, 'File ' . $filepath . ' can not be created');
307         }
308     }
310     /**
311      * Returns the status of the behat test environment
312      *
313      * @return int Error code
314      */
315     public static function get_behat_status() {
317         if (!defined('BEHAT_UTIL')) {
318             throw new coding_exception('This method can be only used by Behat CLI tool');
319         }
321         // Checks the behat set up and the PHP version, returning an error code if something went wrong.
322         if ($errorcode = behat_command::behat_setup_problem()) {
323             return $errorcode;
324         }
326         // Check that test environment is correctly set up, stops execution.
327         self::test_environment_problem();
328     }
330     /**
331      * Disables test mode
332      * @throws coding_exception
333      * @return void
334      */
335     public static function stop_test_mode() {
337         if (!defined('BEHAT_UTIL')) {
338             throw new coding_exception('This method can be only used by Behat CLI tool');
339         }
341         $testenvfile = self::get_test_file_path();
342         behat_config_manager::set_behat_run_config_value('behatsiteenabled', 0);
344         if (!self::is_test_mode_enabled()) {
345             echo "Test environment was already disabled\n";
346         } else {
347             if (!unlink($testenvfile)) {
348                 behat_error(BEHAT_EXITCODE_PERMISSIONS, 'Can not delete test environment file');
349             }
350         }
351     }
353     /**
354      * Checks whether test environment is enabled or disabled
355      *
356      * To check is the current script is running in the test
357      * environment
358      *
359      * @return bool
360      */
361     public static function is_test_mode_enabled() {
363         $testenvfile = self::get_test_file_path();
364         if (file_exists($testenvfile)) {
365             return true;
366         }
368         return false;
369     }
371     /**
372      * Returns the path to the file which specifies if test environment is enabled
373      * @return string
374      */
375     public final static function get_test_file_path() {
376         return behat_command::get_parent_behat_dir() . '/test_environment_enabled.txt';
377     }
379     /**
380      * Removes config settings that were added to the main $CFG config within the Behat CLI
381      * run.
382      *
383      * Database storage is already handled by reset_database and existing config values will
384      * be reset automatically by initialise_cfg(), so we only need to remove added ones.
385      */
386     public static function remove_added_config() {
387         global $CFG;
388         if (!empty($CFG->behat_cli_added_config)) {
389             foreach ($CFG->behat_cli_added_config as $key => $value) {
390                 unset($CFG->{$key});
391             }
392             unset($CFG->behat_cli_added_config);
393         }
394     }
396     /**
397      * Reset contents of all database tables to initial values, reset caches, etc.
398      */
399     public static function reset_all_data() {
400         // Reset database.
401         self::reset_database();
403         // Purge dataroot directory.
404         self::reset_dataroot();
406         // Reset all static caches.
407         accesslib_clear_all_caches(true);
408         accesslib_reset_role_cache();
409         // Reset the nasty strings list used during the last test.
410         nasty_strings::reset_used_strings();
412         filter_manager::reset_caches();
414         // Reset course and module caches.
415         if (class_exists('format_base')) {
416             // If file containing class is not loaded, there is no cache there anyway.
417             format_base::reset_course_cache(0);
418         }
419         get_fast_modinfo(0, 0, true);
421         // Inform data generator.
422         self::get_data_generator()->reset();
424         // Initialise $CFG with default values. This is needed for behat cli process, so we don't have modified
425         // $CFG values from the old run. @see set_config.
426         self::remove_added_config();
427         initialise_cfg();
428     }
430     /**
431      * Pause execution immediately.
432      *
433      * @param Session $session
434      * @param string $message The message to show when pausing.
435      * This will be passed through cli_ansi_format so appropriate ANSI formatting and features are available.
436      */
437     public static function pause(Session $session, string $message): void {
438         $posixexists = function_exists('posix_isatty');
440         // Make sure this step is only used with interactive terminal (if detected).
441         if ($posixexists && !@posix_isatty(STDOUT)) {
442             throw new ExpectationException('Break point should only be used with interactive terminal.', $session);
443         }
445         // Save the cursor position, ring the bell, and add a new line.
446         fwrite(STDOUT, cli_ansi_format("<cursor:save><bell><newline>"));
448         // Output the formatted message and reset colour back to normal.
449         $formattedmessage = cli_ansi_format("{$message}<colour:normal>");
450         fwrite(STDOUT, $formattedmessage);
452         // Wait for input.
453         fread(STDIN, 1024);
455         // Move the cursor back up to the previous position, then restore the original position stored earlier, and move
456         // it back down again.
457         fwrite(STDOUT, cli_ansi_format("<cursor:up><cursor:up><cursor:restore><cursor:down><cursor:down>"));
459         // Add any extra lines back if the provided message was spread over multiple lines.
460         $linecount = count(explode("\n", $formattedmessage));
461         fwrite(STDOUT, str_repeat(cli_ansi_format("<cursor:down>"), $linecount - 1));
462     }