diff --git a/admin/tool/behat/tests/behat/frozen_clock.feature b/admin/tool/behat/tests/behat/frozen_clock.feature new file mode 100644 index 00000000000..635f838b116 --- /dev/null +++ b/admin/tool/behat/tests/behat/frozen_clock.feature @@ -0,0 +1,61 @@ +@tool @tool_behat +Feature: Frozen clock in Behat + In order to write tests that depend on the current system time + As a test writer + I need to set the time using a Behat step + + Background: + Given the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + And the following "activities" exist: + | activity | course | name | idnumber | externalurl | + | url | C1 | Fixture | url1 | #wwwroot#/admin/tool/behat/tests/fixtures/core/showtime.php | + | forum | C1 | TestForum | forum1 | | + + Scenario: Time has been frozen + # Set up 2 forum discussions at different times. This tests the clock in the Behat CLI process. + Given the time is frozen at "2024-03-01 12:34:56" + And the following "mod_forum > discussions" exist: + | user | forum | name | message | + | admin | forum1 | Subject1 | Message1 | + And the time is frozen at "2024-08-01 12:34:56" + And the following "mod_forum > discussions" exist: + | user | forum | name | message | + | admin | forum1 | Subject2 | Message2 | + When I am on the "TestForum" "forum activity" page logged in as admin + Then I should see "1 Mar 2024" in the "Subject1" "table_row" + And I should see "1 Aug 2024" in the "Subject2" "table_row" + # Also view time on the fixture page. This tests the clock for Behat web server requests. + And I am on the "Fixture" "url activity" page + And I should see "Behat time is not the same as real time" + # This Unix time corresponds to 12:34:56 in Perth time zone. + And I should see "Unix time 1722486896" + And I should see "Date-time 2024-08-01 12:34:56" + + # This scenario is second, to verify that the clock automatically goes back to normal after test. + Scenario: Time is normal + Given the following "mod_forum > discussions" exist: + | user | forum | name | message | + | admin | forum1 | Subject1 | Message1 | + When I am on the "TestForum" "forum activity" page logged in as admin + # The time should be the real current time, not the frozen time. + Then I should see "## today ##%d %b %Y##" in the "Subject1" "table_row" + And I am on the "Fixture" "url activity" page + And I should see "Behat time is the same as real time" + + Scenario: Time is frozen and then unfrozen + Given the time is frozen at "2024-03-01 12:34:56" + And the following "mod_forum > discussions" exist: + | user | forum | name | message | + | admin | forum1 | Subject1 | Message1 | + And the time is no longer frozen + And the following "mod_forum > discussions" exist: + | user | forum | name | message | + | admin | forum1 | Subject2 | Message2 | + When I am on the "TestForum" "forum activity" page logged in as admin + Then I should see "1 Mar 2024" in the "Subject1" "table_row" + # The time should be the real current time, not the frozen time for this entry. + Then I should see "## today ##%d %b %Y##" in the "Subject2" "table_row" + And I am on the "Fixture" "url activity" page + And I should see "Behat time is the same as real time" diff --git a/admin/tool/behat/tests/fixtures/core/showtime.php b/admin/tool/behat/tests/fixtures/core/showtime.php new file mode 100644 index 00000000000..6cc27b74eb3 --- /dev/null +++ b/admin/tool/behat/tests/fixtures/core/showtime.php @@ -0,0 +1,52 @@ +. + +/** + * Fixture to show the current server time using \core\clock. + * + * @package tool_behat + * @copyright 2024 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// phpcs:disable moodle.Files.RequireLogin.Missing +require(__DIR__ . '/../../../../../../config.php'); + +defined('BEHAT_SITE_RUNNING') || die('Behat fixture'); + +$PAGE->set_context(\context_system::instance()); +$PAGE->set_url(new \moodle_url('/admin/tool/behat/tests/fixtures/core/showtime.php')); + +echo $OUTPUT->header(); + +$clock = \core\di::get(\core\clock::class); +$dt = $clock->now(); +$realbefore = time(); +$time = $clock->time(); +$realafter = time(); + +echo html_writer::div('Unix time ' . $time); +echo html_writer::div('Date-time ' . $dt->format('Y-m-d H:i:s')); + +echo html_writer::div('TZ ' . $dt->getTimezone()->getName()); + +if ($time >= $realbefore && $time <= $realafter) { + echo html_writer::div('Behat time is the same as real time'); +} else { + echo html_writer::div('Behat time is not the same as real time'); +} + +echo $OUTPUT->footer(); diff --git a/lib/classes/di.php b/lib/classes/di.php index 2be5626f824..97404a42bef 100644 --- a/lib/classes/di.php +++ b/lib/classes/di.php @@ -120,7 +120,16 @@ class di { // The Moodle Clock implementation, which itself is an extension of PSR-20. // Alias the PSR-20 clock interface to the Moodle clock. They are compatible. - \core\clock::class => fn() => new \core\system_clock(), + \core\clock::class => function () { + global $CFG; + + // Web requests to the Behat site can use a frozen clock if configured. + if (defined('BEHAT_SITE_RUNNING') && !empty($CFG->behat_frozen_clock)) { + require_once($CFG->libdir . '/testing/classes/frozen_clock.php'); + return new \frozen_clock((int)$CFG->behat_frozen_clock); + } + return new \core\system_clock(); + }, \Psr\Clock\ClockInterface::class => \DI\get(\core\clock::class), // Note: libphonenumber PhoneNumberUtil uses a singleton. diff --git a/lib/testing/classes/frozen_clock.php b/lib/testing/classes/frozen_clock.php index 2610a425878..15e532ae3fd 100644 --- a/lib/testing/classes/frozen_clock.php +++ b/lib/testing/classes/frozen_clock.php @@ -35,7 +35,10 @@ class frozen_clock implements \core\clock { ?int $time = null, ) { if ($time) { - $this->time = new \DateTimeImmutable("@{$time}"); + // Note that the constructor with time zone does not work when specifying a timestamp, + // so we have to set timezone separately afterward. + $this->time = (new \DateTimeImmutable("@{$time}")) + ->setTimezone(\core_date::get_server_timezone_object()); } else { $this->time = new \DateTimeImmutable(); } @@ -55,7 +58,8 @@ class frozen_clock implements \core\clock { * @param int $time */ public function set_to(int $time): void { - $this->time = new \DateTimeImmutable("@{$time}"); + $this->time = (new \DateTimeImmutable("@{$time}")) + ->setTimezone(\core_date::get_server_timezone_object()); } /** diff --git a/lib/tests/behat/behat_general.php b/lib/tests/behat/behat_general.php index cb6c1ded08e..5913f13bf4e 100644 --- a/lib/tests/behat/behat_general.php +++ b/lib/tests/behat/behat_general.php @@ -2708,4 +2708,34 @@ EOF; throw new Exception("Text '{$text}' found in the row containing '{$rowtext}'"); } } + + /** + * Sets the current time for the remainder of this Behat test. + * + * This is not supported everywhere in Moodle: if code uses \core\clock through DI then + * it will work, but if it just calls time() it will still get the real time. + * + * @Given the time is frozen at :datetime + * @param string $datetime Date and time in a format that strtotime understands + */ + public function the_time_is_frozen_at(string $datetime): void { + global $CFG; + require_once($CFG->libdir . '/testing/classes/frozen_clock.php'); + + $timestamp = strtotime($datetime); + // The config variable is used to set up a frozen clock in each Behat web request. + set_config('behat_frozen_clock', $timestamp); + // Simply setting a frozen clock in DI should work for future steps in Behat CLI process. + \core\di::set(\core\clock::class, new \frozen_clock($timestamp)); + } + + /** + * Stops freezing time so that it goes back to real time. + * + * @Given the time is no longer frozen + */ + public function the_time_is_no_longer_frozen(): void { + unset_config('behat_frozen_clock'); + \core\di::set(\core\clock::class, new \core\system_clock()); + } }