MDL-63977 Behat: Fix bug in 'near' calculation
[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
5aa9b5ce
RT
99 /**
100 * Keeps track of current running suite name.
101 *
102 * @var string current running suite name
103 */
b4bc4286 104 protected static $runningsuite = '';
5aa9b5ce 105
25fbce21
RT
106 /**
107 * Hook to capture BeforeSuite event so as to give access to moodle codebase.
108 * This will try and catch any exception and exists if anything fails.
109 *
110 * @param BeforeSuiteScope $scope scope passed by event fired before suite.
111 * @BeforeSuite
112 */
113 public static function before_suite_hook(BeforeSuiteScope $scope) {
b468dbf5
RT
114 // If behat has been initialised then no need to do this again.
115 if (self::$initprocessesfinished) {
116 return;
117 }
118
25fbce21
RT
119 try {
120 self::before_suite($scope);
121 } catch (behat_stop_exception $e) {
122 echo $e->getMessage() . PHP_EOL;
123 exit(1);
124 }
125 }
126
f5ceb6c2
DM
127 /**
128 * Gives access to moodle codebase, ensures all is ready and sets up the test lock.
129 *
130 * Includes config.php to use moodle codebase with $CFG->behat_*
131 * instead of $CFG->prefix and $CFG->dataroot, called once per suite.
132 *
25fbce21 133 * @param BeforeSuiteScope $scope scope passed by event fired before suite.
f5ceb6c2 134 * @static
25fbce21 135 * @throws behat_stop_exception
f5ceb6c2 136 */
42ad096f 137 public static function before_suite(BeforeSuiteScope $scope) {
f5ceb6c2
DM
138 global $CFG;
139
cfcbc34a
DM
140 // Defined only when the behat CLI command is running, the moodle init setup process will
141 // read this value and switch to $CFG->behat_dataroot and $CFG->behat_prefix instead of
142 // the normal site.
5aa9b5ce
RT
143 if (!defined('BEHAT_TEST')) {
144 define('BEHAT_TEST', 1);
145 }
146
147 if (!defined('CLI_SCRIPT')) {
148 define('CLI_SCRIPT', 1);
149 }
cfcbc34a 150
cfcbc34a 151 // With BEHAT_TEST we will be using $CFG->behat_* instead of $CFG->dataroot, $CFG->prefix and $CFG->wwwroot.
f5ceb6c2
DM
152 require_once(__DIR__ . '/../../../config.php');
153
154 // Now that we are MOODLE_INTERNAL.
155 require_once(__DIR__ . '/../../behat/classes/behat_command.php');
30374580 156 require_once(__DIR__ . '/../../behat/classes/behat_selectors.php');
af4830a2 157 require_once(__DIR__ . '/../../behat/classes/behat_context_helper.php');
f5ceb6c2
DM
158 require_once(__DIR__ . '/../../behat/classes/util.php');
159 require_once(__DIR__ . '/../../testing/classes/test_lock.php');
b08b0a28 160 require_once(__DIR__ . '/../../testing/classes/nasty_strings.php');
f5ceb6c2
DM
161
162 // Avoids vendor/bin/behat to be executed directly without test environment enabled
163 // to prevent undesired db & dataroot modifications, this is also checked
164 // before each scenario (accidental user deletes) in the BeforeScenario hook.
165
166 if (!behat_util::is_test_mode_enabled()) {
25fbce21
RT
167 throw new behat_stop_exception('Behat only can run if test mode is enabled. More info in ' .
168 behat_command::DOCS_URL . '#Running_tests');
f5ceb6c2
DM
169 }
170
d9c55725 171 // Reset all data, before checking for check_server_status.
ab25d8a7 172 // If not done, then it can return apache error, while running tests.
39eb8abe 173 behat_util::clean_tables_updated_by_scenario_list();
ab25d8a7
RT
174 behat_util::reset_all_data();
175
e891c838
RT
176 // Check if server is running and using same version for cli and apache.
177 behat_util::check_server_status();
f5ceb6c2 178
b831d479
DM
179 // Prevents using outdated data, upgrade script would start and tests would fail.
180 if (!behat_util::is_test_data_updated()) {
b32ca4ca 181 $commandpath = 'php admin/tool/behat/cli/init.php';
25fbce21 182 throw new behat_stop_exception("Your behat test site is outdated, please run\n\n " .
d69a6ad9 183 $commandpath . "\n\nfrom your moodle dirroot to drop and install the behat test site again.");
b831d479 184 }
f5ceb6c2
DM
185 // Avoid parallel tests execution, it continues when the previous lock is released.
186 test_lock::acquire('behat');
41eb672b
DM
187
188 // Store the browser reset time if reset after N seconds is specified in config.php.
189 if (!empty($CFG->behat_restart_browser_after)) {
190 // Store the initial browser session opening.
191 self::$lastbrowsersessionstart = time();
192 }
5c0dfe32 193
a964ead0 194 if (!empty($CFG->behat_faildump_path) && !is_writable($CFG->behat_faildump_path)) {
25fbce21 195 throw new behat_stop_exception('You set $CFG->behat_faildump_path to a non-writable directory');
5c0dfe32 196 }
e5f2478b
RT
197
198 // Handle interrupts on PHP7.
199 if (extension_loaded('pcntl')) {
200 $disabled = explode(',', ini_get('disable_functions'));
201 if (!in_array('pcntl_signal', $disabled)) {
202 declare(ticks = 1);
203 }
204 }
f5ceb6c2
DM
205 }
206
3c71c15c
RT
207 /**
208 * Gives access to moodle codebase, to keep track of feature start time.
209 *
25fbce21 210 * @param BeforeFeatureScope $scope scope passed by event fired before feature.
3c71c15c
RT
211 * @BeforeFeature
212 */
25fbce21 213 public static function before_feature(BeforeFeatureScope $scope) {
027212b0 214 if (!defined('BEHAT_FEATURE_TIMING_FILE')) {
3c71c15c
RT
215 return;
216 }
25fbce21 217 $file = $scope->getFeature()->getFile();
08e7f97e
TL
218 self::$timings[$file] = microtime(true);
219 }
220
3c71c15c
RT
221 /**
222 * Gives access to moodle codebase, to keep track of feature end time.
223 *
25fbce21 224 * @param AfterFeatureScope $scope scope passed by event fired after feature.
3c71c15c
RT
225 * @AfterFeature
226 */
25fbce21 227 public static function after_feature(AfterFeatureScope $scope) {
027212b0 228 if (!defined('BEHAT_FEATURE_TIMING_FILE')) {
3c71c15c
RT
229 return;
230 }
25fbce21 231 $file = $scope->getFeature()->getFile();
08e7f97e
TL
232 self::$timings[$file] = microtime(true) - self::$timings[$file];
233 // Probably didn't actually run this, don't output it.
234 if (self::$timings[$file] < 1) {
235 unset(self::$timings[$file]);
236 }
237 }
238
3c71c15c
RT
239 /**
240 * Gives access to moodle codebase, to keep track of suite timings.
241 *
25fbce21 242 * @param AfterSuiteScope $scope scope passed by event fired after suite.
3c71c15c
RT
243 * @AfterSuite
244 */
25fbce21 245 public static function after_suite(AfterSuiteScope $scope) {
027212b0 246 if (!defined('BEHAT_FEATURE_TIMING_FILE')) {
08e7f97e
TL
247 return;
248 }
249 $realroot = realpath(__DIR__.'/../../../').'/';
250 foreach (self::$timings as $k => $v) {
251 $new = str_replace($realroot, '', $k);
252 self::$timings[$new] = round($v, 1);
253 unset(self::$timings[$k]);
254 }
027212b0 255 if ($existing = @json_decode(file_get_contents(BEHAT_FEATURE_TIMING_FILE), true)) {
08e7f97e
TL
256 self::$timings = array_merge($existing, self::$timings);
257 }
258 arsort(self::$timings);
027212b0 259 @file_put_contents(BEHAT_FEATURE_TIMING_FILE, json_encode(self::$timings, JSON_PRETTY_PRINT));
08e7f97e
TL
260 }
261
f5ceb6c2 262 /**
25fbce21 263 * Hook to capture before scenario event to get scope.
f5ceb6c2 264 *
25fbce21 265 * @param BeforeScenarioScope $scope scope passed by event fired before scenario.
f5ceb6c2
DM
266 * @BeforeScenario
267 */
25fbce21
RT
268 public function before_scenario_hook(BeforeScenarioScope $scope) {
269 try {
270 $this->before_scenario($scope);
271 } catch (behat_stop_exception $e) {
272 echo $e->getMessage() . PHP_EOL;
273 exit(1);
274 }
275 }
276
277 /**
278 * Resets the test environment.
279 *
280 * @param BeforeScenarioScope $scope scope passed by event fired before scenario.
281 * @throws behat_stop_exception If here we are not using the test database it should be because of a coding error
282 */
42ad096f 283 public function before_scenario(BeforeScenarioScope $scope) {
5aa9b5ce 284 global $DB, $CFG;
f5ceb6c2
DM
285
286 // As many checks as we can.
cfcbc34a
DM
287 if (!defined('BEHAT_TEST') ||
288 !defined('BEHAT_SITE_RUNNING') ||
f5ceb6c2
DM
289 php_sapi_name() != 'cli' ||
290 !behat_util::is_test_mode_enabled() ||
cfcbc34a 291 !behat_util::is_test_site()) {
25fbce21 292 throw new behat_stop_exception('Behat only can modify the test database and the test dataroot!');
f5ceb6c2
DM
293 }
294
89cf999a
DM
295 $moreinfo = 'More info in ' . behat_command::DOCS_URL . '#Running_tests';
296 $driverexceptionmsg = 'Selenium server is not running, you need to start it to run tests that involve Javascript. ' . $moreinfo;
f59f6b6b
DM
297 try {
298 $session = $this->getSession();
299 } catch (CurlExec $e) {
300 // Exception thrown by WebDriver, so only @javascript tests will be caugth; in
d9c55725 301 // behat_util::check_server_status() we already checked that the server is running.
25fbce21 302 throw new behat_stop_exception($driverexceptionmsg);
89cf999a 303 } catch (DriverException $e) {
25fbce21 304 throw new behat_stop_exception($driverexceptionmsg);
f59f6b6b
DM
305 } catch (UnknownError $e) {
306 // Generic 'I have no idea' Selenium error. Custom exception to provide more feedback about possible solutions.
25fbce21 307 throw new behat_stop_exception($e->getMessage());
f59f6b6b
DM
308 }
309
b4bc4286
RT
310 $suitename = $scope->getSuite()->getName();
311
312 // Register behat selectors for theme, if suite is changed. We do it for every suite change.
313 if ($suitename !== self::$runningsuite) {
314 behat_context_helper::set_environment($scope->getEnvironment());
315
316 // We need the Mink session to do it and we do it only before the first scenario.
520071f0
AN
317 $namedpartialclass = 'behat_partial_named_selector';
318 $namedexactclass = 'behat_exact_named_selector';
319
4422ffc5
RT
320 // If override selector exist, then set it as default behat selectors class.
321 $overrideclass = behat_config_util::get_behat_theme_selector_override_classname($suitename, 'named_partial', true);
322 if (class_exists($overrideclass)) {
323 $namedpartialclass = $overrideclass;
b4bc4286 324 }
4422ffc5
RT
325
326 // If override selector exist, then set it as default behat selectors class.
327 $overrideclass = behat_config_util::get_behat_theme_selector_override_classname($suitename, 'named_exact', true);
328 if (class_exists($overrideclass)) {
329 $namedexactclass = $overrideclass;
330 }
331
520071f0
AN
332 $this->getSession()->getSelectorsHandler()->registerSelector('named_partial', new $namedpartialclass());
333 $this->getSession()->getSelectorsHandler()->registerSelector('named_exact', new $namedexactclass());
17344d4c
DM
334 }
335
55f47341
RT
336 // Reset mink session between the scenarios.
337 $session->reset();
338
de230fd3 339 // Reset $SESSION.
2e00d01d 340 \core\session\manager::init_empty_session();
c8619f33 341
8c35e49b
RT
342 // Ignore E_NOTICE and E_WARNING during reset, as this might be caused because of some existing process
343 // running ajax. This will be investigated in another issue.
344 $errorlevel = error_reporting();
345 error_reporting($errorlevel & ~E_NOTICE & ~E_WARNING);
ef1d45b3 346 behat_util::reset_all_data();
8c35e49b 347 error_reporting($errorlevel);
b08b0a28 348
de230fd3 349 // Assign valid data to admin user (some generator-related code needs a valid user).
f5ceb6c2 350 $user = $DB->get_record('user', array('username' => 'admin'));
d79d5ac2 351 \core\session\manager::set_user($user);
5f4b4e91 352
41eb672b
DM
353 // Reset the browser if specified in config.php.
354 if (!empty($CFG->behat_restart_browser_after) && $this->running_javascript()) {
355 $now = time();
356 if (self::$lastbrowsersessionstart + $CFG->behat_restart_browser_after < $now) {
f59f6b6b 357 $session->restart();
41eb672b
DM
358 self::$lastbrowsersessionstart = $now;
359 }
360 }
361
b4bc4286
RT
362 // Set the theme if not default.
363 if ($suitename !== "default") {
364 set_config('theme', $suitename);
365 self::$runningsuite = $suitename;
366 }
367
5f4b4e91 368 // Start always in the the homepage.
3b7d3fb8 369 try {
f59f6b6b
DM
370 // Let's be conservative as we never know when new upstream issues will affect us.
371 $session->visit($this->locate_path('/'));
3b7d3fb8 372 } catch (UnknownError $e) {
25fbce21 373 throw new behat_stop_exception($e->getMessage());
3b7d3fb8 374 }
870349ee 375
95b43d6b
DM
376 // Checking that the root path is a Moodle test site.
377 if (self::is_first_scenario()) {
25fbce21 378 $notestsiteexception = new behat_stop_exception('The base URL (' . $CFG->wwwroot . ') is not a behat test site, ' .
3b7d3fb8 379 'ensure you started the built-in web server in the correct directory or your web server is correctly started and set up');
19f6703d 380 $this->find("xpath", "//head/child::title[normalize-space(.)='" . behat_util::BEHATSITENAME . "']", $notestsiteexception);
95b43d6b
DM
381
382 self::$initprocessesfinished = true;
383 }
5aa9b5ce 384
3b0b5e57
RT
385 // Run all test with medium (1024x768) screen size, to avoid responsive problems.
386 $this->resize_window('medium');
f5ceb6c2
DM
387 }
388
f5ceb6c2 389 /**
d1e55a47 390 * Wait for JS to complete before beginning interacting with the DOM.
f5ceb6c2 391 *
f49cede7
DM
392 * Executed only when running against a real browser. We wrap it
393 * all in a try & catch to forward the exception to i_look_for_exceptions
394 * so the exception will be at scenario level, which causes a failure, by
395 * default would be at framework level, which will stop the execution of
396 * the run.
f5ceb6c2 397 *
25fbce21 398 * @param BeforeStepScope $scope scope passed by event fired before step.
42ad096f 399 * @BeforeStep
f5ceb6c2 400 */
42ad096f
RT
401 public function before_step_javascript(BeforeStepScope $scope) {
402 self::$currentstepexception = null;
f49cede7 403
42ad096f
RT
404 // Only run if JS.
405 if ($this->running_javascript()) {
406 try {
407 $this->wait_for_pending_js();
408 } catch (Exception $e) {
409 self::$currentstepexception = $e;
410 }
f49cede7 411 }
d1e55a47
DM
412 }
413
414 /**
415 * Wait for JS to complete after finishing the step.
416 *
417 * With this we ensure that there are not AJAX calls
418 * still in progress.
419 *
f49cede7
DM
420 * Executed only when running against a real browser. We wrap it
421 * all in a try & catch to forward the exception to i_look_for_exceptions
422 * so the exception will be at scenario level, which causes a failure, by
423 * default would be at framework level, which will stop the execution of
424 * the run.
d1e55a47 425 *
25fbce21 426 * @param AfterStepScope $scope scope passed by event fired after step..
42ad096f 427 * @AfterStep
d1e55a47 428 */
42ad096f
RT
429 public function after_step_javascript(AfterStepScope $scope) {
430 global $CFG, $DB;
431
ada21cb0
RT
432 // If step is undefined then throw exception, to get failed exit code.
433 if ($scope->getTestResult()->getResultCode() === Behat\Behat\Tester\Result\StepResult::UNDEFINED) {
434 throw new coding_exception("Step '" . $scope->getStep()->getText() . "'' is undefined.");
435 }
436
42ad096f
RT
437 // Save the page content if the step failed.
438 if (!empty($CFG->behat_faildump_path) &&
439 $scope->getTestResult()->getResultCode() === Behat\Testwork\Tester\Result\TestResult::FAILED) {
440 $this->take_contentdump($scope);
441 }
442
443 // Abort any open transactions to prevent subsequent tests hanging.
444 // This does the same as abort_all_db_transactions(), but doesn't call error_log() as we don't
445 // want to see a message in the behat output.
446 if (($scope->getTestResult() instanceof \Behat\Behat\Tester\Result\ExecutedStepResult) &&
447 $scope->getTestResult()->hasException()) {
448 if ($DB && $DB->is_transaction_started()) {
449 $DB->force_transaction_rollback();
450 }
451 }
452
453 // Only run if JS.
454 if (!$this->running_javascript()) {
455 return;
456 }
5c0dfe32
DM
457
458 // Save a screenshot if the step failed.
a964ead0 459 if (!empty($CFG->behat_faildump_path) &&
42ad096f
RT
460 $scope->getTestResult()->getResultCode() === Behat\Testwork\Tester\Result\TestResult::FAILED) {
461 $this->take_screenshot($scope);
5c0dfe32 462 }
f49cede7
DM
463
464 try {
465 $this->wait_for_pending_js();
466 self::$currentstepexception = null;
467 } catch (UnexpectedAlertOpen $e) {
468 self::$currentstepexception = $e;
469
470 // Accepting the alert so the framework can continue properly running
471 // the following scenarios. Some browsers already closes the alert, so
472 // wrapping in a try & catch.
473 try {
474 $this->getSession()->getDriver()->getWebDriverSession()->accept_alert();
475 } catch (Exception $e) {
476 // Catching the generic one as we never know how drivers reacts here.
477 }
478 } catch (Exception $e) {
479 self::$currentstepexception = $e;
480 }
d1e55a47
DM
481 }
482
131d4ac2
T
483 /**
484 * Executed after scenario having switch window to restart session.
485 * This is needed to close all extra browser windows and starting
486 * one browser window.
487 *
25fbce21 488 * @param AfterScenarioScope $scope scope passed by event fired after scenario.
131d4ac2
T
489 * @AfterScenario @_switch_window
490 */
25fbce21 491 public function after_scenario_switchwindow(AfterScenarioScope $scope) {
af7cadbb 492 for ($count = 0; $count < self::EXTENDED_TIMEOUT; $count++) {
5e483833
RT
493 try {
494 $this->getSession()->restart();
495 break;
496 } catch (DriverException $e) {
497 // Wait for timeout and try again.
498 sleep(self::TIMEOUT);
499 }
500 }
501 // If session is not restarted above then it will try to start session before next scenario
502 // and if that fails then exception will be thrown.
131d4ac2
T
503 }
504
a964ead0
AN
505 /**
506 * Getter for self::$faildumpdirname
5c0dfe32
DM
507 *
508 * @return string
509 */
a964ead0
AN
510 protected function get_run_faildump_dir() {
511 return self::$faildumpdirname;
5c0dfe32
DM
512 }
513
514 /**
515 * Take screenshot when a step fails.
3ec07614 516 *
5c0dfe32 517 * @throws Exception
25fbce21 518 * @param AfterStepScope $scope scope passed by event after step.
3ec07614 519 */
42ad096f 520 protected function take_screenshot(AfterStepScope $scope) {
5c0dfe32
DM
521 // Goutte can't save screenshots.
522 if (!$this->running_javascript()) {
523 return false;
3ec07614 524 }
5c0dfe32 525
be8b8950
MJ
526 // Some drivers (e.g. chromedriver) may throw an exception while trying to take a screenshot. If this isn't handled,
527 // the behat run dies. We don't want to lose the information about the failure that triggered the screenshot,
528 // so let's log the exception message to a file (to explain why there's no screenshot) and allow the run to continue,
529 // handling the failure as normal.
530 try {
531 list ($dir, $filename) = $this->get_faildump_filename($scope, 'png');
532 $this->saveScreenshot($filename, $dir);
533 } catch (Exception $e) {
534 // Catching all exceptions as we don't know what the driver might throw.
535 list ($dir, $filename) = $this->get_faildump_filename($scope, 'txt');
536 $message = "Could not save screenshot due to an error\n" . $e->getMessage();
537 file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $message);
538 }
a964ead0
AN
539 }
540
541 /**
542 * Take a dump of the page content when a step fails.
543 *
544 * @throws Exception
25fbce21 545 * @param AfterStepScope $scope scope passed by event after step.
a964ead0 546 */
42ad096f
RT
547 protected function take_contentdump(AfterStepScope $scope) {
548 list ($dir, $filename) = $this->get_faildump_filename($scope, 'html');
a964ead0 549
be8b8950
MJ
550 try {
551 // Driver may throw an exception during getContent(), so do it first to avoid getting an empty file.
552 $content = $this->getSession()->getPage()->getContent();
553 } catch (Exception $e) {
554 // Catching all exceptions as we don't know what the driver might throw.
555 $content = "Could not save contentdump due to an error\n" . $e->getMessage();
556 }
557 file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $content);
a964ead0
AN
558 }
559
560 /**
561 * Determine the full pathname to store a failure-related dump.
562 *
563 * This is used for content such as the DOM, and screenshots.
564 *
25fbce21 565 * @param AfterStepScope $scope scope passed by event after step.
167486b3 566 * @param String $filetype The file suffix to use. Limited to 4 chars.
a964ead0 567 */
42ad096f 568 protected function get_faildump_filename(AfterStepScope $scope, $filetype) {
a964ead0
AN
569 global $CFG;
570
571 // All the contentdumps should be in the same parent dir.
572 if (!$faildumpdir = self::get_run_faildump_dir()) {
573 $faildumpdir = self::$faildumpdirname = date('Ymd_His');
5c0dfe32 574
a964ead0 575 $dir = $CFG->behat_faildump_path . DIRECTORY_SEPARATOR . $faildumpdir;
5c0dfe32 576
a964ead0 577 if (!is_dir($dir) && !mkdir($dir, $CFG->directorypermissions, true)) {
5c0dfe32 578 // It shouldn't, we already checked that the directory is writable.
a964ead0 579 throw new Exception('No directories can be created inside $CFG->behat_faildump_path, check the directory permissions.');
5c0dfe32
DM
580 }
581 } else {
582 // We will always need to know the full path.
a964ead0 583 $dir = $CFG->behat_faildump_path . DIRECTORY_SEPARATOR . $faildumpdir;
5c0dfe32
DM
584 }
585
586 // The scenario title + the failed step text.
a964ead0 587 // We want a i-am-the-scenario-title_i-am-the-failed-step.$filetype format.
42ad096f 588 $filename = $scope->getFeature()->getTitle() . '_' . $scope->getStep()->getText();
167486b3 589
484503da 590 // As file name is limited to 255 characters. Leaving 5 chars for line number and 4 chars for the file.
167486b3 591 // extension as we allow .png for images and .html for DOM contents.
484503da
RT
592 $filenamelen = 245;
593
594 // Suffix suite name to faildump file, if it's not default suite.
595 $suitename = $scope->getSuite()->getName();
596 if ($suitename != 'default') {
597 $suitename = '_' . $suitename;
598 $filenamelen = $filenamelen - strlen($suitename);
599 } else {
600 // No need to append suite name for default.
601 $suitename = '';
602 }
603
604 $filename = preg_replace('/([^a-zA-Z0-9\_]+)/', '-', $filename);
605 $filename = substr($filename, 0, $filenamelen) . $suitename . '_' . $scope->getStep()->getLine() . '.' . $filetype;
5c0dfe32 606
a964ead0 607 return array($dir, $filename);
3ec07614
DW
608 }
609
5f4b4e91
DM
610 /**
611 * Internal step definition to find exceptions, debugging() messages and PHP debug messages.
612 *
613 * Part of behat_hooks class as is part of the testing framework, is auto-executed
614 * after each step so no features will splicitly use it.
615 *
616 * @Given /^I look for exceptions$/
f49cede7 617 * @throw Exception Unknown type, depending on what we caught in the hook or basic \Exception.
1b372a77 618 * @see Moodle\BehatExtension\EventDispatcher\Tester\ChainedStepTester
5f4b4e91
DM
619 */
620 public function i_look_for_exceptions() {
f49cede7
DM
621 // If the step already failed in a hook throw the exception.
622 if (!is_null(self::$currentstepexception)) {
623 throw self::$currentstepexception;
624 }
625
eb9ca848 626 $this->look_for_exceptions();
5f4b4e91
DM
627 }
628
95b43d6b
DM
629 /**
630 * Returns whether the first scenario of the suite is running
631 *
632 * @return bool
633 */
634 protected static function is_first_scenario() {
635 return !(self::$initprocessesfinished);
636 }
f5ceb6c2 637}
a964ead0 638
25fbce21
RT
639/**
640 * Behat stop exception
641 *
642 * This exception is thrown from before suite or scenario if any setup problem found.
643 *
644 * @package core_test
645 * @copyright 2016 Rajesh Taneja <rajesh@moodle.com>
646 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
647 */
e584e6ae 648class behat_stop_exception extends \Exception {
bd1edc83 649}