Merge branch 'wip-mdl-58068' of https://github.com/rajeshtaneja/moodle
[moodle.git] / lib / behat / lib.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  * Behat basic functions
19  *
20  * It does not include MOODLE_INTERNAL because is part of the bootstrap.
21  *
22  * This script should not be usually included, neither any of its functions
23  * used, within mooodle code at all. It's for exclusive use of behat and
24  * moodle setup.php. For places requiring a different/special behavior
25  * needing to check if are being run as part of behat tests, use:
26  *     if (defined('BEHAT_SITE_RUNNING')) { ...
27  *
28  * @package    core
29  * @category   test
30  * @copyright  2012 David MonllaĆ³
31  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
32  */
34 require_once(__DIR__ . '/../testing/lib.php');
36 define('BEHAT_EXITCODE_CONFIG', 250);
37 define('BEHAT_EXITCODE_REQUIREMENT', 251);
38 define('BEHAT_EXITCODE_PERMISSIONS', 252);
39 define('BEHAT_EXITCODE_REINSTALL', 253);
40 define('BEHAT_EXITCODE_INSTALL', 254);
41 define('BEHAT_EXITCODE_INSTALLED', 256);
43 /**
44  * The behat test site fullname and shortname.
45  */
46 define('BEHAT_PARALLEL_SITE_NAME', "behatrun");
48 /**
49  * Exits with an error code
50  *
51  * @param  mixed $errorcode
52  * @param  string $text
53  * @return void Stops execution with error code
54  */
55 function behat_error($errorcode, $text = '') {
57     // Adding error prefixes.
58     switch ($errorcode) {
59         case BEHAT_EXITCODE_CONFIG:
60             $text = 'Behat config error: ' . $text;
61             break;
62         case BEHAT_EXITCODE_REQUIREMENT:
63             $text = 'Behat requirement not satisfied: ' . $text;
64             break;
65         case BEHAT_EXITCODE_PERMISSIONS:
66             $text = 'Behat permissions problem: ' . $text . ', check the permissions';
67             break;
68         case BEHAT_EXITCODE_REINSTALL:
69             $path = testing_cli_argument_path('/admin/tool/behat/cli/init.php');
70             $text = "Reinstall Behat: ".$text.", use:\n php ".$path;
71             break;
72         case BEHAT_EXITCODE_INSTALL:
73             $path = testing_cli_argument_path('/admin/tool/behat/cli/init.php');
74             $text = "Install Behat before enabling it, use:\n php ".$path;
75             break;
76         case BEHAT_EXITCODE_INSTALLED:
77             $text = "The Behat site is already installed";
78             break;
79         default:
80             $text = 'Unknown error ' . $errorcode . ' ' . $text;
81             break;
82     }
84     testing_error($errorcode, $text);
85 }
87 /**
88  * Return logical error string.
89  *
90  * @param int $errtype php error type.
91  * @return string string which will be returned.
92  */
93 function behat_get_error_string($errtype) {
94     switch ($errtype) {
95         case E_USER_ERROR:
96             $errnostr = 'Fatal error';
97             break;
98         case E_WARNING:
99         case E_USER_WARNING:
100             $errnostr = 'Warning';
101             break;
102         case E_NOTICE:
103         case E_USER_NOTICE:
104         case E_STRICT:
105             $errnostr = 'Notice';
106             break;
107         case E_RECOVERABLE_ERROR:
108             $errnostr = 'Catchable';
109             break;
110         default:
111             $errnostr = 'Unknown error type';
112     }
114     return $errnostr;
117 /**
118  * PHP errors handler to use when running behat tests.
119  *
120  * Adds specific CSS classes to identify
121  * the messages.
122  *
123  * @param int $errno
124  * @param string $errstr
125  * @param string $errfile
126  * @param int $errline
127  * @param array $errcontext
128  * @return bool
129  */
130 function behat_error_handler($errno, $errstr, $errfile, $errline, $errcontext) {
132     // If is preceded by an @ we don't show it.
133     if (!error_reporting()) {
134         return true;
135     }
137     // This error handler receives E_ALL | E_STRICT, running the behat test site the debug level is
138     // set to DEVELOPER and will always include E_NOTICE,E_USER_NOTICE... as part of E_ALL, if the current
139     // error_reporting() value does not include one of those levels is because it has been forced through
140     // the moodle code (see fix_utf8() for example) in that cases we respect the forced error level value.
141     $respect = array(E_NOTICE, E_USER_NOTICE, E_STRICT, E_WARNING, E_USER_WARNING);
142     foreach ($respect as $respectable) {
144         // If the current value does not include this kind of errors and the reported error is
145         // at that level don't print anything.
146         if ($errno == $respectable && !(error_reporting() & $respectable)) {
147             return true;
148         }
149     }
151     // Using the default one in case there is a fatal catchable error.
152     default_error_handler($errno, $errstr, $errfile, $errline, $errcontext);
154     $errnostr = behat_get_error_string($errno);
156     // If ajax script then throw exception, so the calling api catch it and show it on web page.
157     if (defined('AJAX_SCRIPT')) {
158         throw new Exception("$errnostr: $errstr in $errfile on line $errline");
159     } else {
160         // Wrapping the output.
161         echo '<div class="phpdebugmessage" data-rel="phpdebugmessage">' . PHP_EOL;
162         echo "$errnostr: $errstr in $errfile on line $errline" . PHP_EOL;
163         echo '</div>';
164     }
166     // Also use the internal error handler so we keep the usual behaviour.
167     return false;
170 /**
171  * Before shutdown save last error entries, so we can fail the test.
172  */
173 function behat_shutdown_function() {
174     // If any error found, then save it.
175     if ($error = error_get_last()) {
176         // Ignore E_WARNING, as they might come via ( @ )suppression and might lead to false failure.
177         if (isset($error['type']) && !($error['type'] & E_WARNING)) {
179             $errors = behat_get_shutdown_process_errors();
181             $errors[] = $error;
182             $errorstosave = json_encode($errors);
184             set_config('process_errors', $errorstosave, 'tool_behat');
185         }
186     }
189 /**
190  * Return php errors save which were save during shutdown.
191  *
192  * @return array
193  */
194 function behat_get_shutdown_process_errors() {
195     global $DB;
197     // Don't use get_config, as it use cache and return invalid value, between selenium and cli process.
198     $phperrors = $DB->get_field('config_plugins', 'value', array('name' => 'process_errors', 'plugin' => 'tool_behat'));
200     if (!empty($phperrors)) {
201         return json_decode($phperrors, true);
202     } else {
203         return array();
204     }
207 /**
208  * Restrict the config.php settings allowed.
209  *
210  * When running the behat features the config.php
211  * settings should not affect the results.
212  *
213  * @return void
214  */
215 function behat_clean_init_config() {
216     global $CFG;
218     $allowed = array_flip(array(
219         'wwwroot', 'dataroot', 'dirroot', 'admin', 'directorypermissions', 'filepermissions',
220         'umaskpermissions', 'dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'prefix',
221         'dboptions', 'proxyhost', 'proxyport', 'proxytype', 'proxyuser', 'proxypassword',
222         'proxybypass', 'theme', 'pathtogs', 'pathtodu', 'aspellpath', 'pathtodot', 'skiplangupgrade',
223         'altcacheconfigpath', 'pathtounoconv', 'alternative_file_system_class'
224     ));
226     // Add extra allowed settings.
227     if (!empty($CFG->behat_extraallowedsettings)) {
228         $allowed = array_merge($allowed, array_flip($CFG->behat_extraallowedsettings));
229     }
231     // Also allowing behat_ prefixed attributes.
232     foreach ($CFG as $key => $value) {
233         if (!isset($allowed[$key]) && strpos($key, 'behat_') !== 0) {
234             unset($CFG->{$key});
235         }
236     }
239 /**
240  * Checks that the behat config vars are properly set.
241  *
242  * @return void Stops execution with error code if something goes wrong.
243  */
244 function behat_check_config_vars() {
245     global $CFG;
247     // Verify prefix value.
248     if (empty($CFG->behat_prefix)) {
249         behat_error(BEHAT_EXITCODE_CONFIG,
250             'Define $CFG->behat_prefix in config.php');
251     }
252     if (!empty($CFG->prefix) and $CFG->behat_prefix == $CFG->prefix) {
253         behat_error(BEHAT_EXITCODE_CONFIG,
254             '$CFG->behat_prefix in config.php must be different from $CFG->prefix');
255     }
256     if (!empty($CFG->phpunit_prefix) and $CFG->behat_prefix == $CFG->phpunit_prefix) {
257         behat_error(BEHAT_EXITCODE_CONFIG,
258             '$CFG->behat_prefix in config.php must be different from $CFG->phpunit_prefix');
259     }
261     // Verify behat wwwroot value.
262     if (empty($CFG->behat_wwwroot)) {
263         behat_error(BEHAT_EXITCODE_CONFIG,
264             'Define $CFG->behat_wwwroot in config.php');
265     }
266     if (!empty($CFG->wwwroot) and $CFG->behat_wwwroot == $CFG->wwwroot) {
267         behat_error(BEHAT_EXITCODE_CONFIG,
268             '$CFG->behat_wwwroot in config.php must be different from $CFG->wwwroot');
269     }
271     // Verify behat dataroot value.
272     if (empty($CFG->behat_dataroot)) {
273         behat_error(BEHAT_EXITCODE_CONFIG,
274             'Define $CFG->behat_dataroot in config.php');
275     }
276     clearstatcache();
277     if (!file_exists($CFG->behat_dataroot_parent)) {
278         $permissions = isset($CFG->directorypermissions) ? $CFG->directorypermissions : 02777;
279         umask(0);
280         if (!mkdir($CFG->behat_dataroot_parent, $permissions, true)) {
281             behat_error(BEHAT_EXITCODE_PERMISSIONS, '$CFG->behat_dataroot directory can not be created');
282         }
283     }
284     $CFG->behat_dataroot_parent = realpath($CFG->behat_dataroot_parent);
285     if (empty($CFG->behat_dataroot_parent) or !is_dir($CFG->behat_dataroot_parent) or !is_writable($CFG->behat_dataroot_parent)) {
286         behat_error(BEHAT_EXITCODE_CONFIG,
287             '$CFG->behat_dataroot in config.php must point to an existing writable directory');
288     }
289     if (!empty($CFG->dataroot) and $CFG->behat_dataroot_parent == realpath($CFG->dataroot)) {
290         behat_error(BEHAT_EXITCODE_CONFIG,
291             '$CFG->behat_dataroot in config.php must be different from $CFG->dataroot');
292     }
293     if (!empty($CFG->phpunit_dataroot) and $CFG->behat_dataroot_parent == realpath($CFG->phpunit_dataroot)) {
294         behat_error(BEHAT_EXITCODE_CONFIG,
295             '$CFG->behat_dataroot in config.php must be different from $CFG->phpunit_dataroot');
296     }
298     // This request is coming from admin/tool/behat/cli/util.php which will call util_single.php. So just return from
299     // here as we don't need to create a dataroot for single run.
300     if (defined('BEHAT_PARALLEL_UTIL') && BEHAT_PARALLEL_UTIL && empty($CFG->behatrunprocess)) {
301         return;
302     }
304     if (!file_exists($CFG->behat_dataroot)) {
305         $permissions = isset($CFG->directorypermissions) ? $CFG->directorypermissions : 02777;
306         umask(0);
307         if (!mkdir($CFG->behat_dataroot, $permissions, true)) {
308             behat_error(BEHAT_EXITCODE_PERMISSIONS, '$CFG->behat_dataroot directory can not be created');
309         }
310     }
311     $CFG->behat_dataroot = realpath($CFG->behat_dataroot);
314 /**
315  * Should we switch to the test site data?
316  * @return bool
317  */
318 function behat_is_test_site() {
319     global $CFG;
321     if (defined('BEHAT_UTIL')) {
322         // This is the admin tool that installs/drops the test site install.
323         return true;
324     }
325     if (defined('BEHAT_TEST')) {
326         // This is the main vendor/bin/behat script.
327         return true;
328     }
329     if (empty($CFG->behat_wwwroot)) {
330         return false;
331     }
332     if (isset($_SERVER['REMOTE_ADDR']) and behat_is_requested_url($CFG->behat_wwwroot)) {
333         // Something is accessing the web server like a real browser.
334         return true;
335     }
337     return false;
340 /**
341  * Fix variables for parallel behat testing.
342  * - behat_wwwroot = behat_wwwroot{behatrunprocess}
343  * - behat_dataroot = behat_dataroot{behatrunprocess}
344  * - behat_prefix = behat_prefix.{behatrunprocess}_ (For oracle it will be firstletter of prefix and behatrunprocess)
345  **/
346 function behat_update_vars_for_process() {
347     global $CFG;
349     $allowedconfigoverride = array('dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'behat_prefix',
350         'behat_wwwroot', 'behat_dataroot');
351     $behatrunprocess = behat_get_run_process();
352     $CFG->behatrunprocess = $behatrunprocess;
354     // Data directory will be a directory under parent directory.
355     $CFG->behat_dataroot_parent = $CFG->behat_dataroot;
356     $CFG->behat_dataroot .= '/'. BEHAT_PARALLEL_SITE_NAME;
358     if ($behatrunprocess) {
359         if (empty($CFG->behat_parallel_run[$behatrunprocess - 1]['behat_wwwroot'])) {
360             // Set www root for run process.
361             if (isset($CFG->behat_wwwroot) &&
362                 !preg_match("#/" . BEHAT_PARALLEL_SITE_NAME . $behatrunprocess . "\$#", $CFG->behat_wwwroot)) {
363                 $CFG->behat_wwwroot .= "/" . BEHAT_PARALLEL_SITE_NAME . $behatrunprocess;
364             }
365         }
367         if (empty($CFG->behat_parallel_run[$behatrunprocess - 1]['behat_dataroot'])) {
368             // Set behat_dataroot.
369             if (!preg_match("#" . $behatrunprocess . "\$#", $CFG->behat_dataroot)) {
370                 $CFG->behat_dataroot .= $behatrunprocess;
371             }
372         }
374         // Set behat_prefix for db, just suffix run process number, to avoid max length exceed.
375         // For oracle only 2 letter prefix is possible.
376         // NOTE: This will not work for parallel process > 9.
377         if ($CFG->dbtype === 'oci') {
378             $CFG->behat_prefix = substr($CFG->behat_prefix, 0, 1);
379             $CFG->behat_prefix .= "{$behatrunprocess}";
380         } else {
381             $CFG->behat_prefix .= "{$behatrunprocess}_";
382         }
384         if (!empty($CFG->behat_parallel_run[$behatrunprocess - 1])) {
385             // Override allowed config vars.
386             foreach ($allowedconfigoverride as $config) {
387                 if (isset($CFG->behat_parallel_run[$behatrunprocess - 1][$config])) {
388                     $CFG->$config = $CFG->behat_parallel_run[$behatrunprocess - 1][$config];
389                 }
390             }
391         }
392     }
395 /**
396  * Checks if the URL requested by the user matches the provided argument
397  *
398  * @param string $url
399  * @return bool Returns true if it matches.
400  */
401 function behat_is_requested_url($url) {
403     $parsedurl = parse_url($url . '/');
404     $parsedurl['port'] = isset($parsedurl['port']) ? $parsedurl['port'] : 80;
405     $parsedurl['path'] = rtrim($parsedurl['path'], '/');
407     // Removing the port.
408     $pos = strpos($_SERVER['HTTP_HOST'], ':');
409     if ($pos !== false) {
410         $requestedhost = substr($_SERVER['HTTP_HOST'], 0, $pos);
411     } else {
412         $requestedhost = $_SERVER['HTTP_HOST'];
413     }
415     // The path should also match.
416     if (empty($parsedurl['path'])) {
417         $matchespath = true;
418     } else if (strpos($_SERVER['SCRIPT_NAME'], $parsedurl['path']) === 0) {
419         $matchespath = true;
420     }
422     // The host and the port should match
423     if ($parsedurl['host'] == $requestedhost && $parsedurl['port'] == $_SERVER['SERVER_PORT'] && !empty($matchespath)) {
424         return true;
425     }
427     return false;
430 /**
431  * Get behat run process from either $_SERVER or command config.
432  *
433  * @return bool|int false if single run, else run process number.
434  */
435 function behat_get_run_process() {
436     global $argv, $CFG;
437     $behatrunprocess = false;
439     // Get behat run process, if set.
440     if (defined('BEHAT_CURRENT_RUN') && BEHAT_CURRENT_RUN) {
441         $behatrunprocess = BEHAT_CURRENT_RUN;
442     } else if (!empty($_SERVER['REMOTE_ADDR'])) {
443         // Try get it from config if present.
444         if (!empty($CFG->behat_parallel_run)) {
445             foreach ($CFG->behat_parallel_run as $run => $behatconfig) {
446                 if (isset($behatconfig['behat_wwwroot']) && behat_is_requested_url($behatconfig['behat_wwwroot'])) {
447                     $behatrunprocess = $run + 1; // We start process from 1.
448                     break;
449                 }
450             }
451         }
452         // Check if parallel site prefix is used.
453         if (empty($behatrunprocess) && preg_match('#/' . BEHAT_PARALLEL_SITE_NAME . '(.+?)/#', $_SERVER['REQUEST_URI'])) {
454             $dirrootrealpath = str_replace("\\", "/", realpath($CFG->dirroot));
455             $serverrealpath = str_replace("\\", "/", realpath($_SERVER['SCRIPT_FILENAME']));
456             $afterpath = str_replace($dirrootrealpath.'/', '', $serverrealpath);
457             if (!$behatrunprocess = preg_filter("#.*/" . BEHAT_PARALLEL_SITE_NAME . "(.+?)/$afterpath#", '$1',
458                 $_SERVER['SCRIPT_FILENAME'])) {
459                 throw new Exception("Unable to determine behat process [afterpath=" . $afterpath .
460                     ", scriptfilename=" . $_SERVER['SCRIPT_FILENAME'] . "]!");
461             }
462         }
463     } else if (defined('BEHAT_TEST') || defined('BEHAT_UTIL')) {
464         if ($match = preg_filter('#--run=(.+)#', '$1', $argv)) {
465             $behatrunprocess = reset($match);
466         } else if ($k = array_search('--config', $argv)) {
467             $behatconfig = str_replace("\\", "/", $argv[$k + 1]);
468             // Try get it from config if present.
469             if (!empty($CFG->behat_parallel_run)) {
470                 foreach ($CFG->behat_parallel_run as $run => $parallelconfig) {
471                     if (!empty($parallelconfig['behat_dataroot']) &&
472                         $parallelconfig['behat_dataroot'] . '/behat/behat.yml' == $behatconfig) {
474                         $behatrunprocess = $run + 1; // We start process from 1.
475                         break;
476                     }
477                 }
478             }
479             // Check if default behat datroot increment was done.
480             if (empty($behatrunprocess)) {
481                 $behatdataroot = str_replace("\\", "/", $CFG->behat_dataroot . '/' . BEHAT_PARALLEL_SITE_NAME);
482                 $behatrunprocess = preg_filter("#^{$behatdataroot}" . "(.+?)[/|\\\]behat[/|\\\]behat\.yml#", '$1',
483                     $behatconfig);
484             }
485         }
486     }
488     return $behatrunprocess;
491 /**
492  * Execute commands in parallel.
493  *
494  * @param array $cmds list of commands to be executed.
495  * @param string $cwd absolute path of working directory.
496  * @param int $delay time in seconds to add delay between each parallel process.
497  * @return array list of processes.
498  */
499 function cli_execute_parallel($cmds, $cwd = null, $delay = 0) {
500     require_once(__DIR__ . "/../../vendor/autoload.php");
502     $processes = array();
504     // Create child process.
505     foreach ($cmds as $name => $cmd) {
506         $process = new Symfony\Component\Process\Process($cmd);
508         $process->setWorkingDirectory($cwd);
509         $process->setTimeout(null);
510         $processes[$name] = $process;
511         $processes[$name]->start();
513         // If error creating process then exit.
514         if ($processes[$name]->getStatus() !== 'started') {
515             echo "Error starting process: $name";
516             foreach ($processes[$name] as $process) {
517                 if ($process) {
518                     $process->signal(SIGKILL);
519                 }
520             }
521             exit(1);
522         }
524         // Sleep for specified delay.
525         if ($delay) {
526             sleep($delay);
527         }
528     }
529     return $processes;