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