Skip to content

Commit

Permalink
MDL-69232 behat: Simplify hooks
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
andrewnicols committed Jul 10, 2020
1 parent 90221b5 commit 3e741db
Showing 1 changed file with 124 additions and 90 deletions.
214 changes: 124 additions & 90 deletions lib/tests/behat/behat_hooks.php
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -170,29 +156,36 @@ public static function before_suite(BeforeSuiteScope $scope) {
// 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.
// If not done, then it can return apache error, while running tests.
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.
Expand Down Expand Up @@ -278,21 +271,6 @@ public static function after_suite(AfterSuiteScope $scope) {
@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.
*/
Expand All @@ -304,50 +282,98 @@ protected function restart_session(): void {
$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.
Expand Down Expand Up @@ -733,6 +759,15 @@ protected static function is_first_scenario() {
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.
*
Expand Down Expand Up @@ -772,20 +807,19 @@ public function register_component_selectors_for_component(string $component): v
* @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 {
}

0 comments on commit 3e741db

Please sign in to comment.