2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * Utils for behat-related stuff
22 * @copyright 2012 David Monllaó
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
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');
36 use Behat\Mink\Session;
37 use Behat\Mink\Exception\ExpectationException;
40 * Init/reset utilities for Behat database and dataroot
44 * @copyright 2013 David Monllaó
45 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
47 class behat_util extends testing_util {
50 * The behat test site fullname and shortname.
52 const BEHATSITENAME = "Acceptance test site";
55 * @var array Files to skip when resetting dataroot folder
57 protected static $datarootskiponreset = array('.', '..', 'behat', 'behattestdir.txt');
60 * @var array Files to skip when dropping dataroot folder
62 protected static $datarootskipondrop = array('.', '..', 'lock');
65 * Installs a site using $CFG->dataroot and $CFG->prefix
66 * @throws coding_exception
69 public static function install_site() {
71 require_once($CFG->dirroot.'/user/lib.php');
72 if (!defined('BEHAT_UTIL')) {
73 throw new coding_exception('This method can be only used by Behat CLI tool');
76 $tables = $DB->get_tables(false);
77 if (!empty($tables)) {
78 behat_error(BEHAT_EXITCODE_INSTALLED);
82 self::reset_dataroot();
85 $options['adminuser'] = 'admin';
86 $options['adminpass'] = 'admin';
87 $options['fullname'] = self::BEHATSITENAME;
88 $options['shortname'] = self::BEHATSITENAME;
90 install_cli_database($options, false);
92 // We need to keep the installed dataroot filedir files.
93 // So each time we reset the dataroot before running a test, the default files are still installed.
94 self::save_original_data_files();
96 $frontpagesummary = new admin_setting_special_frontpagedesc();
97 $frontpagesummary->write_setting(self::BEHATSITENAME);
99 // Update admin user info.
100 $user = $DB->get_record('user', array('username' => 'admin'));
101 $user->email = 'moodle@example.com';
102 $user->firstname = 'Admin';
103 $user->lastname = 'User';
104 $user->city = 'Perth';
105 $user->country = 'AU';
106 user_update_user($user, false);
108 // Disable email message processor.
109 $DB->set_field('message_processors', 'enabled', '0', array('name' => 'email'));
111 // Sets maximum debug level.
112 set_config('debug', DEBUG_DEVELOPER);
113 set_config('debugdisplay', 1);
115 // Disable some settings that are not wanted on test sites.
116 set_config('noemailever', 1);
119 set_config('cronclionly', 0);
121 // Set editor autosave to high value, so as to avoid unwanted ajax.
122 set_config('autosavefrequency', '604800', 'editor_atto');
124 // Set noreplyaddress to an example domain, as it should be valid email address and test site can be a localhost.
125 set_config('noreplyaddress', 'noreply@example.com');
127 // Keeps the current version of database and dataroot.
128 self::store_versions_hash();
130 // Stores the database contents for fast reset.
131 self::store_database_state();
135 * Drops dataroot and remove test database tables
136 * @throws coding_exception
139 public static function drop_site() {
141 if (!defined('BEHAT_UTIL')) {
142 throw new coding_exception('This method can be only used by Behat CLI tool');
145 self::reset_dataroot();
146 self::drop_database(true);
147 self::drop_dataroot();
151 * Delete files and directories under dataroot.
153 public static function drop_dataroot() {
156 // As behat directory is now created under default $CFG->behat_dataroot_parent, so remove the whole dir.
157 if ($CFG->behat_dataroot !== $CFG->behat_dataroot_parent) {
158 remove_dir($CFG->behat_dataroot, false);
160 // It should never come here.
161 throw new moodle_exception("Behat dataroot should not be same as parent behat data root.");
166 * Checks if $CFG->behat_wwwroot is available and using same versions for cli and web.
170 public static function check_server_status() {
173 $url = $CFG->behat_wwwroot . '/admin/tool/behat/tests/behat/fixtures/environment.php';
175 // Get web versions used by behat site.
176 $ch = curl_init($url);
177 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
178 $result = curl_exec($ch);
179 $statuscode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
182 if ($statuscode !== 200 || empty($result) || (!$result = json_decode($result, true))) {
184 behat_error (BEHAT_EXITCODE_REQUIREMENT, $CFG->behat_wwwroot . ' is not available, ensure you specified ' .
185 'correct url and that the server is set up and started.' . PHP_EOL . ' More info in ' .
186 behat_command::DOCS_URL . PHP_EOL);
189 // Check if cli version is same as web version.
190 $clienv = self::get_environment();
191 if ($result != $clienv) {
192 $output = 'Differences detected between cli and webserver...'.PHP_EOL;
193 foreach ($result as $key => $version) {
194 if ($clienv[$key] != $version) {
195 $output .= ' ' . $key . ': ' . PHP_EOL;
196 $output .= ' - web server: ' . $version . PHP_EOL;
197 $output .= ' - cli: ' . $clienv[$key] . PHP_EOL;
206 * Checks whether the test database and dataroot is ready
207 * Stops execution if something went wrong
208 * @throws coding_exception
211 protected static function test_environment_problem() {
214 if (!defined('BEHAT_UTIL')) {
215 throw new coding_exception('This method can be only used by Behat CLI tool');
218 if (!self::is_test_site()) {
219 behat_error(1, 'This is not a behat test site!');
222 $tables = $DB->get_tables(false);
223 if (empty($tables)) {
224 behat_error(BEHAT_EXITCODE_INSTALL, '');
227 if (!self::is_test_data_updated()) {
228 behat_error(BEHAT_EXITCODE_REINSTALL, 'The test environment was initialised for a different version');
235 * It uses CFG->behat_dataroot
237 * Starts the test mode checking the composer installation and
238 * the test environment and updating the available
239 * features and steps definitions.
241 * Stores a file in dataroot/behat to allow Moodle to switch
242 * to the test environment when using cli-server.
243 * @param bool $themesuitewithallfeatures List themes to include core features.
244 * @param string $tags comma separated tag, which will be given preference while distributing features in parallel run.
245 * @param int $parallelruns number of parallel runs.
246 * @param int $run current run.
247 * @throws coding_exception
250 public static function start_test_mode($themesuitewithallfeatures = false, $tags = '', $parallelruns = 0, $run = 0) {
253 if (!defined('BEHAT_UTIL')) {
254 throw new coding_exception('This method can be only used by Behat CLI tool');
257 // Checks the behat set up and the PHP version.
258 if ($errorcode = behat_command::behat_setup_problem()) {
262 // Check that test environment is correctly set up.
263 self::test_environment_problem();
265 // Updates all the Moodle features and steps definitions.
266 behat_config_manager::update_config_file('', true, $tags, $themesuitewithallfeatures, $parallelruns, $run);
268 if (self::is_test_mode_enabled()) {
272 $contents = '$CFG->behat_wwwroot, $CFG->behat_prefix and $CFG->behat_dataroot' .
273 ' are currently used as $CFG->wwwroot, $CFG->prefix and $CFG->dataroot';
274 $filepath = self::get_test_file_path();
275 if (!file_put_contents($filepath, $contents)) {
276 behat_error(BEHAT_EXITCODE_PERMISSIONS, 'File ' . $filepath . ' can not be created');
281 * Returns the status of the behat test environment
283 * @return int Error code
285 public static function get_behat_status() {
287 if (!defined('BEHAT_UTIL')) {
288 throw new coding_exception('This method can be only used by Behat CLI tool');
291 // Checks the behat set up and the PHP version, returning an error code if something went wrong.
292 if ($errorcode = behat_command::behat_setup_problem()) {
296 // Check that test environment is correctly set up, stops execution.
297 self::test_environment_problem();
302 * @throws coding_exception
305 public static function stop_test_mode() {
307 if (!defined('BEHAT_UTIL')) {
308 throw new coding_exception('This method can be only used by Behat CLI tool');
311 $testenvfile = self::get_test_file_path();
312 behat_config_manager::set_behat_run_config_value('behatsiteenabled', 0);
314 if (!self::is_test_mode_enabled()) {
315 echo "Test environment was already disabled\n";
317 if (!unlink($testenvfile)) {
318 behat_error(BEHAT_EXITCODE_PERMISSIONS, 'Can not delete test environment file');
324 * Checks whether test environment is enabled or disabled
326 * To check is the current script is running in the test
331 public static function is_test_mode_enabled() {
333 $testenvfile = self::get_test_file_path();
334 if (file_exists($testenvfile)) {
342 * Returns the path to the file which specifies if test environment is enabled
345 public final static function get_test_file_path() {
346 return behat_command::get_parent_behat_dir() . '/test_environment_enabled.txt';
350 * Removes config settings that were added to the main $CFG config within the Behat CLI
353 * Database storage is already handled by reset_database and existing config values will
354 * be reset automatically by initialise_cfg(), so we only need to remove added ones.
356 public static function remove_added_config() {
358 if (!empty($CFG->behat_cli_added_config)) {
359 foreach ($CFG->behat_cli_added_config as $key => $value) {
362 unset($CFG->behat_cli_added_config);
367 * Reset contents of all database tables to initial values, reset caches, etc.
369 public static function reset_all_data() {
371 self::reset_database();
373 // Purge dataroot directory.
374 self::reset_dataroot();
376 // Reset all static caches.
377 accesslib_clear_all_caches(true);
378 accesslib_reset_role_cache();
379 // Reset the nasty strings list used during the last test.
380 nasty_strings::reset_used_strings();
382 filter_manager::reset_caches();
384 // Reset course and module caches.
385 if (class_exists('format_base')) {
386 // If file containing class is not loaded, there is no cache there anyway.
387 format_base::reset_course_cache(0);
389 get_fast_modinfo(0, 0, true);
391 // Inform data generator.
392 self::get_data_generator()->reset();
394 // Initialise $CFG with default values. This is needed for behat cli process, so we don't have modified
395 // $CFG values from the old run. @see set_config.
396 self::remove_added_config();
401 * Pause execution immediately.
403 * @param Session $session
404 * @param string $message The message to show when pausing.
405 * This will be passed through cli_ansi_format so appropriate ANSI formatting and features are available.
407 public static function pause(Session $session, string $message): void {
408 $posixexists = function_exists('posix_isatty');
410 // Make sure this step is only used with interactive terminal (if detected).
411 if ($posixexists && !@posix_isatty(STDOUT)) {
412 throw new ExpectationException('Break point should only be used with interactive terminal.', $session);
415 // Save the cursor position, ring the bell, and add a new line.
416 fwrite(STDOUT, cli_ansi_format("<cursor:save><bell><newline>"));
418 // Output the formatted message and reset colour back to normal.
419 $formattedmessage = cli_ansi_format("{$message}<colour:normal>");
420 fwrite(STDOUT, $formattedmessage);
425 // Move the cursor back up to the previous position, then restore the original position stored earlier, and move
426 // it back down again.
427 fwrite(STDOUT, cli_ansi_format("<cursor:up><cursor:up><cursor:restore><cursor:down><cursor:down>"));
429 // Add any extra lines back if the provided message was spread over multiple lines.
430 $linecount = count(explode("\n", $formattedmessage));
431 fwrite(STDOUT, str_repeat(cli_ansi_format("<cursor:down>"), $linecount - 1));