This commit is contained in:
@@ -0,0 +1,43 @@
|
|||||||
|
name: PHP Composer
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Validate composer.json and composer.lock
|
||||||
|
run: composer validate --strict
|
||||||
|
|
||||||
|
- name: Cache Composer packages
|
||||||
|
id: composer-cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: vendor
|
||||||
|
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-php-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: composer install --prefer-dist --no-progress
|
||||||
|
|
||||||
|
- name: PHPStan
|
||||||
|
run: composer phpstan
|
||||||
|
|
||||||
|
- name: Code style check
|
||||||
|
run: composer cs-check
|
||||||
|
|
||||||
|
- name: Tests
|
||||||
|
run: composer test
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/vendor/
|
||||||
|
/composer.lock
|
||||||
|
/.php-cs-fixer.cache
|
||||||
|
/.phpunit.result.cache
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use PhpCsFixer\Config;
|
||||||
|
use PhpCsFixer\Finder;
|
||||||
|
|
||||||
|
$finder = Finder::create()
|
||||||
|
->in([
|
||||||
|
__DIR__ . '/src',
|
||||||
|
__DIR__ . '/tests',
|
||||||
|
])
|
||||||
|
->name('*.php')
|
||||||
|
->notName('*.blade.php')
|
||||||
|
->ignoreDotFiles(true)
|
||||||
|
->ignoreVCS(true);
|
||||||
|
|
||||||
|
return (new Config())
|
||||||
|
->setRiskyAllowed(true)
|
||||||
|
->setRules([
|
||||||
|
'@PER-CS2.0' => true,
|
||||||
|
'@PHP83Migration' => true,
|
||||||
|
'ordered_imports' => ['sort_algorithm' => 'alpha'],
|
||||||
|
'no_unused_imports' => true,
|
||||||
|
'global_namespace_import' => ['import_classes' => false, 'import_constants' => false, 'import_functions' => false],
|
||||||
|
'single_quote' => true,
|
||||||
|
'explicit_string_variable' => true,
|
||||||
|
'array_syntax' => ['syntax' => 'short'],
|
||||||
|
'trim_array_spaces' => true,
|
||||||
|
'no_whitespace_before_comma_in_array' => true,
|
||||||
|
'whitespace_after_comma_in_array' => ['ensure_single_space' => true],
|
||||||
|
'declare_strict_types' => true,
|
||||||
|
'strict_param' => true,
|
||||||
|
'strict_comparison' => true,
|
||||||
|
'phpdoc_align' => ['align' => 'left'],
|
||||||
|
'phpdoc_order' => true,
|
||||||
|
'phpdoc_trim' => true,
|
||||||
|
'phpdoc_scalar' => true,
|
||||||
|
'no_superfluous_phpdoc_tags' => ['remove_inheritdoc' => true],
|
||||||
|
'yoda_style' => false,
|
||||||
|
'no_useless_else' => true,
|
||||||
|
'no_useless_return' => true,
|
||||||
|
'concat_space' => ['spacing' => 'one'],
|
||||||
|
'trailing_comma_in_multiline' => ['elements' => ['arrays', 'arguments', 'parameters']],
|
||||||
|
'blank_line_before_statement' => ['statements' => ['return', 'throw', 'try', 'if', 'foreach', 'for', 'while']],
|
||||||
|
])
|
||||||
|
->setFinder($finder);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
FROM php:8.3-cli-alpine
|
||||||
|
|
||||||
|
RUN apk add --no-cache git unzip
|
||||||
|
|
||||||
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
RUN mkdir -p /tmp/composer && chown 1000:1000 /tmp/composer
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
PHP = docker compose run --rm php
|
||||||
|
|
||||||
|
.PHONY: install test phpstan cs-fix cs-check audit shell build
|
||||||
|
|
||||||
|
build:
|
||||||
|
docker compose build
|
||||||
|
|
||||||
|
install:
|
||||||
|
$(PHP) composer install
|
||||||
|
|
||||||
|
test:
|
||||||
|
$(PHP) vendor/bin/phpunit --testsuite Unit --colors=always
|
||||||
|
|
||||||
|
phpstan:
|
||||||
|
$(PHP) vendor/bin/phpstan analyse --no-progress --ansi --memory-limit=512M
|
||||||
|
|
||||||
|
cs-fix:
|
||||||
|
$(PHP) vendor/bin/php-cs-fixer fix --ansi
|
||||||
|
|
||||||
|
cs-check:
|
||||||
|
$(PHP) vendor/bin/php-cs-fixer fix --dry-run --diff --ansi
|
||||||
|
|
||||||
|
audit:
|
||||||
|
$(PHP) composer audit
|
||||||
|
|
||||||
|
ci: audit phpstan cs-check test
|
||||||
|
|
||||||
|
shell:
|
||||||
|
docker compose run --rm php sh
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# tez-utils-clock
|
||||||
|
|
||||||
|
Testable clock abstractions and a high-resolution stopwatch for PHP — `ClockInterface`, `SystemClock`, `FrozenClock` for deterministic tests, and `Stopwatch` with lap support. PHP 8.3+, no dependencies.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require tez/utils-clock
|
||||||
|
```
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
| Class | Purpose |
|
||||||
|
|-------|---------|
|
||||||
|
| `ClockInterface` | PSR-20 compatible clock contract |
|
||||||
|
| `SystemClock` | Returns the actual current time |
|
||||||
|
| `FrozenClock` | Returns a fixed time — for deterministic tests |
|
||||||
|
| `Stopwatch` | High-resolution elapsed time and lap measurement |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ClockInterface
|
||||||
|
|
||||||
|
```php
|
||||||
|
interface ClockInterface
|
||||||
|
{
|
||||||
|
public function now(): \DateTimeImmutable;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
PSR-20 compatible. Inject `ClockInterface` into services instead of calling `new \DateTimeImmutable()` directly — this makes time controllable in tests without mocking.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SystemClock
|
||||||
|
|
||||||
|
Returns the actual current time, honoring the system timezone.
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Tez\Utils\Clock\SystemClock;
|
||||||
|
|
||||||
|
$clock = new SystemClock();
|
||||||
|
$now = $clock->now(); // \DateTimeImmutable
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FrozenClock
|
||||||
|
|
||||||
|
Returns a fixed `DateTimeImmutable` — always the same instant until `set()` is called. Use it in tests to control time without relying on the system clock.
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Tez\Utils\Clock\FrozenClock;
|
||||||
|
|
||||||
|
$clock = new FrozenClock(new \DateTimeImmutable('2026-01-01 12:00:00'));
|
||||||
|
|
||||||
|
$clock->now(); // 2026-01-01 12:00:00
|
||||||
|
$clock->now(); // 2026-01-01 12:00:00 — always the same
|
||||||
|
|
||||||
|
// Advance to a new instant
|
||||||
|
$clock->set(new \DateTimeImmutable('2026-06-15 08:30:00'));
|
||||||
|
$clock->now(); // 2026-06-15 08:30:00
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing example
|
||||||
|
|
||||||
|
```php
|
||||||
|
final class OrderServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testOrderExpiresAfterDeadline(): void
|
||||||
|
{
|
||||||
|
$clock = new FrozenClock(new \DateTimeImmutable('2026-01-01 12:00:00'));
|
||||||
|
$service = new OrderService($clock);
|
||||||
|
|
||||||
|
self::assertFalse($service->isExpired($order));
|
||||||
|
|
||||||
|
// Advance the clock past the deadline
|
||||||
|
$clock->set(new \DateTimeImmutable('2026-01-10 00:00:00'));
|
||||||
|
|
||||||
|
self::assertTrue($service->isExpired($order));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stopwatch
|
||||||
|
|
||||||
|
High-resolution elapsed time measurement using `hrtime()` (nanosecond precision). Not affected by NTP adjustments or system clock changes.
|
||||||
|
|
||||||
|
```php
|
||||||
|
use Tez\Utils\Clock\Stopwatch;
|
||||||
|
|
||||||
|
$sw = Stopwatch::start();
|
||||||
|
|
||||||
|
// ... do work ...
|
||||||
|
|
||||||
|
$sw->elapsed(); // total seconds since start (float)
|
||||||
|
$sw->elapsedMs(); // total milliseconds since start (float)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lap timing
|
||||||
|
|
||||||
|
`lap()` returns the elapsed seconds since the last `lap()` call (or since start if no lap yet), then moves the lap marker forward.
|
||||||
|
|
||||||
|
```php
|
||||||
|
$sw = Stopwatch::start();
|
||||||
|
|
||||||
|
// ... first stage ...
|
||||||
|
$stage1 = $sw->lap(); // seconds for stage 1
|
||||||
|
|
||||||
|
// ... second stage ...
|
||||||
|
$stage2 = $sw->lap(); // seconds for stage 2 only
|
||||||
|
|
||||||
|
$total = $sw->elapsed(); // total since start (unaffected by laps)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reset
|
||||||
|
|
||||||
|
```php
|
||||||
|
$sw->reset(); // resets both the total timer and the lap marker to now
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- PHP 8.3+
|
||||||
|
- No runtime dependencies
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "tez/utils-clock",
|
||||||
|
"type": "library",
|
||||||
|
"description": "Testable clock abstractions and a high-resolution stopwatch for PHP — ClockInterface, SystemClock, FrozenClock for deterministic tests, and Stopwatch with lap support. PHP 8.3+, no dependencies.",
|
||||||
|
"license": "MIT",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Tez\\Utils\\Clock\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Tez\\Utils\\Tests\\Clock\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.3"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^11.0",
|
||||||
|
"jetbrains/phpstorm-attributes": "^1.0",
|
||||||
|
"phpstan/phpstan": "^2.0",
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"cs-fix": "php-cs-fixer fix",
|
||||||
|
"cs-check": "php-cs-fixer fix --dry-run --diff",
|
||||||
|
"phpstan": "phpstan analyse",
|
||||||
|
"test": "phpunit --testsuite Unit --colors=always"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
services:
|
||||||
|
php:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- composer-cache:/tmp/composer
|
||||||
|
user: "${UID:-1000}:${GID:-1000}"
|
||||||
|
environment:
|
||||||
|
COMPOSER_HOME: /tmp/composer
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
composer-cache:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
parameters:
|
||||||
|
level: 9
|
||||||
|
paths:
|
||||||
|
- src
|
||||||
|
- tests
|
||||||
|
treatPhpDocTypesAsCertain: false
|
||||||
|
parallel:
|
||||||
|
maximumNumberOfProcesses: 1
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
colors="true">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Unit">
|
||||||
|
<directory>tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory suffix=".php">src</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
|
</phpunit>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tez\Utils\Clock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PSR-20 compatible clock interface.
|
||||||
|
*
|
||||||
|
* Implementations must return the current time as a DateTimeImmutable.
|
||||||
|
* Compatible with psr/clock if that dependency is ever added.
|
||||||
|
*/
|
||||||
|
interface ClockInterface
|
||||||
|
{
|
||||||
|
public function now(): \DateTimeImmutable;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tez\Utils\Clock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deterministic clock for testing — always returns the same fixed time.
|
||||||
|
*
|
||||||
|
* Use set() to advance the clock between assertions.
|
||||||
|
*/
|
||||||
|
final class FrozenClock implements ClockInterface
|
||||||
|
{
|
||||||
|
public function __construct(private \DateTimeImmutable $fixedTime) {}
|
||||||
|
|
||||||
|
public function now(): \DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->fixedTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advance (or rewind) the clock to a new fixed instant.
|
||||||
|
*/
|
||||||
|
public function set(\DateTimeImmutable $time): void
|
||||||
|
{
|
||||||
|
$this->fixedTime = $time;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tez\Utils\Clock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* High-resolution stopwatch using hrtime() for nanosecond precision.
|
||||||
|
*/
|
||||||
|
final class Stopwatch
|
||||||
|
{
|
||||||
|
private int $startNs;
|
||||||
|
private int $lapNs;
|
||||||
|
|
||||||
|
private function __construct()
|
||||||
|
{
|
||||||
|
$this->startNs = hrtime(true);
|
||||||
|
$this->lapNs = $this->startNs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a new stopwatch and return it.
|
||||||
|
*/
|
||||||
|
public static function start(): self
|
||||||
|
{
|
||||||
|
return new self();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the total elapsed time in seconds since the stopwatch was started.
|
||||||
|
*/
|
||||||
|
public function elapsed(): float
|
||||||
|
{
|
||||||
|
return (hrtime(true) - $this->startNs) / 1_000_000_000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the total elapsed time in milliseconds since the stopwatch was started.
|
||||||
|
*/
|
||||||
|
public function elapsedMs(): float
|
||||||
|
{
|
||||||
|
return (hrtime(true) - $this->startNs) / 1_000_000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the stopwatch (and the lap marker) to now.
|
||||||
|
*/
|
||||||
|
public function reset(): void
|
||||||
|
{
|
||||||
|
$this->startNs = hrtime(true);
|
||||||
|
$this->lapNs = $this->startNs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return elapsed seconds since the last lap() call (or start if no lap yet),
|
||||||
|
* and move the lap marker forward to now.
|
||||||
|
*/
|
||||||
|
public function lap(): float
|
||||||
|
{
|
||||||
|
$now = hrtime(true);
|
||||||
|
$lapElapsed = ($now - $this->lapNs) / 1_000_000_000;
|
||||||
|
$this->lapNs = $now;
|
||||||
|
|
||||||
|
return $lapElapsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tez\Utils\Clock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System clock that returns the actual current time.
|
||||||
|
*
|
||||||
|
* Honours the system timezone via new \DateTimeImmutable().
|
||||||
|
*/
|
||||||
|
final class SystemClock implements ClockInterface
|
||||||
|
{
|
||||||
|
public function now(): \DateTimeImmutable
|
||||||
|
{
|
||||||
|
return new \DateTimeImmutable();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tez\Utils\Tests\Clock;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Tez\Utils\Clock\ClockInterface;
|
||||||
|
use Tez\Utils\Clock\FrozenClock;
|
||||||
|
use Tez\Utils\Clock\Stopwatch;
|
||||||
|
use Tez\Utils\Clock\SystemClock;
|
||||||
|
|
||||||
|
final class ClockTest extends TestCase
|
||||||
|
{
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// SystemClock
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
public function testSystemClockImplementsInterface(): void
|
||||||
|
{
|
||||||
|
self::assertInstanceOf(ClockInterface::class, new SystemClock());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSystemClockReturnsDateTimeImmutable(): void
|
||||||
|
{
|
||||||
|
$clock = new SystemClock();
|
||||||
|
|
||||||
|
self::assertInstanceOf(\DateTimeImmutable::class, $clock->now());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSystemClockReturnsCurrentTime(): void
|
||||||
|
{
|
||||||
|
$before = new \DateTimeImmutable();
|
||||||
|
$now = (new SystemClock())->now();
|
||||||
|
$after = new \DateTimeImmutable();
|
||||||
|
|
||||||
|
self::assertGreaterThanOrEqual($before->getTimestamp(), $now->getTimestamp());
|
||||||
|
self::assertLessThanOrEqual($after->getTimestamp(), $now->getTimestamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// FrozenClock
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
public function testFrozenClockImplementsInterface(): void
|
||||||
|
{
|
||||||
|
$clock = new FrozenClock(new \DateTimeImmutable('2026-01-01 12:00:00'));
|
||||||
|
|
||||||
|
self::assertInstanceOf(ClockInterface::class, $clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFrozenClockAlwaysReturnsSameTime(): void
|
||||||
|
{
|
||||||
|
$fixed = new \DateTimeImmutable('2026-01-01 12:00:00');
|
||||||
|
$clock = new FrozenClock($fixed);
|
||||||
|
|
||||||
|
self::assertSame($fixed, $clock->now());
|
||||||
|
self::assertSame($fixed, $clock->now());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFrozenClockSetAdvancesTime(): void
|
||||||
|
{
|
||||||
|
$clock = new FrozenClock(new \DateTimeImmutable('2026-01-01 12:00:00'));
|
||||||
|
$later = new \DateTimeImmutable('2026-06-15 08:30:00');
|
||||||
|
|
||||||
|
$clock->set($later);
|
||||||
|
|
||||||
|
self::assertSame($later, $clock->now());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFrozenClockIsUsableInTypedContext(): void
|
||||||
|
{
|
||||||
|
$clock = new FrozenClock(new \DateTimeImmutable('2026-01-01 00:00:00'));
|
||||||
|
|
||||||
|
$timestamp = $this->readTimestamp($clock);
|
||||||
|
|
||||||
|
self::assertSame('2026-01-01 00:00:00', $timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function readTimestamp(ClockInterface $clock): string
|
||||||
|
{
|
||||||
|
return $clock->now()->format('Y-m-d H:i:s');
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Stopwatch
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
public function testStopwatchStartReturnsInstance(): void
|
||||||
|
{
|
||||||
|
self::assertInstanceOf(Stopwatch::class, Stopwatch::start());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStopwatchElapsedIsNonNegative(): void
|
||||||
|
{
|
||||||
|
$sw = Stopwatch::start();
|
||||||
|
|
||||||
|
self::assertGreaterThanOrEqual(0.0, $sw->elapsed());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStopwatchElapsedMsIsNonNegative(): void
|
||||||
|
{
|
||||||
|
$sw = Stopwatch::start();
|
||||||
|
|
||||||
|
self::assertGreaterThanOrEqual(0.0, $sw->elapsedMs());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStopwatchElapsedMsIsLargerThanElapsedSeconds(): void
|
||||||
|
{
|
||||||
|
$sw = Stopwatch::start();
|
||||||
|
// A non-trivial amount of time: elapsed in ms should be > in seconds
|
||||||
|
// for any time >= 1ms (both should be close to zero but ms > s for small values)
|
||||||
|
$elapsedSec = $sw->elapsed();
|
||||||
|
$elapsedMs = $sw->elapsedMs();
|
||||||
|
|
||||||
|
// For any positive time: ms value >= s value * 1 (since 1ms = 0.001s)
|
||||||
|
// This holds as long as elapsed < 1 second (which it will be in a test)
|
||||||
|
self::assertGreaterThanOrEqual($elapsedSec, $elapsedMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStopwatchResetMakesElapsedNearZero(): void
|
||||||
|
{
|
||||||
|
$sw = Stopwatch::start();
|
||||||
|
// Let some time pass conceptually (we can't sleep), then reset
|
||||||
|
$sw->reset();
|
||||||
|
|
||||||
|
self::assertLessThan(0.01, $sw->elapsed()); // under 10ms
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStopwatchLapReturnsNonNegative(): void
|
||||||
|
{
|
||||||
|
$sw = Stopwatch::start();
|
||||||
|
|
||||||
|
self::assertGreaterThanOrEqual(0.0, $sw->lap());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStopwatchSubsequentLapsAreIndependent(): void
|
||||||
|
{
|
||||||
|
$sw = Stopwatch::start();
|
||||||
|
$lap1 = $sw->lap();
|
||||||
|
$lap2 = $sw->lap();
|
||||||
|
|
||||||
|
// Both laps are non-negative and their sum should not exceed total elapsed
|
||||||
|
self::assertGreaterThanOrEqual(0.0, $lap1);
|
||||||
|
self::assertGreaterThanOrEqual(0.0, $lap2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testStopwatchLapDoesNotResetTotalElapsed(): void
|
||||||
|
{
|
||||||
|
$sw = Stopwatch::start();
|
||||||
|
$sw->lap();
|
||||||
|
$total = $sw->elapsed();
|
||||||
|
|
||||||
|
self::assertGreaterThanOrEqual(0.0, $total);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user