diff --git a/config-dist.php b/config-dist.php index d35ae8f9c80..87bd5049225 100644 --- a/config-dist.php +++ b/config-dist.php @@ -812,6 +812,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 6f668135a96..a87a17c313c 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 @@ -373,4 +376,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/clilib.php b/lib/clilib.php index ba243f1029e..c0b908e3921 100644 --- a/lib/clilib.php +++ b/lib/clilib.php @@ -227,3 +227,59 @@ function cli_logo($padding=2, $return=false) { cli_write($logo); } } + +/** + * Substitute cursor, colour, and bell placeholders in a CLI output to ANSI escape characters when ANSI is available. + * + * @param string $message + * @return string + */ +function cli_ansi_format(string $message): string { + global $CFG; + + $replacements = [ + "" => "\n", + "" => "\007", + + // Cursor movement: https://www.tldp.org/HOWTO/Bash-Prompt-HOWTO/x361.html. + "" => "\033[s", + "" => "\033[u", + "" => "\033[1A", + "" => "\033[1B", + "" => "\033[1C", + "" => "\033[1D", + ]; + + $colours = [ + 'normal' => '0;0', + 'black' => '0;30', + 'darkGray' => '1;30', + 'blue' => '0;34', + 'lightBlue' => '1;34', + 'green' => '0;32', + 'lightGreen' => '1;32', + 'cyan' => '0;36', + 'lightCyan' => '1;36', + 'red' => '0;31', + 'lightRed' => '1;31', + 'purple' => '0;35', + 'lightPurple' => '1;35', + 'brown' => '0;33', + 'yellow' => '1;33', + 'lightYellow' => '0;93', + 'lightGray' => '0;37', + 'white' => '1;37', + ]; + + foreach ($colours as $colour => $code) { + $replacements[""] = "\033[{$code}m"; + } + + // Windows don't support ANSI code by default, but does if ANSICON is available. + $isansicon = getenv('ANSICON'); + if (($CFG->ostype === 'WINDOWS') && empty($isansicon)) { + return str_replace(array_keys($replacements), '', $message); + } + + return str_replace(array_keys($replacements), array_values($replacements), $message); +} diff --git a/lib/tests/behat/behat_general.php b/lib/tests/behat/behat_general.php index 91c109c0921..4f42c3499da 100644 --- a/lib/tests/behat/behat_general.php +++ b/lib/tests/behat/behat_general.php @@ -1595,27 +1595,9 @@ 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"); - } + public function i_pause_scenario_execution() { + $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 dcc4f67b5af..0d0754e5c41 100644 --- a/lib/tests/behat/behat_hooks.php +++ b/lib/tests/behat/behat_hooks.php @@ -466,11 +466,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 @@ -482,17 +478,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;