Differences Between: [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403]
1 <?php 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 30 require_once (__DIR__ . '/../../behat/behat_base.php'); 31 32 use 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, 40 Behat\Mink\Exception\ExpectationException, 41 Behat\Mink\Exception\DriverException, 42 Facebook\WebDriver\Exception\UnexpectedAlertOpenException, 43 Facebook\WebDriver\Exception\WebDriverCurlException, 44 Facebook\WebDriver\Exception\UnknownErrorException; 45 46 /** 47 * Hooks to the behat process. 48 * 49 * Behat accepts hooks after and before each 50 * suite, feature, scenario and step. 51 * 52 * They can not call other steps as part of their process 53 * like regular steps definitions does. 54 * 55 * Throws generic Exception because they are captured by Behat. 56 * 57 * @package core 58 * @category test 59 * @copyright 2012 David MonllaĆ³ 60 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 61 */ 62 class behat_hooks extends behat_base { 63 64 /** 65 * @var For actions that should only run once. 66 */ 67 protected static $initprocessesfinished = false; 68 69 /** @var bool Whether the first javascript scenario has been seen yet */ 70 protected static $firstjavascriptscenarioseen = false; 71 72 /** 73 * @var bool Scenario running 74 */ 75 protected $scenariorunning = false; 76 77 /** 78 * Some exceptions can only be caught in a before or after step hook, 79 * they can not be thrown there as they will provoke a framework level 80 * failure, but we can store them here to fail the step in i_look_for_exceptions() 81 * which result will be parsed by the framework as the last step result. 82 * 83 * @var ?Exception Null or the exception last step throw in the before or after hook. 84 */ 85 protected static $currentstepexception = null; 86 87 /** 88 * If an Exception is thrown in the BeforeScenario hook it will cause the Scenario to be skipped, and the exit code 89 * to be non-zero triggering a potential rerun. 90 * 91 * To combat this the exception is stored and re-thrown when looking for exceptions. 92 * This allows the test to instead be failed and re-run correctly. 93 * 94 * @var null|Exception 95 */ 96 protected static $currentscenarioexception = null; 97 98 /** 99 * If we are saving any kind of dump on failure we should use the same parent dir during a run. 100 * 101 * @var The parent dir name 102 */ 103 protected static $faildumpdirname = false; 104 105 /** 106 * Keeps track of time taken by feature to execute. 107 * 108 * @var array list of feature timings 109 */ 110 protected static $timings = array(); 111 112 /** 113 * Keeps track of current running suite name. 114 * 115 * @var string current running suite name 116 */ 117 protected static $runningsuite = ''; 118 119 /** 120 * @var array Array (with tag names in keys) of all tags in current scenario. 121 */ 122 protected static $scenariotags; 123 124 /** 125 * Gives access to moodle codebase, ensures all is ready and sets up the test lock. 126 * 127 * Includes config.php to use moodle codebase with $CFG->behat_* instead of $CFG->prefix and $CFG->dataroot, called 128 * once per suite. 129 * 130 * @BeforeSuite 131 * @param BeforeSuiteScope $scope scope passed by event fired before suite. 132 */ 133 public static function before_suite_hook(BeforeSuiteScope $scope) { 134 global $CFG; 135 136 // If behat has been initialised then no need to do this again. 137 if (!self::is_first_scenario()) { 138 return; 139 } 140 141 // Defined only when the behat CLI command is running, the moodle init setup process will 142 // read this value and switch to $CFG->behat_dataroot and $CFG->behat_prefix instead of 143 // the normal site. 144 if (!defined('BEHAT_TEST')) { 145 define('BEHAT_TEST', 1); 146 } 147 148 if (!defined('CLI_SCRIPT')) { 149 define('CLI_SCRIPT', 1); 150 } 151 152 // With BEHAT_TEST we will be using $CFG->behat_* instead of $CFG->dataroot, $CFG->prefix and $CFG->wwwroot. 153 require_once(__DIR__ . '/../../../config.php'); 154 155 // Now that we are MOODLE_INTERNAL. 156 require_once (__DIR__ . '/../../behat/classes/behat_command.php'); 157 require_once (__DIR__ . '/../../behat/classes/behat_selectors.php'); 158 require_once (__DIR__ . '/../../behat/classes/behat_context_helper.php'); 159 require_once (__DIR__ . '/../../behat/classes/util.php'); 160 require_once (__DIR__ . '/../../testing/classes/test_lock.php'); 161 require_once (__DIR__ . '/../../testing/classes/nasty_strings.php'); 162 163 // Avoids vendor/bin/behat to be executed directly without test environment enabled 164 // to prevent undesired db & dataroot modifications, this is also checked 165 // before each scenario (accidental user deletes) in the BeforeScenario hook. 166 167 if (!behat_util::is_test_mode_enabled()) { 168 self::log_and_stop('Behat only can run if test mode is enabled. More info in ' . behat_command::DOCS_URL); 169 } 170 171 // Reset all data, before checking for check_server_status. 172 // If not done, then it can return apache error, while running tests. 173 behat_util::clean_tables_updated_by_scenario_list(); 174 behat_util::reset_all_data(); 175 176 // Check if the web server is running and using same version for cli and apache. 177 behat_util::check_server_status(); 178 179 // Prevents using outdated data, upgrade script would start and tests would fail. 180 if (!behat_util::is_test_data_updated()) { 181 $commandpath = 'php admin/tool/behat/cli/init.php'; 182 $message = <<<EOF 183 Your behat test site is outdated, please run the following command from your Moodle dirroot to drop, and reinstall the Behat test site. 184 185 {$commandpath} 186 187 EOF; 188 self::log_and_stop($message); 189 } 190 191 // Avoid parallel tests execution, it continues when the previous lock is released. 192 test_lock::acquire('behat'); 193 194 if (!empty($CFG->behat_faildump_path) && !is_writable($CFG->behat_faildump_path)) { 195 self::log_and_stop( 196 "The \$CFG->behat_faildump_path value is set to a non-writable directory ({$CFG->behat_faildump_path})." 197 ); 198 } 199 200 // Handle interrupts on PHP7. 201 if (extension_loaded('pcntl')) { 202 $disabled = explode(',', ini_get('disable_functions')); 203 if (!in_array('pcntl_signal', $disabled)) { 204 declare(ticks = 1); 205 } 206 } 207 } 208 209 /** 210 * Run final tests before running the suite. 211 * 212 * @BeforeSuite 213 * @param BeforeSuiteScope $scope scope passed by event fired before suite. 214 */ 215 public static function before_suite_final_checks(BeforeSuiteScope $scope) { 216 $happy = defined('BEHAT_TEST'); 217 $happy = $happy && defined('BEHAT_SITE_RUNNING'); 218 $happy = $happy && php_sapi_name() == 'cli'; 219 $happy = $happy && behat_util::is_test_mode_enabled(); 220 $happy = $happy && behat_util::is_test_site(); 221 222 if (!$happy) { 223 error_log('Behat only can modify the test database and the test dataroot!'); 224 exit(1); 225 } 226 } 227 228 /** 229 * Gives access to moodle codebase, to keep track of feature start time. 230 * 231 * @param BeforeFeatureScope $scope scope passed by event fired before feature. 232 * @BeforeFeature 233 */ 234 public static function before_feature(BeforeFeatureScope $scope) { 235 if (!defined('BEHAT_FEATURE_TIMING_FILE')) { 236 return; 237 } 238 $file = $scope->getFeature()->getFile(); 239 self::$timings[$file] = microtime(true); 240 } 241 242 /** 243 * Gives access to moodle codebase, to keep track of feature end time. 244 * 245 * @param AfterFeatureScope $scope scope passed by event fired after feature. 246 * @AfterFeature 247 */ 248 public static function after_feature(AfterFeatureScope $scope) { 249 if (!defined('BEHAT_FEATURE_TIMING_FILE')) { 250 return; 251 } 252 $file = $scope->getFeature()->getFile(); 253 self::$timings[$file] = microtime(true) - self::$timings[$file]; 254 // Probably didn't actually run this, don't output it. 255 if (self::$timings[$file] < 1) { 256 unset(self::$timings[$file]); 257 } 258 } 259 260 /** 261 * Gives access to moodle codebase, to keep track of suite timings. 262 * 263 * @param AfterSuiteScope $scope scope passed by event fired after suite. 264 * @AfterSuite 265 */ 266 public static function after_suite(AfterSuiteScope $scope) { 267 if (!defined('BEHAT_FEATURE_TIMING_FILE')) { 268 return; 269 } 270 $realroot = realpath(__DIR__.'/../../../').'/'; 271 foreach (self::$timings as $k => $v) { 272 $new = str_replace($realroot, '', $k); 273 self::$timings[$new] = round($v, 1); 274 unset(self::$timings[$k]); 275 } 276 if ($existing = @json_decode(file_get_contents(BEHAT_FEATURE_TIMING_FILE), true)) { 277 self::$timings = array_merge($existing, self::$timings); 278 } 279 arsort(self::$timings); 280 @file_put_contents(BEHAT_FEATURE_TIMING_FILE, json_encode(self::$timings, JSON_PRETTY_PRINT)); 281 } 282 283 /** 284 * Helper function to restart the Mink session. 285 */ 286 protected function restart_session(): void { 287 $session = $this->getSession(); 288 if ($session->isStarted()) { 289 $session->restart(); 290 } else { 291 $this->start_session(); 292 } 293 if ($this->running_javascript() && $this->getSession()->getDriver()->getWebDriverSessionId() === 'session') { 294 throw new DriverException('Unable to create a valid session'); 295 } 296 } 297 298 /** 299 * Start the Session, applying any initial configuratino required. 300 */ 301 protected function start_session(): void { 302 $this->getSession()->start(); 303 304 $this->set_test_timeout_factor(1); 305 } 306 307 /** 308 * Restart the session before each non-javascript scenario. 309 * 310 * @BeforeScenario @~javascript 311 * @param BeforeScenarioScope $scope scope passed by event fired before scenario. 312 */ 313 public function before_goutte_scenarios(BeforeScenarioScope $scope) { 314 if ($this->running_javascript()) { 315 // A bug in the BeforeScenario filtering prevents the @~javascript filter on this hook from working 316 // properly. 317 // See https://github.com/Behat/Behat/issues/1235 for further information. 318 return; 319 } 320 321 $this->restart_session(); 322 } 323 324 /** 325 * Start the session before the first javascript scenario. 326 * 327 * This is treated slightly differently to try to capture when Selenium is not running at all. 328 * 329 * @BeforeScenario @javascript 330 * @param BeforeScenarioScope $scope scope passed by event fired before scenario. 331 */ 332 public function before_first_scenario_start_session(BeforeScenarioScope $scope) { 333 if (!self::is_first_javascript_scenario()) { 334 // The first Scenario has started. 335 // The `before_subsequent_scenario_start_session` function will restart the session instead. 336 return; 337 } 338 339 $docsurl = behat_command::DOCS_URL; 340 $driverexceptionmsg = <<<EOF 341 342 The Selenium or WebDriver server is not running. You must start it to run tests that involve Javascript. 343 See {$docsurl} for more information. 344 345 The following debugging information is available: 346 347 EOF; 348 349 try { 350 $this->restart_session(); 351 } catch (WebDriverCurlException | DriverException $e) { 352 // Thrown by WebDriver. 353 self::log_and_stop( 354 $driverexceptionmsg . '. ' . 355 $e->getMessage() . "\n\n" . 356 format_backtrace($e->getTrace(), true) 357 ); 358 } catch (UnknownErrorException $e) { 359 // Generic 'I have no idea' Selenium error. Custom exception to provide more feedback about possible solutions. 360 self::log_and_stop( 361 $e->getMessage() . "\n\n" . 362 format_backtrace($e->getTrace(), true) 363 ); 364 } 365 } 366 367 /** 368 * Start the session before each javascript scenario. 369 * 370 * Note: Before the first scenario the @see before_first_scenario_start_session() function is used instead. 371 * 372 * @BeforeScenario @javascript 373 * @param BeforeScenarioScope $scope scope passed by event fired before scenario. 374 */ 375 public function before_subsequent_scenario_start_session(BeforeScenarioScope $scope) { 376 if (self::is_first_javascript_scenario()) { 377 // The initial init has not yet finished. 378 // The `before_first_scenario_start_session` function will have started the session instead. 379 return; 380 } 381 self::$currentscenarioexception = null; 382 383 try { 384 $this->restart_session(); 385 } catch (Exception $e) { 386 self::$currentscenarioexception = $e; 387 } 388 } 389 390 /** 391 * Resets the test environment. 392 * 393 * @BeforeScenario 394 * @param BeforeScenarioScope $scope scope passed by event fired before scenario. 395 */ 396 public function before_scenario_hook(BeforeScenarioScope $scope) { 397 global $DB; 398 if (self::$currentscenarioexception) { 399 // A BeforeScenario hook triggered an exception and marked this test as failed. 400 // Skip this hook as it will likely fail. 401 return; 402 } 403 404 $suitename = $scope->getSuite()->getName(); 405 406 // Register behat selectors for theme, if suite is changed. We do it for every suite change. 407 if ($suitename !== self::$runningsuite) { 408 self::$runningsuite = $suitename; 409 behat_context_helper::set_environment($scope->getEnvironment()); 410 411 // We need the Mink session to do it and we do it only before the first scenario. 412 $namedpartialclass = 'behat_partial_named_selector'; 413 $namedexactclass = 'behat_exact_named_selector'; 414 415 // If override selector exist, then set it as default behat selectors class. 416 $overrideclass = behat_config_util::get_behat_theme_selector_override_classname($suitename, 'named_partial', true); 417 if (class_exists($overrideclass)) { 418 $namedpartialclass = $overrideclass; 419 } 420 421 // If override selector exist, then set it as default behat selectors class. 422 $overrideclass = behat_config_util::get_behat_theme_selector_override_classname($suitename, 'named_exact', true); 423 if (class_exists($overrideclass)) { 424 $namedexactclass = $overrideclass; 425 } 426 427 $this->getSession()->getSelectorsHandler()->registerSelector('named_partial', new $namedpartialclass()); 428 $this->getSession()->getSelectorsHandler()->registerSelector('named_exact', new $namedexactclass()); 429 430 // Register component named selectors. 431 foreach (\core_component::get_component_names() as $component) { 432 $this->register_component_selectors_for_component($component); 433 } 434 435 } 436 437 // Reset $SESSION. 438 \core\session\manager::init_empty_session(); 439 440 // Ignore E_NOTICE and E_WARNING during reset, as this might be caused because of some existing process 441 // running ajax. This will be investigated in another issue. 442 $errorlevel = error_reporting(); 443 error_reporting($errorlevel & ~E_NOTICE & ~E_WARNING); 444 behat_util::reset_all_data(); 445 error_reporting($errorlevel); 446 447 if ($this->running_javascript()) { 448 // Fetch the user agent. 449 // This isused to choose between the SVG/Non-SVG versions of themes. 450 $useragent = $this->getSession()->evaluateScript('return navigator.userAgent;'); 451 \core_useragent::instance(true, $useragent); 452 453 // Restore the saved themes. 454 behat_util::restore_saved_themes(); 455 } 456 457 // Assign valid data to admin user (some generator-related code needs a valid user). 458 $user = $DB->get_record('user', array('username' => 'admin')); 459 \core\session\manager::set_user($user); 460 461 // Set the theme if not default. 462 if ($suitename !== "default") { 463 set_config('theme', $suitename); 464 } 465 466 // Reset the scenariorunning variable to ensure that Step 0 occurs. 467 $this->scenariorunning = false; 468 469 // Set up the tags for current scenario. 470 self::fetch_tags_for_scenario($scope); 471 472 // If scenario requires the Moodle app to be running, set this up. 473 if ($this->has_tag('app')) { 474 $this->execute('behat_app::start_scenario'); 475 476 return; 477 } 478 479 // Run all test with medium (1024x768) screen size, to avoid responsive problems. 480 $this->resize_window('medium'); 481 } 482 483 /** 484 * Mark the first Javascript Scenario as have been seen. 485 * 486 * @BeforeScenario 487 * @param BeforeScenarioScope $scope scope passed by event fired before scenario. 488 */ 489 public function mark_first_js_scenario_as_seen(BeforeScenarioScope $scope) { 490 self::$firstjavascriptscenarioseen = true; 491 } 492 493 /** 494 * Hook to open the site root before the first step in the suite. 495 * Yes, this is in a strange location and should be in the BeforeScenario hook, but failures in the test setUp lead 496 * to the test being incorrectly marked as skipped with no way to force the test to be failed. 497 * 498 * @param BeforeStepScope $scope 499 * @BeforeStep 500 */ 501 public function before_step(BeforeStepScope $scope) { 502 global $CFG; 503 504 if (!$this->scenariorunning) { 505 // We need to visit / before the first step in any Scenario. 506 // This is our Step 0. 507 // Ideally this would be in the BeforeScenario hook, but any exception in there will lead to the test being 508 // skipped rather than it being failed. 509 // 510 // We also need to check that the site returned is a Behat site. 511 // Again, this would be better in the BeforeSuite hook, but that does not have access to the selectors in 512 // order to perform the necessary searches. 513 $session = $this->getSession(); 514 $this->execute('behat_general::i_visit', ['/']); 515 516 // Checking that the root path is a Moodle test site. 517 if (self::is_first_scenario()) { 518 $message = "The base URL ({$CFG->wwwroot}) is not a behat test site. " . 519 'Ensure that you started the built-in web server in the correct directory, ' . 520 'or that your web server is correctly set up and started.'; 521 522 $this->find( 523 "xpath", "//head/child::title[normalize-space(.)='" . behat_util::BEHATSITENAME . "']", 524 new ExpectationException($message, $session) 525 ); 526 527 } 528 $this->scenariorunning = true; 529 } 530 } 531 532 /** 533 * Sets up the tags for the current scenario. 534 * 535 * @param \Behat\Behat\Hook\Scope\BeforeScenarioScope $scope Scope 536 */ 537 protected static function fetch_tags_for_scenario(\Behat\Behat\Hook\Scope\BeforeScenarioScope $scope) { 538 self::$scenariotags = array_flip(array_merge( 539 $scope->getScenario()->getTags(), 540 $scope->getFeature()->getTags() 541 )); 542 } 543 544 /** 545 * Gets the tags for the current scenario 546 * 547 * @return array Array where key is tag name and value is an integer 548 */ 549 public static function get_tags_for_scenario() : array { 550 return self::$scenariotags; 551 } 552 553 /** 554 * Wait for JS to complete before beginning interacting with the DOM. 555 * 556 * Executed only when running against a real browser. We wrap it 557 * all in a try & catch to forward the exception to i_look_for_exceptions 558 * so the exception will be at scenario level, which causes a failure, by 559 * default would be at framework level, which will stop the execution of 560 * the run. 561 * 562 * @param BeforeStepScope $scope scope passed by event fired before step. 563 * @BeforeStep 564 */ 565 public function before_step_javascript(BeforeStepScope $scope) { 566 if (self::$currentscenarioexception) { 567 // A BeforeScenario hook triggered an exception and marked this test as failed. 568 // Skip this hook as it will likely fail. 569 return; 570 } 571 572 self::$currentstepexception = null; 573 574 // Only run if JS. 575 if ($this->running_javascript()) { 576 try { 577 $this->wait_for_pending_js(); 578 } catch (Exception $e) { 579 self::$currentstepexception = $e; 580 } 581 } 582 } 583 584 /** 585 * Wait for JS to complete after finishing the step. 586 * 587 * With this we ensure that there are not AJAX calls 588 * still in progress. 589 * 590 * Executed only when running against a real browser. We wrap it 591 * all in a try & catch to forward the exception to i_look_for_exceptions 592 * so the exception will be at scenario level, which causes a failure, by 593 * default would be at framework level, which will stop the execution of 594 * the run. 595 * 596 * @param AfterStepScope $scope scope passed by event fired after step.. 597 * @AfterStep 598 */ 599 public function after_step_javascript(AfterStepScope $scope) { 600 global $CFG, $DB; 601 602 // If step is undefined then throw exception, to get failed exit code. 603 if ($scope->getTestResult()->getResultCode() === Behat\Behat\Tester\Result\StepResult::UNDEFINED) { 604 throw new coding_exception("Step '" . $scope->getStep()->getText() . "'' is undefined."); 605 } 606 607 $isfailed = $scope->getTestResult()->getResultCode() === Behat\Testwork\Tester\Result\TestResult::FAILED; 608 609 // Abort any open transactions to prevent subsequent tests hanging. 610 // This does the same as abort_all_db_transactions(), but doesn't call error_log() as we don't 611 // want to see a message in the behat output. 612 if (($scope->getTestResult() instanceof \Behat\Behat\Tester\Result\ExecutedStepResult) && 613 $scope->getTestResult()->hasException()) { 614 if ($DB && $DB->is_transaction_started()) { 615 $DB->force_transaction_rollback(); 616 } 617 } 618 619 if ($isfailed && !empty($CFG->behat_faildump_path)) { 620 // Save the page content (html). 621 $this->take_contentdump($scope); 622 623 if ($this->running_javascript()) { 624 // Save a screenshot. 625 $this->take_screenshot($scope); 626 } 627 } 628 629 if ($isfailed && !empty($CFG->behat_pause_on_fail)) { 630 $exception = $scope->getTestResult()->getException(); 631 $message = "<colour:lightRed>Scenario failed. "; 632 $message .= "<colour:lightYellow>Paused for inspection. Press <colour:lightRed>Enter/Return<colour:lightYellow> to continue.<newline>"; 633 $message .= "<colour:lightRed>Exception follows:<newline>"; 634 $message .= trim($exception->getMessage()); 635 behat_util::pause($this->getSession(), $message); 636 } 637 638 // Only run if JS. 639 if (!$this->running_javascript()) { 640 return; 641 } 642 643 try { 644 $this->wait_for_pending_js(); 645 self::$currentstepexception = null; 646 } catch (UnexpectedAlertOpenException $e) { 647 self::$currentstepexception = $e; 648 649 // Accepting the alert so the framework can continue properly running 650 // the following scenarios. Some browsers already closes the alert, so 651 // wrapping in a try & catch. 652 try { 653 $this->getSession()->getDriver()->getWebDriver()->switchTo()->alert()->accept(); 654 } catch (Exception $e) { 655 // Catching the generic one as we never know how drivers reacts here. 656 } 657 } catch (Exception $e) { 658 self::$currentstepexception = $e; 659 } 660 } 661 662 /** 663 * Reset the session between each scenario. 664 * 665 * @param AfterScenarioScope $scope scope passed by event fired after scenario. 666 * @AfterScenario 667 */ 668 public function reset_webdriver_between_scenarios(AfterScenarioScope $scope) { 669 try { 670 $this->getSession()->stop(); 671 } catch (Exception $e) { 672 $error = <<<EOF 673 674 Error while stopping WebDriver: %s (%d) '%s' 675 Attempting to continue with test run. Stacktrace follows: 676 677 %s 678 EOF; 679 error_log(sprintf( 680 $error, 681 get_class($e), 682 $e->getCode(), 683 $e->getMessage(), 684 format_backtrace($e->getTrace(), true) 685 )); 686 } 687 } 688 689 /** 690 * Getter for self::$faildumpdirname 691 * 692 * @return string 693 */ 694 protected function get_run_faildump_dir() { 695 return self::$faildumpdirname; 696 } 697 698 /** 699 * Take screenshot when a step fails. 700 * 701 * @throws Exception 702 * @param AfterStepScope $scope scope passed by event after step. 703 */ 704 protected function take_screenshot(AfterStepScope $scope) { 705 // Goutte can't save screenshots. 706 if (!$this->running_javascript()) { 707 return false; 708 } 709 710 // Some drivers (e.g. chromedriver) may throw an exception while trying to take a screenshot. If this isn't handled, 711 // the behat run dies. We don't want to lose the information about the failure that triggered the screenshot, 712 // so let's log the exception message to a file (to explain why there's no screenshot) and allow the run to continue, 713 // handling the failure as normal. 714 try { 715 list ($dir, $filename) = $this->get_faildump_filename($scope, 'png'); 716 $this->saveScreenshot($filename, $dir); 717 } catch (Exception $e) { 718 // Catching all exceptions as we don't know what the driver might throw. 719 list ($dir, $filename) = $this->get_faildump_filename($scope, 'txt'); 720 $message = "Could not save screenshot due to an error\n" . $e->getMessage(); 721 file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $message); 722 } 723 } 724 725 /** 726 * Take a dump of the page content when a step fails. 727 * 728 * @throws Exception 729 * @param AfterStepScope $scope scope passed by event after step. 730 */ 731 protected function take_contentdump(AfterStepScope $scope) { 732 list ($dir, $filename) = $this->get_faildump_filename($scope, 'html'); 733 734 try { 735 // Driver may throw an exception during getContent(), so do it first to avoid getting an empty file. 736 $content = $this->getSession()->getPage()->getContent(); 737 } catch (Exception $e) { 738 // Catching all exceptions as we don't know what the driver might throw. 739 $content = "Could not save contentdump due to an error\n" . $e->getMessage(); 740 } 741 file_put_contents($dir . DIRECTORY_SEPARATOR . $filename, $content); 742 } 743 744 /** 745 * Determine the full pathname to store a failure-related dump. 746 * 747 * This is used for content such as the DOM, and screenshots. 748 * 749 * @param AfterStepScope $scope scope passed by event after step. 750 * @param String $filetype The file suffix to use. Limited to 4 chars. 751 */ 752 protected function get_faildump_filename(AfterStepScope $scope, $filetype) { 753 global $CFG; 754 755 // All the contentdumps should be in the same parent dir. 756 if (!$faildumpdir = self::get_run_faildump_dir()) { 757 $faildumpdir = self::$faildumpdirname = date('Ymd_His'); 758 759 $dir = $CFG->behat_faildump_path . DIRECTORY_SEPARATOR . $faildumpdir; 760 761 if (!is_dir($dir) && !mkdir($dir, $CFG->directorypermissions, true)) { 762 // It shouldn't, we already checked that the directory is writable. 763 throw new Exception('No directories can be created inside $CFG->behat_faildump_path, check the directory permissions.'); 764 } 765 } else { 766 // We will always need to know the full path. 767 $dir = $CFG->behat_faildump_path . DIRECTORY_SEPARATOR . $faildumpdir; 768 } 769 770 // The scenario title + the failed step text. 771 // We want a i-am-the-scenario-title_i-am-the-failed-step.$filetype format. 772 $filename = $scope->getFeature()->getTitle() . '_' . $scope->getStep()->getText(); 773 774 // As file name is limited to 255 characters. Leaving 5 chars for line number and 4 chars for the file. 775 // extension as we allow .png for images and .html for DOM contents. 776 $filenamelen = 245; 777 778 // Suffix suite name to faildump file, if it's not default suite. 779 $suitename = $scope->getSuite()->getName(); 780 if ($suitename != 'default') { 781 $suitename = '_' . $suitename; 782 $filenamelen = $filenamelen - strlen($suitename); 783 } else { 784 // No need to append suite name for default. 785 $suitename = ''; 786 } 787 788 $filename = preg_replace('/([^a-zA-Z0-9\_]+)/', '-', $filename); 789 $filename = substr($filename, 0, $filenamelen) . $suitename . '_' . $scope->getStep()->getLine() . '.' . $filetype; 790 791 return array($dir, $filename); 792 } 793 794 /** 795 * Internal step definition to find exceptions, debugging() messages and PHP debug messages. 796 * 797 * Part of behat_hooks class as is part of the testing framework, is auto-executed 798 * after each step so no features will splicitly use it. 799 * 800 * @Given /^I look for exceptions$/ 801 * @throw Exception Unknown type, depending on what we caught in the hook or basic \Exception. 802 * @see Moodle\BehatExtension\EventDispatcher\Tester\ChainedStepTester 803 */ 804 public function i_look_for_exceptions() { 805 // If the scenario already failed in a hook throw the exception. 806 if (!is_null(self::$currentscenarioexception)) { 807 throw self::$currentscenarioexception; 808 } 809 810 // If the step already failed in a hook throw the exception. 811 if (!is_null(self::$currentstepexception)) { 812 throw self::$currentstepexception; 813 } 814 815 $this->look_for_exceptions(); 816 } 817 818 /** 819 * Returns whether the first scenario of the suite is running 820 * 821 * @return bool 822 */ 823 protected static function is_first_scenario() { 824 return !(self::$initprocessesfinished); 825 } 826 827 /** 828 * Returns whether the first scenario of the suite is running 829 * 830 * @return bool 831 */ 832 protected static function is_first_javascript_scenario(): bool { 833 return !self::$firstjavascriptscenarioseen; 834 } 835 836 /** 837 * Register a set of component selectors. 838 * 839 * @param string $component 840 */ 841 public function register_component_selectors_for_component(string $component): void { 842 $context = behat_context_helper::get_component_context($component); 843 844 if ($context === null) { 845 return; 846 } 847 848 $namedpartial = $this->getSession()->getSelectorsHandler()->getSelector('named_partial'); 849 $namedexact = $this->getSession()->getSelectorsHandler()->getSelector('named_exact'); 850 851 // Replacements must come before selectors as they are used in the selectors. 852 foreach ($context->get_named_replacements() as $replacement) { 853 $namedpartial->register_replacement($component, $replacement); 854 $namedexact->register_replacement($component, $replacement); 855 } 856 857 foreach ($context->get_partial_named_selectors() as $selector) { 858 $namedpartial->register_component_selector($component, $selector); 859 } 860 861 foreach ($context->get_exact_named_selectors() as $selector) { 862 $namedexact->register_component_selector($component, $selector); 863 } 864 865 } 866 867 /** 868 * Mark the first step as having been completed. 869 * 870 * This must be the last BeforeStep hook in the setup. 871 * 872 * @param BeforeStepScope $scope 873 * @BeforeStep 874 */ 875 public function first_step_setup_complete(BeforeStepScope $scope): void { 876 self::$initprocessesfinished = true; 877 } 878 879 /** 880 * Log a notification, and then exit. 881 * 882 * @param string $message The content to dispaly 883 */ 884 protected static function log_and_stop(string $message): void { 885 error_log($message); 886 887 exit(1); 888 } 889 890 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body