weekly release 3.2dev
[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
42ad096f
RT
32use Behat\Testwork\Hook\Scope\BeforeSuiteScope,
33 Behat\Testwork\Hook\Scope\AfterSuiteScope,
34 Behat\Behat\Hook\Scope\BeforeFeatureScope,
35 Behat\Behat\Hook\Scope\AfterFeatureScope,
36 Behat\Behat\Hook\Scope\BeforeScenarioScope,
37 Behat\Behat\Hook\Scope\AfterScenarioScope,
38 Behat\Behat\Hook\Scope\BeforeStepScope,
39 Behat\Behat\Hook\Scope\AfterStepScope,
89cf999a 40 Behat\Mink\Exception\DriverException as DriverException,
870349ee
DM
41 WebDriver\Exception\NoSuchWindow as NoSuchWindow,
42 WebDriver\Exception\UnexpectedAlertOpen as UnexpectedAlertOpen,
3b7d3fb8
DM
43 WebDriver\Exception\UnknownError as UnknownError,
44 WebDriver\Exception\CurlExec as CurlExec,
870349ee 45 WebDriver\Exception\NoAlertOpenError as NoAlertOpenError;
f5ceb6c2
DM
46
47/**
48 * Hooks to the behat process.
49 *
50 * Behat accepts hooks after and before each
51 * suite, feature, scenario and step.
52 *
53 * They can not call other steps as part of their process
54 * like regular steps definitions does.
55 *
56 * Throws generic Exception because they are captured by Behat.
57 *
58 * @package core
59 * @category test
60 * @copyright 2012 David Monllaó
61 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
62 */
63class behat_hooks extends behat_base {
64
41eb672b
DM
65 /**
66 * @var Last browser session start time.
67 */
68 protected static $lastbrowsersessionstart = 0;
69
95b43d6b
DM
70 /**
71 * @var For actions that should only run once.
72 */
73 protected static $initprocessesfinished = false;
74
f49cede7
DM
75 /**
76 * Some exceptions can only be caught in a before or after step hook,
77 * they can not be thrown there as they will provoke a framework level
78 * failure, but we can store them here to fail the step in i_look_for_exceptions()
79 * which result will be parsed by the framework as the last step result.
80 *
81 * @var Null or the exception last step throw in the before or after hook.
82 */
83 protected static $currentstepexception = null;
84
5c0dfe32 85 /**
a964ead0 86 * If we are saving any kind of dump on failure we should use the same parent dir during a run.
5c0dfe32
DM
87 *
88 * @var The parent dir name
89 */
a964ead0 90 protected static $faildumpdirname = false;
5c0dfe32 91
3c71c15c
RT
92 /**
93 * Keeps track of time taken by feature to execute.
94 *
95 * @var array list of feature timings
96 */
97 protected static $timings = array();
98
f5ceb6c2
DM
99 /**
100 * Gives access to moodle codebase, ensures all is ready and sets up the test lock.
101 *
102 * Includes config.php to use moodle codebase with $CFG->behat_*
103 * instead of $CFG->prefix and $CFG->dataroot, called once per suite.
104 *
3c71c15c 105 * @param SuiteEvent $event event before suite.
f5ceb6c2
DM
106 * @static
107 * @throws Exception
108 * @BeforeSuite
109 */
42ad096f 110 public static function before_suite(BeforeSuiteScope $scope) {
f5ceb6c2
DM
111 global $CFG;
112
cfcbc34a
DM
113 // Defined only when the behat CLI command is running, the moodle init setup process will
114 // read this value and switch to $CFG->behat_dataroot and $CFG->behat_prefix instead of
115 // the normal site.
116 define('BEHAT_TEST', 1);
117
f5ceb6c2 118 define('CLI_SCRIPT', 1);
cfcbc34a 119 // With BEHAT_TEST we will be using $CFG->behat_* instead of $CFG->dataroot, $CFG->prefix and $CFG->wwwroot.
f5ceb6c2
DM
120 require_once(__DIR__ . '/../../../config.php');
121
122 // Now that we are MOODLE_INTERNAL.
123 require_once(__DIR__ . '/../../behat/classes/behat_command.php');
17344d4c 124 require_once(__DIR__ . '/../../behat/classes/behat_selectors.php');
af4830a2 125 require_once(__DIR__ . '/../../behat/classes/behat_context_helper.php');
f5ceb6c2
DM
126 require_once(__DIR__ . '/../../behat/classes/util.php');
127 require_once(__DIR__ . '/../../testing/classes/test_lock.php');
b08b0a28 128 require_once(__DIR__ . '/../../testing/classes/nasty_strings.php');
f5ceb6c2
DM
129
130 // Avoids vendor/bin/behat to be executed directly without test environment enabled
131 // to prevent undesired db & dataroot modifications, this is also checked
132 // before each scenario (accidental user deletes) in the BeforeScenario hook.
133
134 if (!behat_util::is_test_mode_enabled()) {
135 throw new Exception('Behat only can run if test mode is enabled. More info in ' . behat_command::DOCS_URL . '#Running_tests');
136 }
137
d9c55725 138 // Reset all data, before checking for check_server_status.
ab25d8a7
RT
139 // If not done, then it can return apache error, while running tests.
140 behat_util::reset_all_data();
141
e891c838
RT
142 // Check if server is running and using same version for cli and apache.
143 behat_util::check_server_status();
f5ceb6c2 144
b831d479
DM
145 // Prevents using outdated data, upgrade script would start and tests would fail.
146 if (!behat_util::is_test_data_updated()) {
b32ca4ca 147 $commandpath = 'php admin/tool/behat/cli/init.php';
d69a6ad9
TH
148 throw new Exception("Your behat test site is outdated, please run\n\n " .
149 $commandpath . "\n\nfrom your moodle dirroot to drop and install the behat test site again.");
b831d479 150 }
f5ceb6c2
DM
151 // Avoid parallel tests execution, it continues when the previous lock is released.
152 test_lock::acquire('behat');
41eb672b
DM
153
154 // Store the browser reset time if reset after N seconds is specified in config.php.
155 if (!empty($CFG->behat_restart_browser_after)) {
156 // Store the initial browser session opening.
157 self::$lastbrowsersessionstart = time();
158 }
5c0dfe32 159
a964ead0
AN
160 if (!empty($CFG->behat_faildump_path) && !is_writable($CFG->behat_faildump_path)) {
161 throw new Exception('You set $CFG->behat_faildump_path to a non-writable directory');
5c0dfe32 162 }
e5f2478b
RT
163
164 // Handle interrupts on PHP7.
165 if (extension_loaded('pcntl')) {
166 $disabled = explode(',', ini_get('disable_functions'));
167 if (!in_array('pcntl_signal', $disabled)) {
168 declare(ticks = 1);
169 }
170 }
f5ceb6c2
DM
171 }
172
3c71c15c
RT
173 /**
174 * Gives access to moodle codebase, to keep track of feature start time.
175 *
176 * @param FeatureEvent $event event fired before feature.
3c71c15c
RT
177 * @BeforeFeature
178 */
42ad096f 179 public static function before_feature(BeforeFeatureScope $event) {
027212b0 180 if (!defined('BEHAT_FEATURE_TIMING_FILE')) {
3c71c15c
RT
181 return;
182 }
183 $file = $event->getFeature()->getFile();
08e7f97e
TL
184 self::$timings[$file] = microtime(true);
185 }
186
3c71c15c
RT
187 /**
188 * Gives access to moodle codebase, to keep track of feature end time.
189 *
190 * @param FeatureEvent $event event fired after feature.
3c71c15c
RT
191 * @AfterFeature
192 */
42ad096f 193 public static function after_feature(AfterFeatureScope $event) {
027212b0 194 if (!defined('BEHAT_FEATURE_TIMING_FILE')) {
3c71c15c
RT
195 return;
196 }
197 $file = $event->getFeature()->getFile();
08e7f97e
TL
198 self::$timings[$file] = microtime(true) - self::$timings[$file];
199 // Probably didn't actually run this, don't output it.
200 if (self::$timings[$file] < 1) {
201 unset(self::$timings[$file]);
202 }
203 }
204
3c71c15c
RT
205 /**
206 * Gives access to moodle codebase, to keep track of suite timings.
207 *
208 * @param SuiteEvent $event event fired after suite.
3c71c15c
RT
209 * @AfterSuite
210 */
42ad096f 211 public static function after_suite(AfterSuiteScope $event) {
027212b0 212 if (!defined('BEHAT_FEATURE_TIMING_FILE')) {
08e7f97e
TL
213 return;
214 }
215 $realroot = realpath(__DIR__.'/../../../').'/';
216 foreach (self::$timings as $k => $v) {
217 $new = str_replace($realroot, '', $k);
218 self::$timings[$new] = round($v, 1);
219 unset(self::$timings[$k]);
220 }
027212b0 221 if ($existing = @json_decode(file_get_contents(BEHAT_FEATURE_TIMING_FILE), true)) {
08e7f97e
TL
222 self::$timings = array_merge($existing, self::$timings);
223 }
224 arsort(self::$timings);
027212b0 225 @file_put_contents(BEHAT_FEATURE_TIMING_FILE, json_encode(self::$timings, JSON_PRETTY_PRINT));
08e7f97e
TL
226 }
227
f5ceb6c2
DM
228 /**
229 * Resets the test environment.
230 *
3c71c15c 231 * @param OutlineExampleEvent|ScenarioEvent $event event fired before scenario.
f5ceb6c2
DM
232 * @throws coding_exception If here we are not using the test database it should be because of a coding error
233 * @BeforeScenario
234 */
42ad096f 235 public function before_scenario(BeforeScenarioScope $scope) {
f5ceb6c2
DM
236 global $DB, $SESSION, $CFG;
237
238 // As many checks as we can.
cfcbc34a
DM
239 if (!defined('BEHAT_TEST') ||
240 !defined('BEHAT_SITE_RUNNING') ||
f5ceb6c2
DM
241 php_sapi_name() != 'cli' ||
242 !behat_util::is_test_mode_enabled() ||
cfcbc34a 243 !behat_util::is_test_site()) {
46ac40cd 244 throw new coding_exception('Behat only can modify the test database and the test dataroot!');
f5ceb6c2
DM
245 }
246
89cf999a
DM
247 $moreinfo = 'More info in ' . behat_command::DOCS_URL . '#Running_tests';
248 $driverexceptionmsg = 'Selenium server is not running, you need to start it to run tests that involve Javascript. ' . $moreinfo;
f59f6b6b
DM
249 try {
250 $session = $this->getSession();
251 } catch (CurlExec $e) {
252 // Exception thrown by WebDriver, so only @javascript tests will be caugth; in
d9c55725 253 // behat_util::check_server_status() we already checked that the server is running.
0f3fba7d 254 $this->stop_execution($driverexceptionmsg);
89cf999a 255 } catch (DriverException $e) {
0f3fba7d 256 $this->stop_execution($driverexceptionmsg);
f59f6b6b
DM
257 } catch (UnknownError $e) {
258 // Generic 'I have no idea' Selenium error. Custom exception to provide more feedback about possible solutions.
0f3fba7d 259 $this->stop_execution($e->getMessage());
f59f6b6b
DM
260 }
261
17344d4c
DM
262 // We need the Mink session to do it and we do it only before the first scenario.
263 if (self::is_first_scenario()) {
f59f6b6b 264 behat_selectors::register_moodle_selectors($session);
86055d11 265 behat_context_helper::set_session($scope->getEnvironment());
17344d4c
DM
266 }
267
55f47341
RT
268 // Reset mink session between the scenarios.
269 $session->reset();
270
de230fd3 271 // Reset $SESSION.
2e00d01d 272 \core\session\manager::init_empty_session();
c8619f33 273
ef1d45b3 274 behat_util::reset_all_data();
b08b0a28 275
de230fd3 276 // Assign valid data to admin user (some generator-related code needs a valid user).
f5ceb6c2 277 $user = $DB->get_record('user', array('username' => 'admin'));
d79d5ac2 278 \core\session\manager::set_user($user);
5f4b4e91 279
41eb672b
DM
280 // Reset the browser if specified in config.php.
281 if (!empty($CFG->behat_restart_browser_after) && $this->running_javascript()) {
282 $now = time();
283 if (self::$lastbrowsersessionstart + $CFG->behat_restart_browser_after < $now) {
f59f6b6b 284 $session->restart();
41eb672b
DM
285 self::$lastbrowsersessionstart = $now;
286 }
287 }
288
5f4b4e91 289 // Start always in the the homepage.
3b7d3fb8 290 try {
f59f6b6b
DM
291 // Let's be conservative as we never know when new upstream issues will affect us.
292 $session->visit($this->locate_path('/'));
3b7d3fb8 293 } catch (UnknownError $e) {
0f3fba7d 294 $this->stop_execution($e->getMessage());
3b7d3fb8 295 }
870349ee 296
f59f6b6b 297
95b43d6b
DM
298 // Checking that the root path is a Moodle test site.
299 if (self::is_first_scenario()) {
300 $notestsiteexception = new Exception('The base URL (' . $CFG->wwwroot . ') is not a behat test site, ' .
3b7d3fb8 301 'ensure you started the built-in web server in the correct directory or your web server is correctly started and set up');
19f6703d 302 $this->find("xpath", "//head/child::title[normalize-space(.)='" . behat_util::BEHATSITENAME . "']", $notestsiteexception);
95b43d6b
DM
303
304 self::$initprocessesfinished = true;
305 }
3b0b5e57
RT
306 // Run all test with medium (1024x768) screen size, to avoid responsive problems.
307 $this->resize_window('medium');
f5ceb6c2
DM
308 }
309
f5ceb6c2 310 /**
d1e55a47 311 * Wait for JS to complete before beginning interacting with the DOM.
f5ceb6c2 312 *
f49cede7
DM
313 * Executed only when running against a real browser. We wrap it
314 * all in a try & catch to forward the exception to i_look_for_exceptions
315 * so the exception will be at scenario level, which causes a failure, by
316 * default would be at framework level, which will stop the execution of
317 * the run.
f5ceb6c2 318 *
42ad096f 319 * @BeforeStep
f5ceb6c2 320 */
42ad096f
RT
321 public function before_step_javascript(BeforeStepScope $scope) {
322 self::$currentstepexception = null;
f49cede7 323
42ad096f
RT
324 // Only run if JS.
325 if ($this->running_javascript()) {
326 try {
327 $this->wait_for_pending_js();
328 } catch (Exception $e) {
329 self::$currentstepexception = $e;
330 }
f49cede7 331 }
d1e55a47
DM
332 }
333
334 /**
335 * Wait for JS to complete after finishing the step.
336 *
337 * With this we ensure that there are not AJAX calls
338 * still in progress.
339 *
f49cede7
DM
340 * Executed only when running against a real browser. We wrap it
341 * all in a try & catch to forward the exception to i_look_for_exceptions
342 * so the exception will be at scenario level, which causes a failure, by
343 * default would be at framework level, which will stop the execution of
344 * the run.
d1e55a47 345 *
42ad096f 346 * @AfterStep
d1e55a47 347 */
42ad096f
RT
348 public function after_step_javascript(AfterStepScope $scope) {
349 global $CFG, $DB;
350
351 // Save the page content if the step failed.
352 if (!empty($CFG->behat_faildump_path) &&
353 $scope->getTestResult()->getResultCode() === Behat\Testwork\Tester\Result\TestResult::FAILED) {
354 $this->take_contentdump($scope);
355 }
356
357 // Abort any open transactions to prevent subsequent tests hanging.
358 // This does the same as abort_all_db_transactions(), but doesn't call error_log() as we don't
359 // want to see a message in the behat output.
360 if (($scope->getTestResult() instanceof \Behat\Behat\Tester\Result\ExecutedStepResult) &&
361 $scope->getTestResult()->hasException()) {
362 if ($DB && $DB->is_transaction_started()) {
363 $DB->force_transaction_rollback();
364 }
365 }
366
367 // Only run if JS.
368 if (!$this->running_javascript()) {
369 return;
370 }
5c0dfe32
DM
371
372 // Save a screenshot if the step failed.
a964ead0 373 if (!empty($CFG->behat_faildump_path) &&
42ad096f
RT
374 $scope->getTestResult()->getResultCode() === Behat\Testwork\Tester\Result\TestResult::FAILED) {
375 $this->take_screenshot($scope);
5c0dfe32 376 }
f49cede7
DM
377
378 try {
379 $this->wait_for_pending_js();
380 self::$currentstepexception = null;
381 } catch (UnexpectedAlertOpen $e) {
382 self::$currentstepexception = $e;
383
384 // Accepting the alert so the framework can continue properly running
385 // the following scenarios. Some browsers already closes the alert, so
386 // wrapping in a try & catch.
387 try {
388 $this->getSession()->getDriver()->getWebDriverSession()->accept_alert();
389 } catch (Exception $e) {
390 // Catching the generic one as we never know how drivers reacts here.
391 }
392 } catch (Exception $e) {
393 self::$currentstepexception = $e;
394 }
d1e55a47
DM
395 }
396
131d4ac2
T
397 /**
398 * Executed after scenario having switch window to restart session.
399 * This is needed to close all extra browser windows and starting
400 * one browser window.
401 *
42ad096f 402 * @param AfterScenarioScope $event event fired after scenario.
131d4ac2
T
403 * @AfterScenario @_switch_window
404 */
42ad096f 405 public function after_scenario_switchwindow(AfterScenarioScope $event) {
af7cadbb 406 for ($count = 0; $count < self::EXTENDED_TIMEOUT; $count++) {
5e483833
RT
407 try {
408 $this->getSession()->restart();
409 break;
410 } catch (DriverException $e) {
411 // Wait for timeout and try again.
412 sleep(self::TIMEOUT);
413 }
414 }
415 // If session is not restarted above then it will try to start session before next scenario
416 // and if that fails then exception will be thrown.
131d4ac2
T
417 }
418
a964ead0
AN
419 /**
420 * Getter for self::$faildumpdirname
5c0dfe32
DM
421 *
422 * @return string
423 */
a964ead0
AN
424 protected function get_run_faildump_dir() {
425 return self::$faildumpdirname;
5c0dfe32
DM
426 }
427
428 /**
429 * Take screenshot when a step fails.
3ec07614 430 *
5c0dfe32 431 * @throws Exception
42ad096f 432 * @param AfterStepScope $scope
3ec07614 433 */
42ad096f 434 protected function take_screenshot(AfterStepScope $scope) {
5c0dfe32
DM
435 // Goutte can't save screenshots.
436 if (!$this->running_javascript()) {
437 return false;
3ec07614 438 }
5c0dfe32 439
be8b8950
MJ
440 // Some drivers (e.g. chromedriver) may throw an exception while trying to take a screenshot. If this isn't handled,
441 // the behat run dies. We don't want to lose the information about the failure that triggered the screenshot,
442 // so let's log the exception message to a file (to explain why there's no screenshot) and allow the run to continue,
443 // handling the failure as normal.
444 try {
445 list ($dir, $filename) = $this->get_faildump_filename($scope, 'png');
446 $this->saveScreenshot($filename, $dir);
447 } catch (Exception $e) {
448 // Catching all exceptions as we don't know what the driver might throw.
449 list ($dir, $filename) = $this->get_faildump_filename($scope, 'txt');
450 $message = "Could not save screenshot due to an error\n" . $e->getMessage();
451 file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $message);
452 }
a964ead0
AN
453 }
454
455 /**
456 * Take a dump of the page content when a step fails.
457 *
458 * @throws Exception
42ad096f 459 * @param AfterStepScope $scope
a964ead0 460 */
42ad096f
RT
461 protected function take_contentdump(AfterStepScope $scope) {
462 list ($dir, $filename) = $this->get_faildump_filename($scope, 'html');
a964ead0 463
be8b8950
MJ
464 try {
465 // Driver may throw an exception during getContent(), so do it first to avoid getting an empty file.
466 $content = $this->getSession()->getPage()->getContent();
467 } catch (Exception $e) {
468 // Catching all exceptions as we don't know what the driver might throw.
469 $content = "Could not save contentdump due to an error\n" . $e->getMessage();
470 }
471 file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $content);
a964ead0
AN
472 }
473
474 /**
475 * Determine the full pathname to store a failure-related dump.
476 *
477 * This is used for content such as the DOM, and screenshots.
478 *
42ad096f 479 * @param AfterStepScope $scope
167486b3 480 * @param String $filetype The file suffix to use. Limited to 4 chars.
a964ead0 481 */
42ad096f 482 protected function get_faildump_filename(AfterStepScope $scope, $filetype) {
a964ead0
AN
483 global $CFG;
484
485 // All the contentdumps should be in the same parent dir.
486 if (!$faildumpdir = self::get_run_faildump_dir()) {
487 $faildumpdir = self::$faildumpdirname = date('Ymd_His');
5c0dfe32 488
a964ead0 489 $dir = $CFG->behat_faildump_path . DIRECTORY_SEPARATOR . $faildumpdir;
5c0dfe32 490
a964ead0 491 if (!is_dir($dir) && !mkdir($dir, $CFG->directorypermissions, true)) {
5c0dfe32 492 // It shouldn't, we already checked that the directory is writable.
a964ead0 493 throw new Exception('No directories can be created inside $CFG->behat_faildump_path, check the directory permissions.');
5c0dfe32
DM
494 }
495 } else {
496 // We will always need to know the full path.
a964ead0 497 $dir = $CFG->behat_faildump_path . DIRECTORY_SEPARATOR . $faildumpdir;
5c0dfe32
DM
498 }
499
500 // The scenario title + the failed step text.
a964ead0 501 // We want a i-am-the-scenario-title_i-am-the-failed-step.$filetype format.
42ad096f 502 $filename = $scope->getFeature()->getTitle() . '_' . $scope->getStep()->getText();
167486b3
DM
503 $filename = preg_replace('/([^a-zA-Z0-9\_]+)/', '-', $filename);
504
42ad096f 505 // File name limited to 255 characters. Leaving 5 chars for line number and 4 chars for the file.
167486b3 506 // extension as we allow .png for images and .html for DOM contents.
42ad096f 507 $filename = substr($filename, 0, 245) . '_' . $scope->getStep()->getLine() . '.' . $filetype;
5c0dfe32 508
a964ead0 509 return array($dir, $filename);
3ec07614
DW
510 }
511
5f4b4e91
DM
512 /**
513 * Internal step definition to find exceptions, debugging() messages and PHP debug messages.
514 *
515 * Part of behat_hooks class as is part of the testing framework, is auto-executed
516 * after each step so no features will splicitly use it.
517 *
518 * @Given /^I look for exceptions$/
f49cede7 519 * @throw Exception Unknown type, depending on what we caught in the hook or basic \Exception.
5f4b4e91
DM
520 * @see Moodle\BehatExtension\Tester\MoodleStepTester
521 */
522 public function i_look_for_exceptions() {
f49cede7
DM
523 // If the step already failed in a hook throw the exception.
524 if (!is_null(self::$currentstepexception)) {
525 throw self::$currentstepexception;
526 }
527
eb9ca848 528 $this->look_for_exceptions();
5f4b4e91
DM
529 }
530
95b43d6b
DM
531 /**
532 * Returns whether the first scenario of the suite is running
533 *
534 * @return bool
535 */
536 protected static function is_first_scenario() {
537 return !(self::$initprocessesfinished);
538 }
3b7d3fb8
DM
539
540 /**
0f3fba7d 541 * Stops execution because of some exception.
3b7d3fb8 542 *
0f3fba7d 543 * @param string $exception
3b7d3fb8
DM
544 * @return void
545 */
0f3fba7d 546 protected function stop_execution($exception) {
3b7d3fb8 547 $text = get_string('unknownexceptioninfo', 'tool_behat');
0f3fba7d
RT
548 echo $text . PHP_EOL . $exception . PHP_EOL;
549 exit(1);
3b7d3fb8
DM
550 }
551
f5ceb6c2 552}
a964ead0 553