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