diff --git a/config-dist.php b/config-dist.php index cf3118a44d8..56b3803b2b4 100644 --- a/config-dist.php +++ b/config-dist.php @@ -847,6 +847,10 @@ $CFG->admin = 'admin'; // Example: // $CFG->behat_faildump_path = '/my/path/to/save/failure/dumps'; // +// You can make behat pause upon failure to help you diagnose and debug problems with your tests. +// +// $CFG->behat_pause_on_fail = true; +// // You can specify db, selenium wd_host etc. for behat parallel run by setting following variable. // Example: // $CFG->behat_parallel_run = array ( diff --git a/lib/behat/classes/util.php b/lib/behat/classes/util.php index 4f11771f00a..1988e015593 100644 --- a/lib/behat/classes/util.php +++ b/lib/behat/classes/util.php @@ -31,6 +31,9 @@ require_once(__DIR__ . '/behat_command.php'); require_once(__DIR__ . '/behat_config_manager.php'); require_once(__DIR__ . '/../../filelib.php'); +require_once(__DIR__ . '/../../clilib.php'); + +use Behat\Mink\Session; /** * Init/reset utilities for Behat database and dataroot @@ -374,4 +377,38 @@ class behat_util extends testing_util { // $CFG values from the old run. @see set_config. initialise_cfg(); } + + /** + * Pause execution immediately. + * + * @param Session $session + * @param string $message The message to show when pausing. + * This will be passed through cli_ansi_format so appropriate ANSI formatting and features are available. + */ + public static function pause(Session $session, string $message): void { + $posixexists = function_exists('posix_isatty'); + + // Make sure this step is only used with interactive terminal (if detected). + if ($posixexists && !@posix_isatty(STDOUT)) { + throw new ExpectationException('Break point should only be used with interactive terminal.', $session); + } + + // Save the cursor position, ring the bell, and add a new line. + fwrite(STDOUT, cli_ansi_format("")); + + // Output the formatted message and reset colour back to normal. + $formattedmessage = cli_ansi_format("{$message}"); + fwrite(STDOUT, $formattedmessage); + + // Wait for input. + fread(STDIN, 1024); + + // Move the cursor back up to the previous position, then restore the original position stored earlier, and move + // it back down again. + fwrite(STDOUT, cli_ansi_format("")); + + // Add any extra lines back if the provided message was spread over multiple lines. + $linecount = count(explode("\n", $formattedmessage)); + fwrite(STDOUT, str_repeat(cli_ansi_format(""), $linecount - 1)); + } } diff --git a/lib/tests/behat/behat_general.php b/lib/tests/behat/behat_general.php index 18bfe439365..19db96e1832 100644 --- a/lib/tests/behat/behat_general.php +++ b/lib/tests/behat/behat_general.php @@ -1636,26 +1636,8 @@ EOF; * @Then /^(?:|I )pause(?:| scenario execution)$/ */ public function i_pause_scenario_executon() { - global $CFG; - - $posixexists = function_exists('posix_isatty'); - - // Make sure this step is only used with interactive terminal (if detected). - if ($posixexists && !@posix_isatty(STDOUT)) { - $session = $this->getSession(); - throw new ExpectationException('Break point should only be used with interative terminal.', $session); - } - - // Windows don't support ANSI code by default, but with ANSICON. - $isansicon = getenv('ANSICON'); - if (($CFG->ostype === 'WINDOWS') && empty($isansicon)) { - fwrite(STDOUT, "Paused. Press Enter/Return to continue."); - fread(STDIN, 1024); - } else { - fwrite(STDOUT, "\033[s\n\033[0;93mPaused. Press \033[1;31mEnter/Return\033[0;93m to continue.\033[0m"); - fread(STDIN, 1024); - fwrite(STDOUT, "\033[2A\033[u\033[2B"); - } + $message = "Paused. Press Enter/Return to continue."; + behat_util::pause($this->getSession(), $message); } /** diff --git a/lib/tests/behat/behat_hooks.php b/lib/tests/behat/behat_hooks.php index 8cda5a7d7f8..46b3c87afe1 100644 --- a/lib/tests/behat/behat_hooks.php +++ b/lib/tests/behat/behat_hooks.php @@ -500,11 +500,7 @@ class behat_hooks extends behat_base { throw new coding_exception("Step '" . $scope->getStep()->getText() . "'' is undefined."); } - // Save the page content if the step failed. - if (!empty($CFG->behat_faildump_path) && - $scope->getTestResult()->getResultCode() === Behat\Testwork\Tester\Result\TestResult::FAILED) { - $this->take_contentdump($scope); - } + $isfailed = $scope->getTestResult()->getResultCode() === Behat\Testwork\Tester\Result\TestResult::FAILED; // Abort any open transactions to prevent subsequent tests hanging. // This does the same as abort_all_db_transactions(), but doesn't call error_log() as we don't @@ -516,17 +512,30 @@ class behat_hooks extends behat_base { } } + if ($isfailed && !empty($CFG->behat_faildump_path)) { + // Save the page content (html). + $this->take_contentdump($scope); + + if ($this->running_javascript()) { + // Save a screenshot. + $this->take_screenshot($scope); + } + } + + if ($isfailed && !empty($CFG->behat_pause_on_fail)) { + $exception = $scope->getTestResult()->getException(); + $message = "Scenario failed. "; + $message .= "Paused for inspection. Press Enter/Return to continue."; + $message .= "Exception follows:"; + $message .= trim($exception->getMessage()); + behat_util::pause($this->getSession(), $message); + } + // Only run if JS. if (!$this->running_javascript()) { return; } - // Save a screenshot if the step failed. - if (!empty($CFG->behat_faildump_path) && - $scope->getTestResult()->getResultCode() === Behat\Testwork\Tester\Result\TestResult::FAILED) { - $this->take_screenshot($scope); - } - try { $this->wait_for_pending_js(); self::$currentstepexception = null;