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