MDL-39752 behat: Parallel execution support
[moodle.git] / lib / tests / behat / behat_hooks.php
CommitLineData
f5ceb6c2 1<?php
f5ceb6c2
DM
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/>.
16
17/**
18 * Behat hooks steps definitions.
19 *
20 * This methods are used by Behat CLI command.
21 *
22 * @package core
23 * @category test
24 * @copyright 2012 David Monllaó
25 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 */
27
28// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
29
30require_once(__DIR__ . '/../../behat/behat_base.php');
31
1303eb29
DM
32use Behat\Behat\Event\SuiteEvent as SuiteEvent,
33 Behat\Behat\Event\ScenarioEvent as ScenarioEvent,
34 Behat\Behat\Event\StepEvent as StepEvent,
89cf999a 35 Behat\Mink\Exception\DriverException as DriverException,
870349ee
DM
36 WebDriver\Exception\NoSuchWindow as NoSuchWindow,
37 WebDriver\Exception\UnexpectedAlertOpen as UnexpectedAlertOpen,
3b7d3fb8
DM
38 WebDriver\Exception\UnknownError as UnknownError,
39 WebDriver\Exception\CurlExec as CurlExec,
870349ee 40 WebDriver\Exception\NoAlertOpenError as NoAlertOpenError;
f5ceb6c2
DM
41
42/**
43 * Hooks to the behat process.
44 *
45 * Behat accepts hooks after and before each
46 * suite, feature, scenario and step.
47 *
48 * They can not call other steps as part of their process
49 * like regular steps definitions does.
50 *
51 * Throws generic Exception because they are captured by Behat.
52 *
53 * @package core
54 * @category test
55 * @copyright 2012 David Monllaó
56 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
57 */
58class behat_hooks extends behat_base {
59
41eb672b
DM
60 /**
61 * @var Last browser session start time.
62 */
63 protected static $lastbrowsersessionstart = 0;
64
95b43d6b
DM
65 /**
66 * @var For actions that should only run once.
67 */
68 protected static $initprocessesfinished = false;
69
f49cede7
DM
70 /**
71 * Some exceptions can only be caught in a before or after step hook,
72 * they can not be thrown there as they will provoke a framework level
73 * failure, but we can store them here to fail the step in i_look_for_exceptions()
74 * which result will be parsed by the framework as the last step result.
75 *
76 * @var Null or the exception last step throw in the before or after hook.
77 */
78 protected static $currentstepexception = null;
79
5c0dfe32 80 /**
a964ead0 81 * If we are saving any kind of dump on failure we should use the same parent dir during a run.
5c0dfe32
DM
82 *
83 * @var The parent dir name
84 */
a964ead0 85 protected static $faildumpdirname = false;
5c0dfe32 86
f5ceb6c2
DM
87 /**
88 * Gives access to moodle codebase, ensures all is ready and sets up the test lock.
89 *
90 * Includes config.php to use moodle codebase with $CFG->behat_*
91 * instead of $CFG->prefix and $CFG->dataroot, called once per suite.
92 *
93 * @static
94 * @throws Exception
95 * @BeforeSuite
96 */
97 public static function before_suite($event) {
98 global $CFG;
99
cfcbc34a
DM
100 // Defined only when the behat CLI command is running, the moodle init setup process will
101 // read this value and switch to $CFG->behat_dataroot and $CFG->behat_prefix instead of
102 // the normal site.
103 define('BEHAT_TEST', 1);
104
f5ceb6c2
DM
105 define('CLI_SCRIPT', 1);
106
cfcbc34a 107 // With BEHAT_TEST we will be using $CFG->behat_* instead of $CFG->dataroot, $CFG->prefix and $CFG->wwwroot.
f5ceb6c2
DM
108 require_once(__DIR__ . '/../../../config.php');
109
110 // Now that we are MOODLE_INTERNAL.
111 require_once(__DIR__ . '/../../behat/classes/behat_command.php');
17344d4c 112 require_once(__DIR__ . '/../../behat/classes/behat_selectors.php');
af4830a2 113 require_once(__DIR__ . '/../../behat/classes/behat_context_helper.php');
f5ceb6c2
DM
114 require_once(__DIR__ . '/../../behat/classes/util.php');
115 require_once(__DIR__ . '/../../testing/classes/test_lock.php');
b08b0a28 116 require_once(__DIR__ . '/../../testing/classes/nasty_strings.php');
f5ceb6c2
DM
117
118 // Avoids vendor/bin/behat to be executed directly without test environment enabled
119 // to prevent undesired db & dataroot modifications, this is also checked
120 // before each scenario (accidental user deletes) in the BeforeScenario hook.
121
122 if (!behat_util::is_test_mode_enabled()) {
123 throw new Exception('Behat only can run if test mode is enabled. More info in ' . behat_command::DOCS_URL . '#Running_tests');
124 }
125
ab25d8a7
RT
126 // Reset all data, before checking for is_server_running.
127 // If not done, then it can return apache error, while running tests.
128 behat_util::reset_all_data();
129
f5ceb6c2 130 if (!behat_util::is_server_running()) {
3b7d3fb8 131 throw new Exception($CFG->behat_wwwroot .
60129d5d 132 ' is not available, ensure you specified correct url and that the server is set up and started.' .
3b7d3fb8 133 ' More info in ' . behat_command::DOCS_URL . '#Running_tests');
f5ceb6c2
DM
134 }
135
b831d479
DM
136 // Prevents using outdated data, upgrade script would start and tests would fail.
137 if (!behat_util::is_test_data_updated()) {
b32ca4ca 138 $commandpath = 'php admin/tool/behat/cli/init.php';
d69a6ad9
TH
139 throw new Exception("Your behat test site is outdated, please run\n\n " .
140 $commandpath . "\n\nfrom your moodle dirroot to drop and install the behat test site again.");
b831d479 141 }
f5ceb6c2
DM
142 // Avoid parallel tests execution, it continues when the previous lock is released.
143 test_lock::acquire('behat');
41eb672b
DM
144
145 // Store the browser reset time if reset after N seconds is specified in config.php.
146 if (!empty($CFG->behat_restart_browser_after)) {
147 // Store the initial browser session opening.
148 self::$lastbrowsersessionstart = time();
149 }
5c0dfe32 150
a964ead0
AN
151 if (!empty($CFG->behat_faildump_path) && !is_writable($CFG->behat_faildump_path)) {
152 throw new Exception('You set $CFG->behat_faildump_path to a non-writable directory');
5c0dfe32 153 }
f5ceb6c2
DM
154 }
155
08e7f97e
TL
156 protected static $timings = array();
157
158 /** @BeforeFeature */
159 public static function before_feature($obj) {
160 $file = $obj->getFeature()->getFile();
161 self::$timings[$file] = microtime(true);
162 }
163
164 /** @AfterFeature */
165 public static function teardownFeature($obj) {
166 $file = $obj->getFeature()->getFile();
167 self::$timings[$file] = microtime(true) - self::$timings[$file];
168 // Probably didn't actually run this, don't output it.
169 if (self::$timings[$file] < 1) {
170 unset(self::$timings[$file]);
171 }
172 }
173
174 /** @AfterSuite */
175 public static function tearDown($obj) {
176 global $CFG;
177 if (!defined('BEHAT_FEATURE_TIMING')) {
178 return;
179 }
180 $realroot = realpath(__DIR__.'/../../../').'/';
181 foreach (self::$timings as $k => $v) {
182 $new = str_replace($realroot, '', $k);
183 self::$timings[$new] = round($v, 1);
184 unset(self::$timings[$k]);
185 }
186 if ($existing = @json_decode(file_get_contents(BEHAT_FEATURE_TIMING), true)) {
187 self::$timings = array_merge($existing, self::$timings);
188 }
189 arsort(self::$timings);
190 @file_put_contents(BEHAT_FEATURE_TIMING, json_encode(self::$timings, JSON_PRETTY_PRINT));
191 }
192
f5ceb6c2
DM
193 /**
194 * Resets the test environment.
195 *
196 * @throws coding_exception If here we are not using the test database it should be because of a coding error
197 * @BeforeScenario
198 */
199 public function before_scenario($event) {
200 global $DB, $SESSION, $CFG;
201
202 // As many checks as we can.
cfcbc34a
DM
203 if (!defined('BEHAT_TEST') ||
204 !defined('BEHAT_SITE_RUNNING') ||
f5ceb6c2
DM
205 php_sapi_name() != 'cli' ||
206 !behat_util::is_test_mode_enabled() ||
cfcbc34a 207 !behat_util::is_test_site()) {
46ac40cd 208 throw new coding_exception('Behat only can modify the test database and the test dataroot!');
f5ceb6c2
DM
209 }
210
89cf999a
DM
211 $moreinfo = 'More info in ' . behat_command::DOCS_URL . '#Running_tests';
212 $driverexceptionmsg = 'Selenium server is not running, you need to start it to run tests that involve Javascript. ' . $moreinfo;
f59f6b6b
DM
213 try {
214 $session = $this->getSession();
215 } catch (CurlExec $e) {
216 // Exception thrown by WebDriver, so only @javascript tests will be caugth; in
217 // behat_util::is_server_running() we already checked that the server is running.
89cf999a
DM
218 throw new Exception($driverexceptionmsg);
219 } catch (DriverException $e) {
220 throw new Exception($driverexceptionmsg);
f59f6b6b
DM
221 } catch (UnknownError $e) {
222 // Generic 'I have no idea' Selenium error. Custom exception to provide more feedback about possible solutions.
223 $this->throw_unknown_exception($e);
224 }
225
226
17344d4c
DM
227 // We need the Mink session to do it and we do it only before the first scenario.
228 if (self::is_first_scenario()) {
f59f6b6b 229 behat_selectors::register_moodle_selectors($session);
af4830a2 230 behat_context_helper::set_session($session);
17344d4c
DM
231 }
232
55f47341
RT
233 // Reset mink session between the scenarios.
234 $session->reset();
235
de230fd3 236 // Reset $SESSION.
2e00d01d 237 \core\session\manager::init_empty_session();
c8619f33 238
ef1d45b3 239 behat_util::reset_all_data();
b08b0a28 240
de230fd3 241 // Assign valid data to admin user (some generator-related code needs a valid user).
f5ceb6c2 242 $user = $DB->get_record('user', array('username' => 'admin'));
d79d5ac2 243 \core\session\manager::set_user($user);
5f4b4e91 244
41eb672b
DM
245 // Reset the browser if specified in config.php.
246 if (!empty($CFG->behat_restart_browser_after) && $this->running_javascript()) {
247 $now = time();
248 if (self::$lastbrowsersessionstart + $CFG->behat_restart_browser_after < $now) {
f59f6b6b 249 $session->restart();
41eb672b
DM
250 self::$lastbrowsersessionstart = $now;
251 }
252 }
253
5f4b4e91 254 // Start always in the the homepage.
3b7d3fb8 255 try {
f59f6b6b
DM
256 // Let's be conservative as we never know when new upstream issues will affect us.
257 $session->visit($this->locate_path('/'));
3b7d3fb8 258 } catch (UnknownError $e) {
3b7d3fb8
DM
259 $this->throw_unknown_exception($e);
260 }
870349ee 261
f59f6b6b 262
95b43d6b
DM
263 // Checking that the root path is a Moodle test site.
264 if (self::is_first_scenario()) {
265 $notestsiteexception = new Exception('The base URL (' . $CFG->wwwroot . ') is not a behat test site, ' .
3b7d3fb8 266 'ensure you started the built-in web server in the correct directory or your web server is correctly started and set up');
19f6703d 267 $this->find("xpath", "//head/child::title[normalize-space(.)='" . behat_util::BEHATSITENAME . "']", $notestsiteexception);
95b43d6b
DM
268
269 self::$initprocessesfinished = true;
270 }
3b0b5e57
RT
271 // Run all test with medium (1024x768) screen size, to avoid responsive problems.
272 $this->resize_window('medium');
f5ceb6c2
DM
273 }
274
f5ceb6c2 275 /**
d1e55a47 276 * Wait for JS to complete before beginning interacting with the DOM.
f5ceb6c2 277 *
f49cede7
DM
278 * Executed only when running against a real browser. We wrap it
279 * all in a try & catch to forward the exception to i_look_for_exceptions
280 * so the exception will be at scenario level, which causes a failure, by
281 * default would be at framework level, which will stop the execution of
282 * the run.
f5ceb6c2 283 *
6e2c417c 284 * @BeforeStep @javascript
f5ceb6c2 285 */
6e2c417c 286 public function before_step_javascript($event) {
f49cede7
DM
287
288 try {
289 $this->wait_for_pending_js();
290 self::$currentstepexception = null;
291 } catch (Exception $e) {
292 self::$currentstepexception = $e;
293 }
d1e55a47
DM
294 }
295
296 /**
297 * Wait for JS to complete after finishing the step.
298 *
299 * With this we ensure that there are not AJAX calls
300 * still in progress.
301 *
f49cede7
DM
302 * Executed only when running against a real browser. We wrap it
303 * all in a try & catch to forward the exception to i_look_for_exceptions
304 * so the exception will be at scenario level, which causes a failure, by
305 * default would be at framework level, which will stop the execution of
306 * the run.
d1e55a47
DM
307 *
308 * @AfterStep @javascript
309 */
310 public function after_step_javascript($event) {
5c0dfe32
DM
311 global $CFG;
312
313 // Save a screenshot if the step failed.
a964ead0 314 if (!empty($CFG->behat_faildump_path) &&
5c0dfe32
DM
315 $event->getResult() === StepEvent::FAILED) {
316 $this->take_screenshot($event);
317 }
f49cede7
DM
318
319 try {
320 $this->wait_for_pending_js();
321 self::$currentstepexception = null;
322 } catch (UnexpectedAlertOpen $e) {
323 self::$currentstepexception = $e;
324
325 // Accepting the alert so the framework can continue properly running
326 // the following scenarios. Some browsers already closes the alert, so
327 // wrapping in a try & catch.
328 try {
329 $this->getSession()->getDriver()->getWebDriverSession()->accept_alert();
330 } catch (Exception $e) {
331 // Catching the generic one as we never know how drivers reacts here.
332 }
333 } catch (Exception $e) {
334 self::$currentstepexception = $e;
335 }
d1e55a47
DM
336 }
337
3ec07614 338 /**
a964ead0
AN
339 * Execute any steps required after the step has finished.
340 *
341 * This includes creating an HTML dump of the content if there was a failure.
342 *
343 * @AfterStep
344 */
345 public function after_step($event) {
346 global $CFG;
347
348 // Save the page content if the step failed.
349 if (!empty($CFG->behat_faildump_path) &&
350 $event->getResult() === StepEvent::FAILED) {
351 $this->take_contentdump($event);
352 }
353 }
354
355 /**
356 * Getter for self::$faildumpdirname
5c0dfe32
DM
357 *
358 * @return string
359 */
a964ead0
AN
360 protected function get_run_faildump_dir() {
361 return self::$faildumpdirname;
5c0dfe32
DM
362 }
363
364 /**
365 * Take screenshot when a step fails.
3ec07614 366 *
5c0dfe32
DM
367 * @throws Exception
368 * @param StepEvent $event
3ec07614 369 */
5c0dfe32 370 protected function take_screenshot(StepEvent $event) {
5c0dfe32
DM
371 // Goutte can't save screenshots.
372 if (!$this->running_javascript()) {
373 return false;
3ec07614 374 }
5c0dfe32 375
a964ead0
AN
376 list ($dir, $filename) = $this->get_faildump_filename($event, 'png');
377 $this->saveScreenshot($filename, $dir);
378 }
379
380 /**
381 * Take a dump of the page content when a step fails.
382 *
383 * @throws Exception
384 * @param StepEvent $event
385 */
386 protected function take_contentdump(StepEvent $event) {
387 list ($dir, $filename) = $this->get_faildump_filename($event, 'html');
388
389 $fh = fopen($dir . DIRECTORY_SEPARATOR . $filename, 'w');
390 fwrite($fh, $this->getSession()->getPage()->getContent());
391 fclose($fh);
392 }
393
394 /**
395 * Determine the full pathname to store a failure-related dump.
396 *
397 * This is used for content such as the DOM, and screenshots.
398 *
399 * @param StepEvent $event
167486b3 400 * @param String $filetype The file suffix to use. Limited to 4 chars.
a964ead0
AN
401 */
402 protected function get_faildump_filename(StepEvent $event, $filetype) {
403 global $CFG;
404
405 // All the contentdumps should be in the same parent dir.
406 if (!$faildumpdir = self::get_run_faildump_dir()) {
407 $faildumpdir = self::$faildumpdirname = date('Ymd_His');
5c0dfe32 408
a964ead0 409 $dir = $CFG->behat_faildump_path . DIRECTORY_SEPARATOR . $faildumpdir;
5c0dfe32 410
a964ead0 411 if (!is_dir($dir) && !mkdir($dir, $CFG->directorypermissions, true)) {
5c0dfe32 412 // It shouldn't, we already checked that the directory is writable.
a964ead0 413 throw new Exception('No directories can be created inside $CFG->behat_faildump_path, check the directory permissions.');
5c0dfe32
DM
414 }
415 } else {
416 // We will always need to know the full path.
a964ead0 417 $dir = $CFG->behat_faildump_path . DIRECTORY_SEPARATOR . $faildumpdir;
5c0dfe32
DM
418 }
419
420 // The scenario title + the failed step text.
a964ead0 421 // We want a i-am-the-scenario-title_i-am-the-failed-step.$filetype format.
5c0dfe32 422 $filename = $event->getStep()->getParent()->getTitle() . '_' . $event->getStep()->getText();
167486b3
DM
423 $filename = preg_replace('/([^a-zA-Z0-9\_]+)/', '-', $filename);
424
a2c271dd 425 // File name limited to 255 characters. Leaving 4 chars for the file
167486b3 426 // extension as we allow .png for images and .html for DOM contents.
a2c271dd 427 $filename = substr($filename, 0, 250) . '.' . $filetype;
5c0dfe32 428
a964ead0 429 return array($dir, $filename);
3ec07614
DW
430 }
431
d1e55a47
DM
432 /**
433 * Waits for all the JS to be loaded.
434 *
afe9f42a 435 * @throws \Exception
d1e55a47
DM
436 * @throws NoSuchWindow
437 * @throws UnknownError
438 * @return bool True or false depending whether all the JS is loaded or not.
439 */
440 protected function wait_for_pending_js() {
441
c1faf86b
DM
442 // We don't use behat_base::spin() here as we don't want to end up with an exception
443 // if the page & JSs don't finish loading properly.
444 for ($i = 0; $i < self::EXTENDED_TIMEOUT * 10; $i++) {
6e2c417c
DW
445 $pending = '';
446 try {
2ca7f4e6
RT
447 $jscode =
448 'if (typeof M === "undefined") {
449 if (document.readyState === "complete") {
450 return "";
451 } else {
452 return "incomplete";
453 }
454 } else if (' . self::PAGE_READY_JS . ') {
455 return "";
456 } else {
457 return M.util.pending_js.join(":");
458 }';
d1e55a47 459 $pending = $this->getSession()->evaluateScript($jscode);
6e2c417c 460 } catch (NoSuchWindow $nsw) {
d1e55a47 461 // We catch an exception here, in case we just closed the window we were interacting with.
6e2c417c
DW
462 // No javascript is running if there is no window right?
463 $pending = '';
d1e55a47 464 } catch (UnknownError $e) {
f49cede7
DM
465 // M is not defined when the window or the frame don't exist anymore.
466 if (strstr($e->getMessage(), 'M is not defined') != false) {
467 $pending = '';
468 }
28abad1a 469 }
d1e55a47
DM
470
471 // If there are no pending JS we stop waiting.
6e2c417c 472 if ($pending === '') {
d1e55a47 473 return true;
6e2c417c 474 }
d1e55a47 475
6e2c417c
DW
476 // 0.1 seconds.
477 usleep(100000);
1303eb29 478 }
d1e55a47 479
afe9f42a
DM
480 // Timeout waiting for JS to complete. It will be catched and forwarded to behat_hooks::i_look_for_exceptions().
481 // It is unlikely that Javascript code of a page or an AJAX request needs more than self::EXTENDED_TIMEOUT seconds
482 // to be loaded, although when pages contains Javascript errors M.util.js_complete() can not be executed, so the
483 // number of JS pending code and JS completed code will not match and we will reach this point.
484 throw new \Exception('Javascript code and/or AJAX requests are not ready after ' . self::EXTENDED_TIMEOUT .
485 ' seconds. There is a Javascript error or the code is extremely slow.');
f5ceb6c2
DM
486 }
487
5f4b4e91
DM
488 /**
489 * Internal step definition to find exceptions, debugging() messages and PHP debug messages.
490 *
491 * Part of behat_hooks class as is part of the testing framework, is auto-executed
492 * after each step so no features will splicitly use it.
493 *
494 * @Given /^I look for exceptions$/
f49cede7 495 * @throw Exception Unknown type, depending on what we caught in the hook or basic \Exception.
5f4b4e91
DM
496 * @see Moodle\BehatExtension\Tester\MoodleStepTester
497 */
498 public function i_look_for_exceptions() {
499
f49cede7
DM
500 // If the step already failed in a hook throw the exception.
501 if (!is_null(self::$currentstepexception)) {
502 throw self::$currentstepexception;
503 }
504
217e8e59
DM
505 // Wrap in try in case we were interacting with a closed window.
506 try {
5f4b4e91 507
217e8e59 508 // Exceptions.
3e76c7fa 509 $exceptionsxpath = "//div[@data-rel='fatalerror']";
8aea365f 510 // Debugging messages.
3e76c7fa 511 $debuggingxpath = "//div[@data-rel='debugging']";
8aea365f 512 // PHP debug messages.
3e76c7fa 513 $phperrorxpath = "//div[@data-rel='phpdebugmessage']";
8aea365f 514 // Any other backtrace.
6cd21356 515 $othersxpath = "(//*[contains(., ': call to ')])[1]";
8aea365f
DM
516
517 $xpaths = array($exceptionsxpath, $debuggingxpath, $phperrorxpath, $othersxpath);
518 $joinedxpath = implode(' | ', $xpaths);
519
520 // Joined xpath expression. Most of the time there will be no exceptions, so this pre-check
521 // is faster than to send the 4 xpath queries for each step.
522 if (!$this->getSession()->getDriver()->find($joinedxpath)) {
523 return;
524 }
525
526 // Exceptions.
527 if ($errormsg = $this->getSession()->getPage()->find('xpath', $exceptionsxpath)) {
5f4b4e91 528
217e8e59 529 // Getting the debugging info and the backtrace.
f8b589c9
RT
530 $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.alert-error');
531 // If errorinfoboxes is empty, try find notifytiny (original) class.
532 if (empty($errorinfoboxes)) {
533 $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.notifytiny');
534 }
217e8e59
DM
535 $errorinfo = $this->get_debug_text($errorinfoboxes[0]->getHtml()) . "\n" .
536 $this->get_debug_text($errorinfoboxes[1]->getHtml());
5f4b4e91 537
217e8e59
DM
538 $msg = "Moodle exception: " . $errormsg->getText() . "\n" . $errorinfo;
539 throw new \Exception(html_entity_decode($msg));
540 }
541
542 // Debugging messages.
8aea365f 543 if ($debuggingmessages = $this->getSession()->getPage()->findAll('xpath', $debuggingxpath)) {
217e8e59
DM
544 $msgs = array();
545 foreach ($debuggingmessages as $debuggingmessage) {
546 $msgs[] = $this->get_debug_text($debuggingmessage->getHtml());
547 }
548 $msg = "debugging() message/s found:\n" . implode("\n", $msgs);
549 throw new \Exception(html_entity_decode($msg));
5f4b4e91 550 }
90ed22ab 551
217e8e59 552 // PHP debug messages.
8aea365f 553 if ($phpmessages = $this->getSession()->getPage()->findAll('xpath', $phperrorxpath)) {
90ed22ab 554
217e8e59
DM
555 $msgs = array();
556 foreach ($phpmessages as $phpmessage) {
557 $msgs[] = $this->get_debug_text($phpmessage->getHtml());
558 }
559 $msg = "PHP debug message/s found:\n" . implode("\n", $msgs);
560 throw new \Exception(html_entity_decode($msg));
90ed22ab 561 }
bbd802f7 562
217e8e59 563 // Any other backtrace.
9a1f4922
DM
564 // First looking through xpath as it is faster than get and parse the whole page contents,
565 // we get the contents and look for matches once we found something to suspect that there is a backtrace.
8aea365f 566 if ($this->getSession()->getDriver()->find($othersxpath)) {
9a1f4922
DM
567 $backtracespattern = '/(line [0-9]* of [^:]*: call to [\->&;:a-zA-Z_\x7f-\xff][\->&;:a-zA-Z0-9_\x7f-\xff]*)/';
568 if (preg_match_all($backtracespattern, $this->getSession()->getPage()->getContent(), $backtraces)) {
569 $msgs = array();
570 foreach ($backtraces[0] as $backtrace) {
571 $msgs[] = $backtrace . '()';
572 }
573 $msg = "Other backtraces found:\n" . implode("\n", $msgs);
574 throw new \Exception(htmlentities($msg));
217e8e59 575 }
bbd802f7 576 }
217e8e59
DM
577
578 } catch (NoSuchWindow $e) {
579 // If we were interacting with a popup window it will not exists after closing it.
bbd802f7 580 }
5f4b4e91
DM
581 }
582
583 /**
584 * Converts HTML tags to line breaks to display the info in CLI
585 *
586 * @param string $html
587 * @return string
588 */
589 protected function get_debug_text($html) {
590
591 // Replacing HTML tags for new lines and keeping only the text.
592 $notags = preg_replace('/<+\s*\/*\s*([A-Z][A-Z0-9]*)\b[^>]*\/*\s*>*/i', "\n", $html);
593 return preg_replace("/(\n)+/s", "\n", $notags);
594 }
595
95b43d6b
DM
596 /**
597 * Returns whether the first scenario of the suite is running
598 *
599 * @return bool
600 */
601 protected static function is_first_scenario() {
602 return !(self::$initprocessesfinished);
603 }
3b7d3fb8
DM
604
605 /**
606 * Throws an exception after appending an extra info text.
607 *
608 * @throws Exception
609 * @param UnknownError $exception
610 * @return void
611 */
612 protected function throw_unknown_exception(UnknownError $exception) {
613 $text = get_string('unknownexceptioninfo', 'tool_behat');
614 throw new Exception($text . PHP_EOL . $exception->getMessage());
615 }
616
f5ceb6c2 617}
a964ead0 618