Commit eec91baf authored by Andrew Nicols's avatar Andrew Nicols
Browse files

MDL-69232 behat: Simplify hooks

The behat hooks were needlessly complicated which made them much harder
to read, and understand, leading to bugs during development.

These have been significantly simplified to favour clarity over
overloading.
parent 775b9a36
......@@ -68,6 +68,9 @@ class behat_hooks extends behat_base {
*/
protected static $initprocessesfinished = false;
/** @var bool Whether the first javascript scenario has been seen yet */
protected static $firstjavascriptscenarioseen = false;
/**
* @var bool Scenario running
*/
......@@ -110,39 +113,22 @@ class behat_hooks extends behat_base {
protected static $scenariotags;
/**
* Hook to capture BeforeSuite event so as to give access to moodle codebase.
* This will try and catch any exception and exists if anything fails.
* Gives access to moodle codebase, ensures all is ready and sets up the test lock.
*
* Includes config.php to use moodle codebase with $CFG->behat_* instead of $CFG->prefix and $CFG->dataroot, called
* once per suite.
*
* @param BeforeSuiteScope $scope scope passed by event fired before suite.
* @BeforeSuite
* @param BeforeSuiteScope $scope scope passed by event fired before suite.
*/
public static function before_suite_hook(BeforeSuiteScope $scope) {
global $CFG;
// If behat has been initialised then no need to do this again.
if (self::$initprocessesfinished) {
if (!self::is_first_scenario()) {
return;
}
try {
self::before_suite($scope);
} catch (behat_stop_exception $e) {
echo $e->getMessage() . PHP_EOL;
exit(1);
}
}
/**
* Gives access to moodle codebase, ensures all is ready and sets up the test lock.
*
* Includes config.php to use moodle codebase with $CFG->behat_*
* instead of $CFG->prefix and $CFG->dataroot, called once per suite.
*
* @param BeforeSuiteScope $scope scope passed by event fired before suite.
* @static
* @throws behat_stop_exception
*/
public static function before_suite(BeforeSuiteScope $scope) {
global $CFG;
// Defined only when the behat CLI command is running, the moodle init setup process will
// read this value and switch to $CFG->behat_dataroot and $CFG->behat_prefix instead of
// the normal site.
......@@ -170,8 +156,7 @@ class behat_hooks extends behat_base {
// before each scenario (accidental user deletes) in the BeforeScenario hook.
if (!behat_util::is_test_mode_enabled()) {
throw new behat_stop_exception('Behat only can run if test mode is enabled. More info in ' .
behat_command::DOCS_URL);
self::log_and_stop('Behat only can run if test mode is enabled. More info in ' . behat_command::DOCS_URL);
}
// Reset all data, before checking for check_server_status.
......@@ -179,20 +164,28 @@ class behat_hooks extends behat_base {
behat_util::clean_tables_updated_by_scenario_list();
behat_util::reset_all_data();
// Check if server is running and using same version for cli and apache.
// Check if the web server is running and using same version for cli and apache.
behat_util::check_server_status();
// Prevents using outdated data, upgrade script would start and tests would fail.
if (!behat_util::is_test_data_updated()) {
$commandpath = 'php admin/tool/behat/cli/init.php';
throw new behat_stop_exception("Your behat test site is outdated, please run\n\n " .
$commandpath . "\n\nfrom your moodle dirroot to drop and install the behat test site again.");
$message = <<<EOF
Your behat test site is outdated, please run the following command from your Moodle dirroot to drop, and reinstall the Behat test site.
{$comandpath}
EOF;
self::log_and_stop($message);
}
// Avoid parallel tests execution, it continues when the previous lock is released.
test_lock::acquire('behat');
if (!empty($CFG->behat_faildump_path) && !is_writable($CFG->behat_faildump_path)) {
throw new behat_stop_exception('You set $CFG->behat_faildump_path to a non-writable directory');
self::log_and_stop(
"The \$CFG->behat_faildump_path value is set to a non-writable directory ({$CFG->behat_faildump_path})."
);
}
// Handle interrupts on PHP7.
......@@ -278,21 +271,6 @@ class behat_hooks extends behat_base {
@file_put_contents(BEHAT_FEATURE_TIMING_FILE, json_encode(self::$timings, JSON_PRETTY_PRINT));
}
/**
* Hook to capture before scenario event to get scope.
*
* @param BeforeScenarioScope $scope scope passed by event fired before scenario.
* @BeforeScenario
*/
public function before_scenario_hook(BeforeScenarioScope $scope) {
try {
$this->before_scenario($scope);
} catch (behat_stop_exception $e) {
echo $e->getMessage() . PHP_EOL;
exit(1);
}
}
/**
* Helper function to restart the Mink session.
*/
......@@ -304,50 +282,98 @@ class behat_hooks extends behat_base {
$session->start();
}
if ($this->running_javascript() && $this->getSession()->getDriver()->getWebDriverSessionId() === 'session') {
throw new DriverException('Unable to create valid session');
throw new DriverException('Unable to create a valid session');
}
}
/**
* Resets the test environment.
* Restart the session before each non-javascript scenario.
*
* @BeforeScenario @~javascript
* @param BeforeScenarioScope $scope scope passed by event fired before scenario.
*/
public function before_goutte_scenarios(BeforeScenarioScope $scope) {
if ($this->running_javascript()) {
// A bug in the BeforeScenario filtering prevents the @~javascript filter on this hook from working
// properly.
// See https://github.com/Behat/Behat/issues/1235 for further information.
return;
}
$this->restart_session();
}
/**
* Start the session before the first javascript scenario.
*
* This is treated slightly differently to try to capture when Selenium is not running at all.
*
* @BeforeScenario @javascript
* @param BeforeScenarioScope $scope scope passed by event fired before scenario.
* @throws behat_stop_exception If here we are not using the test database it should be because of a coding error
*/
public function before_scenario(BeforeScenarioScope $scope) {
global $DB, $CFG;
public function before_first_scenario_start_session(BeforeScenarioScope $scope) {
if (!self::is_first_javascript_scenario()) {
// The first Scenario has started.
// The `before_subsequent_scenario_start_session` function will restart the session instead.
return;
}
self::$firstjavascriptscenarioseen = true;
$docsurl = behat_command::DOCS_URL;
$driverexceptionmsg = <<<EOF
The Selenium or WebDriver server is not running. You must start it to run tests that involve Javascript.
See {$docsurl} for more information.
The following debugging information is available:
EOF;
if (self::$initprocessesfinished) {
try {
$this->restart_session();
} else {
$moreinfo = 'More info in ' . behat_command::DOCS_URL;
$driverexceptionmsg = 'Selenium server is not running, you need to start it to run tests that involve Javascript. ' . $moreinfo;
} catch (CurlExec | DriverException $e) {
// The CurlExec Exception is thrown by WebDriver.
self::log_and_stop(
$driverexceptionmsg . '. ' .
$e->getMessage() . "\n\n" .
format_backtrace($e->getTrace(), true)
);
} catch (UnknownError $e) {
// Generic 'I have no idea' Selenium error. Custom exception to provide more feedback about possible solutions.
self::log_and_stop(
$e->getMessage() . "\n\n" .
format_backtrace($e->getTrace(), true)
);
}
}
try {
$this->restart_session();
} catch (CurlExec $e) {
// Exception thrown by WebDriver, so only @javascript tests will be caugth; in
// behat_util::check_server_status() we already checked that the server is running.
throw new behat_stop_exception(
$driverexceptionmsg . '. ' .
$e->getMessage() . "\n\n" .
format_backtrace($e->getTrace(), true)
);
} catch (DriverException $e) {
throw new behat_stop_exception(
$driverexceptionmsg . '. ' .
$e->getMessage() . "\n\n" .
format_backtrace($e->getTrace(), true)
);
} catch (UnknownError $e) {
// Generic 'I have no idea' Selenium error. Custom exception to provide more feedback about possible solutions.
throw new behat_stop_exception(
$e->getMessage() . "\n\n" .
format_backtrace($e->getTrace(), true)
);
}
/**
* Start the session before each javascript scenario.
*
* Note: Before the first scenario the @see before_first_scenario_start_session() function is used instead.
*
* @BeforeScenario @javascript
* @param BeforeScenarioScope $scope scope passed by event fired before scenario.
*/
public function before_subsequent_scenario_start_session(BeforeScenarioScope $scope) {
if (self::is_first_javascript_scenario()) {
// The initial init has not yet finished.
// The `before_first_scenario_start_session` function will have started the session instead.
return;
}
$this->restart_session();
}
/**
* Resets the test environment.
*
* @BeforeScenario
* @param BeforeScenarioScope $scope scope passed by event fired before scenario.
*/
public function before_scenario_hook(BeforeScenarioScope $scope) {
global $DB;
$suitename = $scope->getSuite()->getName();
// Register behat selectors for theme, if suite is changed. We do it for every suite change.
......@@ -733,6 +759,15 @@ class behat_hooks extends behat_base {
return !(self::$initprocessesfinished);
}
/**
* Returns whether the first scenario of the suite is running
*
* @return bool
*/
protected static function is_first_javascript_scenario(): bool {
return !self::$firstjavascriptscenarioseen;
}
/**
* Register a set of component selectors.
*
......@@ -772,20 +807,19 @@ class behat_hooks extends behat_base {
* @param BeforeStepScope $scope
* @BeforeStep
*/
public function first_step_setup_complete(BeforeStepScope $scope) {
public function first_step_setup_complete(BeforeStepScope $scope): void {
self::$initprocessesfinished = true;
}
}
/**
* Log a notification, and then exit.
*
* @param string $message The content to dispaly
*/
protected static function log_and_stop(string $message): void {
error_log($message);
exit(1);
}
/**
* Behat stop exception
*
* This exception is thrown from before suite or scenario if any setup problem found.
*
* @package core_test
* @copyright 2016 Rajesh Taneja <rajesh@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_stop_exception extends \Exception {
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment