From 298c13ac3b25823a71b9f48e74ac187fbe06be41 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 6 Feb 2024 13:05:49 +0800 Subject: [PATCH] MDL-80838 core: Add PSR-20/Clock support This commit adds the PSR-20 ClockInterface to core, with a moodle-specific extension to the Interface at `\core\clock`, and a standard clock at `\core\system_clock`. Further clocks are provided as `\incrementing_clock` and `\frozen_clock` which are available to unit tests using: - `$this->mock_clock_with_incrementing(?int $starttime = null);` - `$this->mock_clock_with_frozen(?int $time = null);` For the incrementing clock, every call to fetch the time will bump the current time by one second. For the frozen clock the time will not change, but can be modified with: - `$clock->set_to(int $time);`; and - `$clock->bump(int $seconds = 1);` --- lib/classes/clock.php | 33 +++++++ lib/classes/component.php | 1 + lib/classes/di.php | 5 + lib/classes/system_clock.php | 34 +++++++ lib/phpunit/classes/advanced_testcase.php | 34 +++++++ lib/phpunit/tests/advanced_test.php | 65 +++++++++++++ lib/psr/clock/LICENSE | 19 ++++ lib/psr/clock/README.md | 61 ++++++++++++ lib/psr/clock/readme_moodle.txt | 12 +++ lib/psr/clock/src/ClockInterface.php | 13 +++ lib/testing/classes/frozen_clock.php | 69 ++++++++++++++ lib/testing/classes/incrementing_clock.php | 67 +++++++++++++ lib/testing/tests/clock_test.php | 106 +++++++++++++++++++++ lib/tests/system_clock_test.php | 37 +++++++ lib/thirdpartylibs.xml | 8 ++ 15 files changed, 564 insertions(+) create mode 100644 lib/classes/clock.php create mode 100644 lib/classes/system_clock.php create mode 100644 lib/psr/clock/LICENSE create mode 100644 lib/psr/clock/README.md create mode 100644 lib/psr/clock/readme_moodle.txt create mode 100644 lib/psr/clock/src/ClockInterface.php create mode 100644 lib/testing/classes/frozen_clock.php create mode 100644 lib/testing/classes/incrementing_clock.php create mode 100644 lib/testing/tests/clock_test.php create mode 100644 lib/tests/system_clock_test.php diff --git a/lib/classes/clock.php b/lib/classes/clock.php new file mode 100644 index 00000000000..cbb0885d674 --- /dev/null +++ b/lib/classes/clock.php @@ -0,0 +1,33 @@ +. + +namespace core; + +/** + * Moodle Clock interface. + * + * @package core + * @copyright 2024 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface clock extends \Psr\Clock\ClockInterface { + /** + * Return the unix time stamp for the current representation of the time. + * + * @return int + */ + public function time(): int; +} diff --git a/lib/classes/component.php b/lib/classes/component.php index bc13015f3a7..592aa6a6728 100644 --- a/lib/classes/component.php +++ b/lib/classes/component.php @@ -116,6 +116,7 @@ class core_component { 'lib/psr/http-factory/src', ], 'Psr\\EventDispatcher' => 'lib/psr/event-dispatcher/src', + 'Psr\\Clock' => 'lib/psr/clock/src', 'Psr\\Container' => 'lib/psr/container/src', 'GuzzleHttp\\Psr7' => 'lib/guzzlehttp/psr7/src', 'GuzzleHttp\\Promise' => 'lib/guzzlehttp/promises/src', diff --git a/lib/classes/di.php b/lib/classes/di.php index 7ce9ac29dcf..c8c1147c9d0 100644 --- a/lib/classes/di.php +++ b/lib/classes/di.php @@ -117,6 +117,11 @@ class di { // The string manager. \core_string_manager::class => fn() => get_string_manager(), + + // 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(), + \Psr\Clock\ClockInterface::class => \DI\get(\core\clock::class), ]); // Add any additional definitions using hooks. diff --git a/lib/classes/system_clock.php b/lib/classes/system_clock.php new file mode 100644 index 00000000000..32cee16898d --- /dev/null +++ b/lib/classes/system_clock.php @@ -0,0 +1,34 @@ +. + +namespace core; + +/** + * Standard system clock implementation. + * + * @package core + * @copyright 2024 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class system_clock implements clock { + public function now(): \DateTimeImmutable { + return new \DateTimeImmutable(); + } + + public function time(): int { + return $this->now()->getTimestamp(); + } +} diff --git a/lib/phpunit/classes/advanced_testcase.php b/lib/phpunit/classes/advanced_testcase.php index 9674a480156..4f039900f6c 100644 --- a/lib/phpunit/classes/advanced_testcase.php +++ b/lib/phpunit/classes/advanced_testcase.php @@ -729,4 +729,38 @@ abstract class advanced_testcase extends base_testcase { \core\task\manager::adhoc_task_complete($task); } } + + /** + * Mock the clock with an incrementing clock. + * + * @param null|int $starttime + * @return \incrementing_clock + */ + public function mock_clock_with_incrementing( + ?int $starttime = null, + ): \incrementing_clock { + require_once(dirname(__DIR__, 2) . '/testing/classes/incrementing_clock.php'); + $clock = new \incrementing_clock($starttime); + + \core\di::set(\core\clock::class, $clock); + + return $clock; + } + + /** + * Mock the clock with a frozen clock. + * + * @param null|int $time + * @return \frozen_clock + */ + public function mock_clock_with_frozen( + ?int $time = null, + ): \frozen_clock { + require_once(dirname(__DIR__, 2) . '/testing/classes/frozen_clock.php'); + $clock = new \frozen_clock($time); + + \core\di::set(\core\clock::class, $clock); + + return $clock; + } } diff --git a/lib/phpunit/tests/advanced_test.php b/lib/phpunit/tests/advanced_test.php index 530fdd04af9..818bbe51bda 100644 --- a/lib/phpunit/tests/advanced_test.php +++ b/lib/phpunit/tests/advanced_test.php @@ -735,4 +735,69 @@ class advanced_test extends \advanced_testcase { $this->runAdhocTasks(); $this->expectOutputRegex("/Task was run as {$user->id}/"); } + + /** + * Test the incrementing mock clock. + * + * @covers ::mock_clock_with_incrementing + * @covers \incrementing_clock + */ + public function test_mock_clock_with_incrementing(): void { + $standard = \core\di::get(\core\clock::class); + $this->assertInstanceOf(\Psr\Clock\ClockInterface::class, $standard); + $this->assertInstanceOf(\core\clock::class, $standard); + + $newclock = $this->mock_clock_with_incrementing(0); + $mockedclock = \core\di::get(\core\clock::class); + $this->assertInstanceOf(\incrementing_clock::class, $newclock); + $this->assertSame($newclock, $mockedclock); + + // Test the functionality. + $this->assertEquals(0, $mockedclock->now()->getTimestamp()); + $this->assertEquals(1, $newclock->now()->getTimestamp()); + $this->assertEquals(2, $mockedclock->now()->getTimestamp()); + + // Specify a specific start time. + $newclock = $this->mock_clock_with_incrementing(12345); + $mockedclock = \core\di::get(\core\clock::class); + $this->assertSame($newclock, $mockedclock); + + $this->assertEquals(12345, $mockedclock->now()->getTimestamp()); + $this->assertEquals(12346, $newclock->now()->getTimestamp()); + $this->assertEquals(12347, $mockedclock->now()->getTimestamp()); + + $this->assertEquals($newclock->time, $mockedclock->now()->getTimestamp()); + } + + /** + * Test the incrementing mock clock. + * + * @covers ::mock_clock_with_frozen + * @covers \frozen_clock + */ + public function test_mock_clock_with_frozen(): void { + $standard = \core\di::get(\core\clock::class); + $this->assertInstanceOf(\Psr\Clock\ClockInterface::class, $standard); + $this->assertInstanceOf(\core\clock::class, $standard); + + $newclock = $this->mock_clock_with_frozen(0); + $mockedclock = \core\di::get(\core\clock::class); + $this->assertInstanceOf(\frozen_clock::class, $newclock); + $this->assertSame($newclock, $mockedclock); + + // Test the functionality. + $initialtime = $mockedclock->now()->getTimestamp(); + $this->assertEquals($initialtime, $newclock->now()->getTimestamp()); + $this->assertEquals($initialtime, $mockedclock->now()->getTimestamp()); + + // Specify a specific start time. + $newclock = $this->mock_clock_with_frozen(12345); + $mockedclock = \core\di::get(\core\clock::class); + $this->assertSame($newclock, $mockedclock); + + $initialtime = $mockedclock->now(); + $this->assertEquals($initialtime, $mockedclock->now()); + $this->assertEquals($initialtime, $newclock->now()); + $this->assertEquals($initialtime, $mockedclock->now()); + } } diff --git a/lib/psr/clock/LICENSE b/lib/psr/clock/LICENSE new file mode 100644 index 00000000000..be683421232 --- /dev/null +++ b/lib/psr/clock/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2017 PHP Framework Interoperability Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/lib/psr/clock/README.md b/lib/psr/clock/README.md new file mode 100644 index 00000000000..7dedc2d0661 --- /dev/null +++ b/lib/psr/clock/README.md @@ -0,0 +1,61 @@ +# PSR Clock + +This repository holds the interface for [PSR-20][psr-url]. + +Note that this is not a clock of its own. It is merely an interface that +describes a clock. See the specification for more details. + +## Installation + +```bash +composer require psr/clock +``` + +## Usage + +If you need a clock, you can use the interface like this: + +```php +clock = $clock; + } + + public function doSomething() + { + /** @var DateTimeImmutable $currentDateAndTime */ + $currentDateAndTime = $this->clock->now(); + // do something useful with that information + } +} +``` + +You can then pick one of the [implementations][implementation-url] of the interface to get a clock. + +If you want to implement the interface, you can require this package and +implement `Psr\Clock\ClockInterface` in your code. + +Don't forget to add `psr/clock-implementation` to your `composer.json`s `provide`-section like this: + +```json +{ + "provide": { + "psr/clock-implementation": "1.0" + } +} +``` + +And please read the [specification text][specification-url] for details on the interface. + +[psr-url]: https://www.php-fig.org/psr/psr-20 +[package-url]: https://packagist.org/packages/psr/clock +[implementation-url]: https://packagist.org/providers/psr/clock-implementation +[specification-url]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-20-clock.md diff --git a/lib/psr/clock/readme_moodle.txt b/lib/psr/clock/readme_moodle.txt new file mode 100644 index 00000000000..476fdc7e433 --- /dev/null +++ b/lib/psr/clock/readme_moodle.txt @@ -0,0 +1,12 @@ +# PSR-20 Clock + +## Installation + +1. Visit https://github.com/php-fig/clock +2. Download the latest release +3. Unzip in this folder +4. Update `thirdpartylibs.xml` +5. Remove any unnecessary files, including: + - Any tests + - CHANGELOG.md + - composer.json diff --git a/lib/psr/clock/src/ClockInterface.php b/lib/psr/clock/src/ClockInterface.php new file mode 100644 index 00000000000..7b6d8d8aae2 --- /dev/null +++ b/lib/psr/clock/src/ClockInterface.php @@ -0,0 +1,13 @@ +. + +/** + * Frozen clock for testing purposes. + * + * @package core + * @copyright 2024 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @property-read \DateTimeImmutable $time The current time of the clock + */ +class frozen_clock implements \core\clock { + /** @var DateTimeImmutable The next time of the clock */ + public DateTimeImmutable $time; + + /** + * Create a new instance of the frozen clock. + * + * @param null|int $time The initial time to use. If not specified, the current time is used. + */ + public function __construct( + ?int $time = null, + ) { + if ($time) { + $this->time = new \DateTimeImmutable("@{$time}"); + } else { + $this->time = new \DateTimeImmutable(); + } + } + + public function now(): \DateTimeImmutable { + return $this->time; + } + + public function time(): int { + return $this->time->getTimestamp(); + } + + /** + * Set the time of the clock. + * + * @param int $time + */ + public function set_to(int $time): void { + $this->time = new \DateTimeImmutable("@{$time}"); + } + + /** + * Bump the time by a number of seconds. + * + * @param int $seconds + */ + public function bump(int $seconds = 1): void { + $this->time = $this->time->modify("+{$seconds} seconds"); + } +} diff --git a/lib/testing/classes/incrementing_clock.php b/lib/testing/classes/incrementing_clock.php new file mode 100644 index 00000000000..e124c7b7d19 --- /dev/null +++ b/lib/testing/classes/incrementing_clock.php @@ -0,0 +1,67 @@ +. + +/** + * Incrementing clock for testing purposes. + * + * @package core + * @copyright 2024 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @property-read int $time The current time of the clock + */ +class incrementing_clock implements \core\clock { + /** @var int The next time of the clock */ + public int $time; + + /** + * Create a new instance of the incrementing clock. + * + * @param null|int $starttime The initial time to use. If not specified, the current time is used. + */ + public function __construct( + ?int $starttime = null, + ) { + $this->time = $starttime ?? time(); + } + + public function now(): \DateTimeImmutable { + return new \DateTimeImmutable('@' . $this->time++); + } + + public function time(): int { + return $this->now()->getTimestamp(); + } + + /** + * Set the time of the clock. + * + * @param int $time + */ + public function set_to(int $time): void { + $this->time = $time; + } + + /** + * Bump the time by a number of seconds. + * + * Note: The act of fetching the time will also bump the time by one second. + * + * @param int $seconds + */ + public function bump(int $seconds = 1): void { + $this->time += $seconds; + } +} diff --git a/lib/testing/tests/clock_test.php b/lib/testing/tests/clock_test.php new file mode 100644 index 00000000000..3f88ca9597c --- /dev/null +++ b/lib/testing/tests/clock_test.php @@ -0,0 +1,106 @@ +. + +namespace core; + +use frozen_clock; +use incrementing_clock; + +/** + * Tests for testing clocks. + * + * @package core + * @copyright 2024 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class clock_test extends \advanced_testcase { + /** + * Test the incrementing mock clock. + * + * @covers \incrementing_clock + */ + public function test_clock_with_incrementing(): void { + require_once(__DIR__ . '/../classes/incrementing_clock.php'); + + $clock = new incrementing_clock(); + $this->assertInstanceOf(\incrementing_clock::class, $clock); + + $initialtime = $clock->now()->getTimestamp(); + + // Test the functionality. + $this->assertEquals($initialtime + 1, $clock->now()->getTimestamp()); + $this->assertEquals($initialtime + 2, $clock->time()); + $this->assertEquals($initialtime + 3, $clock->now()->getTimestamp()); + + // Specify a specific start time. + $clock = new incrementing_clock(12345); + + $this->assertEquals(12345, $clock->now()->getTimestamp()); + $this->assertEquals(12346, $clock->time()); + $this->assertEquals(12347, $clock->now()->getTimestamp()); + + $clock->set_to(12345); + $this->assertEquals(12345, $clock->time()); + $this->assertEquals(12346, $clock->time()); + + $clock->bump(); + $this->assertEquals(12348, $clock->time()); + $clock->bump(); + $this->assertEquals(12350, $clock->time()); + $clock->bump(5); + $this->assertEquals(12356, $clock->time()); + } + + /** + * Test the incrementing mock clock. + * + * @covers \frozen_clock + */ + public function test_mock_clock_with_frozen(): void { + require_once(__DIR__ . '/../classes/frozen_clock.php'); + + $clock = new frozen_clock(); + + // Test the functionality. + $initialtime = $clock->now()->getTimestamp(); + $this->assertEquals($initialtime, $clock->now()->getTimestamp()); + $this->assertEquals($initialtime, $clock->now()->getTimestamp()); + $this->assertEquals($initialtime, $clock->now()->getTimestamp()); + $this->assertEquals($initialtime, $clock->time()); + + // Specify a specific start time. + $clock = new frozen_clock(12345); + + $initialtime = $clock->now(); + $this->assertEquals($initialtime, $clock->now()); + $this->assertEquals($initialtime, $clock->now()); + $this->assertEquals($initialtime, $clock->now()); + + $clock->set_to(12345); + $this->assertEquals(12345, $clock->now()->getTimestamp()); + $this->assertEquals(12345, $clock->now()->getTimestamp()); + $this->assertEquals(12345, $clock->now()->getTimestamp()); + + $this->assertEquals(12345, $clock->time()); + + $clock->bump(); + $this->assertEquals(12346, $clock->time()); + $clock->bump(); + $this->assertEquals(12347, $clock->time()); + $clock->bump(5); + $this->assertEquals(12352, $clock->time()); + } +} diff --git a/lib/tests/system_clock_test.php b/lib/tests/system_clock_test.php new file mode 100644 index 00000000000..c8f5555303d --- /dev/null +++ b/lib/tests/system_clock_test.php @@ -0,0 +1,37 @@ +. + +namespace core; + +/** + * Tests for the standard ClockInterface implementation. + * + * @package core + * @category test + * @copyright 2024 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @covers \core\system_clock + */ +final class system_clock_test extends \advanced_testcase { + public function test_now(): void { + $starttime = time(); + + $clock = new system_clock(); + $now = $clock->now(); + $this->assertInstanceOf(\DateTimeImmutable::class, $now); + $this->assertGreaterThanOrEqual($starttime, $now->getTimestamp()); + } +} diff --git a/lib/thirdpartylibs.xml b/lib/thirdpartylibs.xml index e96b97ac564..24cb3d8432c 100644 --- a/lib/thirdpartylibs.xml +++ b/lib/thirdpartylibs.xml @@ -612,6 +612,14 @@ All rights reserved. MIT https://github.com/php-fig/container + + psr/clock + clock + Clock Interface (PHP FIG PSR-20). + 1.0.0 + MIT + https://github.com/php-fig/clock + psr/http-client http-client -- 2.43.0