/** * Internal step definition to find exceptions, debugging() messages and PHP debug messages. * * Part of behat_hooks class as is part of the testing framework, is auto-executed * after each step so no features will splicitly use it. * * @throws Exception Unknown type, depending on what we caught in the hook or basic \Exception. * @see Moodle\BehatExtension\Tester\MoodleStepTester */ public function look_for_exceptions() { // Wrap in try in case we were interacting with a closed window. try { // Exceptions. $exceptionsxpath = "//div[@data-rel='fatalerror']"; // Debugging messages. $debuggingxpath = "//div[@data-rel='debugging']"; // PHP debug messages. $phperrorxpath = "//div[@data-rel='phpdebugmessage']"; // Any other backtrace. $othersxpath = "(//*[contains(., ': call to ')])[1]"; $xpaths = array($exceptionsxpath, $debuggingxpath, $phperrorxpath, $othersxpath); $joinedxpath = implode(' | ', $xpaths); // Joined xpath expression. Most of the time there will be no exceptions, so this pre-check // is faster than to send the 4 xpath queries for each step. if (!$this->getSession()->getDriver()->find($joinedxpath)) { // Check if we have recorded any errors in driver process. $phperrors = behat_get_shutdown_process_errors(); if (!empty($phperrors)) { foreach ($phperrors as $error) { $errnostring = behat_get_error_string($error['type']); $msgs[] = $errnostring . ": " . $error['message'] . " at " . $error['file'] . ": " . $error['line']; } $msg = "PHP errors found:\n" . implode("\n", $msgs); throw new \Exception(htmlentities($msg)); } return; } // Exceptions. if ($errormsg = $this->getSession()->getPage()->find('xpath', $exceptionsxpath)) { // Getting the debugging info and the backtrace. $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.alert-error'); // If errorinfoboxes is empty, try find alert-danger (bootstrap4) class. if (empty($errorinfoboxes)) { $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.alert-danger'); } // If errorinfoboxes is empty, try find notifytiny (original) class. if (empty($errorinfoboxes)) { $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.notifytiny'); } // If errorinfoboxes is empty, try find ajax/JS exception in dialogue. if (empty($errorinfoboxes)) { $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.moodle-exception-message'); // If ajax/JS exception. if ($errorinfoboxes) { $errorinfo = $this->get_debug_text($errorinfoboxes[0]->getHtml()); } } else { $errorinfo = $this->get_debug_text($errorinfoboxes[0]->getHtml()) . "\n" . $this->get_debug_text($errorinfoboxes[1]->getHtml()); } $msg = "Moodle exception: " . $errormsg->getText() . "\n" . $errorinfo; throw new \Exception(html_entity_decode($msg)); } // Debugging messages. if ($debuggingmessages = $this->getSession()->getPage()->findAll('xpath', $debuggingxpath)) { $msgs = array(); foreach ($debuggingmessages as $debuggingmessage) { $msgs[] = $this->get_debug_text($debuggingmessage->getHtml()); } $msg = "debugging() message/s found:\n" . implode("\n", $msgs); throw new \Exception(html_entity_decode($msg)); } // PHP debug messages. if ($phpmessages = $this->getSession()->getPage()->findAll('xpath', $phperrorxpath)) { $msgs = array(); foreach ($phpmessages as $phpmessage) { $msgs[] = $this->get_debug_text($phpmessage->getHtml()); } $msg = "PHP debug message/s found:\n" . implode("\n", $msgs); throw new \Exception(html_entity_decode($msg)); } // Any other backtrace. // First looking through xpath as it is faster than get and parse the whole page contents, // we get the contents and look for matches once we found something to suspect that there is a backtrace. if ($this->getSession()->getDriver()->find($othersxpath)) { $backtracespattern = '/(line [0-9]* of [^:]*: call to [\\->&;:a-zA-Z_\\x7f-\\xff][\\->&;:a-zA-Z0-9_\\x7f-\\xff]*)/'; if (preg_match_all($backtracespattern, $this->getSession()->getPage()->getContent(), $backtraces)) { $msgs = array(); foreach ($backtraces[0] as $backtrace) { $msgs[] = $backtrace . '()'; } $msg = "Other backtraces found:\n" . implode("\n", $msgs); throw new \Exception(htmlentities($msg)); } } } catch (NoSuchWindow $e) { // If we were interacting with a popup window it will not exists after closing it. } }
/** * PHP errors handler to use when running behat tests. * * Adds specific CSS classes to identify * the messages. * * @param int $errno * @param string $errstr * @param string $errfile * @param int $errline * @param array $errcontext * @return bool */ function behat_error_handler($errno, $errstr, $errfile, $errline, $errcontext) { // If is preceded by an @ we don't show it. if (!error_reporting()) { return true; } // This error handler receives E_ALL | E_STRICT, running the behat test site the debug level is // set to DEVELOPER and will always include E_NOTICE,E_USER_NOTICE... as part of E_ALL, if the current // error_reporting() value does not include one of those levels is because it has been forced through // the moodle code (see fix_utf8() for example) in that cases we respect the forced error level value. $respect = array(E_NOTICE, E_USER_NOTICE, E_STRICT, E_WARNING, E_USER_WARNING); foreach ($respect as $respectable) { // If the current value does not include this kind of errors and the reported error is // at that level don't print anything. if ($errno == $respectable && !(error_reporting() & $respectable)) { return true; } } // Using the default one in case there is a fatal catchable error. default_error_handler($errno, $errstr, $errfile, $errline, $errcontext); $errnostr = behat_get_error_string($errno); // If ajax script then throw exception, so the calling api catch it and show it on web page. if (defined('AJAX_SCRIPT')) { throw new Exception("$errnostr: $errstr in $errfile on line $errline"); } else { // Wrapping the output. echo '<div class="phpdebugmessage" data-rel="phpdebugmessage">' . PHP_EOL; echo "$errnostr: $errstr in $errfile on line $errline" . PHP_EOL; echo '</div>'; } // Also use the internal error handler so we keep the usual behaviour. return false; }