Repository: tomloprod/time-warden
Branch: main
Commit: e9051e2c66ad
Files: 32
Total size: 74.6 KB
Directory structure:
gitextract_0ftg2r_n/
├── .editorconfig
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ ├── formats.yml
│ └── tests.yml
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── composer.json
├── phpstan.neon.dist
├── phpunit.xml.dist
├── pint.json
├── rector.php
├── src/
│ ├── Concerns/
│ │ └── HasTasks.php
│ ├── Contracts/
│ │ └── Taskable.php
│ ├── Group.php
│ ├── Services/
│ │ └── TimeWardenManager.php
│ ├── Support/
│ │ ├── Console/
│ │ │ └── Table.php
│ │ ├── Facades/
│ │ │ └── TimeWarden.php
│ │ └── TimeWardenAlias.php
│ ├── Task.php
│ └── TimeWardenSummary.php
└── tests/
├── ArchTest.php
├── Contracts/
│ └── TaskableTest.php
├── GroupTest.php
├── Services/
│ └── TimeWardenManagerTest.php
├── Support/
│ ├── Facades/
│ │ └── TimeWardenTest.php
│ └── TimeWardenAliasTest.php
├── TaskTest.php
└── TimeWardenSummaryTest.php
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
; This file is for unifying the coding style for different editors and IDEs.
; More information at http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.yml]
indent_size = 2
================================================
FILE: .gitattributes
================================================
/docs export-ignore
/tests export-ignore
/scripts export-ignore
/.github export-ignore
/.php_cs export-ignore
.editorconfig export-ignore
.gitattributes export-ignore
.gitignore export-ignore
phpstan.neon.dist export-ignore
phpunit.xml.dist export-ignore
rector.php export-ignore
CHANGELOG.md export-ignore
CONTRIBUTING.md export-ignore
README.md export-ignore
================================================
FILE: .github/FUNDING.yml
================================================
custom: https://www.paypal.com/paypalme/tomloprod
================================================
FILE: .github/workflows/formats.yml
================================================
name: Formats
on: ['push', 'pull_request']
jobs:
ci:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest]
php: [8.2]
dependency-version: [prefer-lowest, prefer-stable]
name: Formats P${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, mbstring, zip
coverage: pcov
- name: Get Composer cache directory
id: composer-cache
shell: bash
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: dependencies-php-${{ matrix.php }}-os-${{ matrix.os }}-version-${{ matrix.dependency-version }}-composer-${{ hashFiles('composer.json') }}
restore-keys: dependencies-php-${{ matrix.php }}-os-${{ matrix.os }}-version-${{ matrix.dependency-version }}-composer-
- name: Install Composer dependencies
run: composer update --${{ matrix.dependency-version }} --no-interaction --prefer-dist
- name: Coding Style Checks
run: composer test:lint
- name: Type Checks
run: composer test:types
================================================
FILE: .github/workflows/tests.yml
================================================
name: Tests
on: ['push', 'pull_request']
jobs:
ci:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
php: [8.2, 8.3]
dependency-version: [prefer-lowest, prefer-stable]
name: Tests P${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, mbstring, zip
coverage: none
- name: Get Composer cache directory
id: composer-cache
shell: bash
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: dependencies-php-${{ matrix.php }}-os-${{ matrix.os }}-version-${{ matrix.dependency-version }}-composer-${{ hashFiles('composer.json') }}
restore-keys: dependencies-php-${{ matrix.php }}-os-${{ matrix.os }}-version-${{ matrix.dependency-version }}-composer-
- name: Install Composer dependencies
run: composer update --${{ matrix.dependency-version }} --no-interaction --prefer-dist
- name: Integration Tests
run: php ./vendor/bin/pest
================================================
FILE: .gitignore
================================================
/.phpunit.result.cache
/.phpunit.cache
/.php-cs-fixer.cache
/.php-cs-fixer.php
/composer.lock
/phpunit.xml
/vendor/
*.swp
*.swo
================================================
FILE: CHANGELOG.md
================================================
## Version 1.0.1
> 20 May, 2024
- test: created task belongs to group (`getTaskable()`) by @tomloprod in https://github.com/tomloprod/time-warden/pull/1
- ref: replace task substitution system by @tomloprod in https://github.com/tomloprod/time-warden/pull/2
## Version 1.0.0
> 20 May, 2024
- First TimeWarden version
================================================
FILE: CONTRIBUTING.md
================================================
# 🧑🤝🧑 Contributing
Contributions are welcome, and are accepted via pull requests.
Please review these guidelines before submitting any pull requests.
## Process
1. Fork the project
1. Create a new branch
1. Code, test, commit and push
1. Open a pull request detailing your changes.
## Guidelines
Time warden uses a few tools to ensure the code quality and consistency. [Pest](https://pestphp.com) is the testing framework of choice, and we also use [PHPStan](https://phpstan.org) for static analysis. Pest's type coverage is at 100%, and the test suite is also at 100% coverage.
In terms of code style, we use [Laravel Pint](https://laravel.com/docs/11.x/pint) to ensure the code is consistent and follows the Laravel conventions. We also use [Rector](https://getrector.org) to ensure the code is up to date with the latest PHP version.
You run these tools individually using the following commands:
```bash
# Lint the code using Pint
composer lint
composer test:lint
# Refactor the code using Rector
composer refactor
composer test:refactor
# Run PHPStan
composer test:types
# Run the test suite
composer test:unit
# Run all the tools
composer test
```
================================================
FILE: LICENSE.md
================================================
The MIT License (MIT)
Copyright (c) Tomás López
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.
================================================
FILE: README.md
================================================
------
## ⏱️ **About TimeWarden**
TimeWarden is a lightweight PHP library that allows you to **monitor the processing time of tasks** (*useful during the development stage and debugging*) and also lets you set estimated execution times for tasks, **enabling reactive actions** when tasks exceed their estimated duration.
TimeWarden uses **high-resolution timing** (`hrtime`) for **nanosecond precision**, ensuring accurate measurements even for very fast operations.
TimeWarden is framework-agnostic, meaning it's not exclusive to any particular framework. It can seamlessly integrate into any PHP application, whether they utilize frameworks like Laravel (🧡), Symfony, or operate without any framework at all.
## **✨ Getting Started**
### Reactive Actions
You can specify an estimated execution time for each task and set an action to be performed when the time is exceeded (*example: send an email, add an entry to the error log, etc.*).
#### Example
```php
timeWarden()->task('Checking articles')->start();
foreach ($articles as $article) {
// Perform long process... 🕒
}
// Using traditional anonymous function
timeWarden()->stop(static function (Task $task): void {
$task->onExceedsMilliseconds(500, static function (Task $task): void {
// Do what you need, for example, send an email 🙂
Mail::to('foo@bar.com')->queue(
new SlowArticleProcess($task)
);
});
});
// Or using an arrow function
timeWarden()->stop(static function (Task $task): void {
$task->onExceedsMilliseconds(500, fn (Task $task) => Log::error($task->name.' has taken too long'));
});
```
#### Available methods
If you're not convinced about using `onExceedsMilliseconds`, you have other options:
```php
$task->onExceedsSeconds(10, function () { ... });
$task->onExceedsMinutes(5, function () { ... });
$task->onExceedsHours(2, function () { ... });
```
### Quick Measurement
TimeWarden provides a convenient `measure()` method that automatically handles task creation, starting, and stopping for you. This method executes a callable and returns the execution time in milliseconds.
#### Example
```php
// Simple measurement
$duration = timeWarden()->measure(function() {
// Your code here
sleep(1);
processData();
});
echo "Execution took: {$duration} ms";
// With custom task name
$duration = timeWarden()->measure(function() {
// Your code here
processArticles();
}, 'Processing Articles');
// The task will appear in your TimeWarden output with the specified name
echo timeWarden()->output();
```
The `measure()` method:
- Automatically creates a task with the provided name (or 'callable' by default)
- Starts timing before execution
- Executes your callable
- Stops timing after execution (even if an exception occurs)
- Returns the duration in milliseconds
- Integrates with groups and the TimeWarden workflow
### Execution Time Debugging
It allows you to measure the execution time of tasks in your application, as well as the possibility of adding those tasks to a group.
#### Simple tasks
```php
timeWarden()->task('Articles task');
foreach ($articles as $article) {
// Perform long process...
}
// Previous task is automatically stopped when a new task is created
timeWarden()->task('Customers task');
foreach ($customers as $customer) {
// Perform long process...
}
/**
* You can print the results directly or obtain a
* summary with the `getSummary()` method
*/
echo timeWarden()->output();
```
**Result:**
```log
╔═════════════════════ TIMEWARDEN ═════╤═══════════════╗
║ GROUP │ TASK │ DURATION (MS) ║
╠═════════════════════╪════════════════╪═══════════════╣
║ default (320.37 ms) │ Articles task │ 70.23 ║
║ │ Customers task │ 250.14 ║
╚══════════════════ Total: 320.37 ms ══╧═══════════════╝
```
#### Grouped tasks
```php
timeWarden()->group('Articles')->task('Loop of articles')->start();
foreach ($articles as $article) {
// Perform first operations
}
timeWarden()->task('Other articles process')->start();
Foo::bar();
// Previous task is automatically stopped when a new task is created
timeWarden()->group('Customers')->task('Customers task')->start();
foreach ($customers as $customer) {
// Perform long process...
}
timeWarden()->task('Other customer process')->start();
Bar::foo();
/**
* You can print the results directly or obtain a
* summary with the `getSummary()` method
*/
echo timeWarden()->output();
```
**Result:**
```log
╔═══════════════════════╤══ TIMEWARDEN ══════════╤═══════════════╗
║ GROUP │ TASK │ DURATION (MS) ║
╠═══════════════════════╪════════════════════════╪═══════════════╣
║ Articles (85.46 ms) │ Loop of articles │ 70.24 ║
║ │ Other articles process │ 15.22 ║
╟───────────────────────┼────────────────────────┼───────────────╢
║ Customers (280.46 ms) │ Customers task │ 250.22 ║
║ │ Other customer process │ 30.24 ║
╚═══════════════════════ Total: 365.92 ms ═══════╧═══════════════╝
```
#### 🧙 Tip
If your application has any logging system, it would be a perfect place to send the output.
```php
if (app()->environment('local')) {
Log::debug(timeWarden()->output());
}
```
### Ways of using TimeWarden
You can use TimeWarden either with the aliases `timeWarden()` (or `timewarden()`):
```php
timeWarden()->task('Task 1')->start();
```
or by directly invoking the static methods of the `TimeWarden` facade:
```php
TimeWarden::task('Task 1')->start();
```
You decide how to use it 🙂
## **🧱 Architecture**
TimeWarden is composed of several types of elements. Below are some features of each of these elements.
### `TimeWarden`
`Tomloprod\TimeWarden\Support\Facades\TimeWarden` is a facade that acts as a simplified interface for using the rest of the TimeWarden elements.
#### Methods
Most methods in this class return their own instance, allowing fluent syntax through method chaining.
```php
// Destroys the TimeWarden instance and returns a new one.
TimeWarden::reset(): TimeWarden
// Creates a new group.
TimeWarden::group(string $groupName): TimeWarden
/**
* Creates a new task inside the last created group
* or within the TimeWarden instance itself.
*/
TimeWarden::task(string $taskName): TimeWarden
// Starts the last created task
TimeWarden::start(): TimeWarden
// Stops the last created task
TimeWarden::stop(): TimeWarden
// Measures the execution time of a callable and returns duration in milliseconds
TimeWarden::measure(callable $fn, ?string $taskName = null): float
// Obtains all the created groups
TimeWarden::getGroups(): array
/**
* It allows you to obtain a TimeWardenSummary instance,
* which is useful for getting a summary of all groups
* and tasks generated by TimeWarden.
*
* Through that instance, you can retrieve the summary
* in array or string (JSON) format.
*/
TimeWarden::getSummary(): TimeWardenSummary
/**
* Returns a table with execution time debugging info
* (ideal for displaying in the console).
*/
TimeWarden::output(): string
```
Additionally, it has all the methods of the [Taskable](#taskable) interface.
### `Task`
All tasks you create are instances of `Tomloprod\TimeWarden\Task`.
The most useful methods and properties of a task are the following:
#### Properties
- `name`
#### Methods
```php
$task = new Task('Task 1');
$task->start(): void
$task->stop(?callable $fn = null): void
// Returns the duration of the task in a human-readable format. Example: *1day 10h 20min 30sec 150ms*
$task->getFriendlyDuration(): string
// Returns the duration of the task in milliseconds
$task->getDuration(): float
// Returns the taskable element to which the task belongs.
$task->getTaskable(): ?Taskable
$task->hasStarted(): bool
$task->hasEnded(): bool
$task->getStartDateTime(): ?DateTimeImmutable
$task->getEndDateTime(): ?DateTimeImmutable
// Returns the start and end timestamps in nanoseconds (high precision)
$task->getStartTimestamp(): int
$task->getEndTimestamp(): int
/** @return array */
$task->toArray(): array
// Reactive execution time methods
$task->onExceedsMilliseconds(float $milliseconds, callable $fn): ?Task
$task->onExceedsSeconds(float $seconds, callable $fn): ?Task
$task->onExceedsMinutes(float $minutes, callable $fn): ?Task
$task->onExceedsHours(float $hours, callable $fn): ?Task
```
### `Group`
All groups you create are instances of the `Tomloprod\TimeWarden\Group` object.
The most useful methods and properties of a group are the following:
#### Properties
- `name`
#### Methods
```php
// Starts the last created task inside this group
$group->start(): void
```
Additionally, it has all the methods of the [Taskable](#taskable) interface.
### `Taskable`
`Tomloprod\TimeWarden\Contracts\Taskable` is the interface used by the **TimeWarden** instance as well as by each task **group**
#### Methods
```php
// Create a new task within the taskable.
$taskable->createTask(string $taskName): Task
$taskable->getTasks(): array
$taskable->getLastTask(): ?Task
// Return the total time in milliseconds of all tasks within the taskable.
$taskable->getDuration(): float
$taskable->toArray(): array
$taskable->toJson(): string
```
### `TimeWardenSummary`
`Tomloprod\TimeWarden\TimeWardenSummary` is a class that allows obtaining a general summary of groups and their tasks generated with TimeWarden.
It is useful for obtaining a summary in array or string (JSON) format.
You can obtain an instance of `TimeWardenSummary` as follows:
```php
/** @var Tomloprod\TimeWarden\TimeWardenSummary $timeWardenSummary */
$timeWardenSummary = timeWarden()->getSummary();
```
#### Methods
```php
$timeWardenSummary->toArray(): array
$timeWardenSummary->toJson(): string
```
Here is an example of the data returned in array format:
```php
$summaryArray = [
[
'name' => 'default',
'duration' => 42.0,
'tasks' => [
[
'name' => 'TaskName1',
'duration' => 19.0,
'friendly_duration' => '19ms',
'start_timestamp' => 1496664000000000000, // nanoseconds
'end_timestamp' => 1496664000019000000, // nanoseconds
'start_datetime' => '2017-06-05T12:00:00+00:00',
'end_datetime' => '2017-06-05T12:00:00+00:00',
],
[
'name' => 'TaskName2',
'duration' => 23.0,
'friendly_duration' => '23ms',
'start_timestamp' => 1496664000000000000, // nanoseconds
'end_timestamp' => 1496664000023000000, // nanoseconds
'start_datetime' => '2017-06-05T12:00:00+00:00',
'end_datetime' => '2017-06-05T12:00:00+00:00',
],
],
],
[ // Others groups... ],
];
```
## **🚀 Installation & Requirements**
> **Requires [PHP 8.2+](https://php.net/releases/)**
You may use [Composer](https://getcomposer.org) to install TimeWarden into your PHP project:
```bash
composer require tomloprod/time-warden
```
## **🧑🤝🧑 Contributing**
Contributions are welcome, and are accepted via pull requests.
Please [review these guidelines](./CONTRIBUTING.md) before submitting any pull requests.
------
**TimeWarden** was created by **[Tomás López](https://twitter.com/tomloprod)** and open-sourced under the **[MIT license](https://opensource.org/licenses/MIT)**.
================================================
FILE: composer.json
================================================
{
"name": "tomloprod/time-warden",
"description": "TimeWarden is a lightweight PHP library that enables you to monitor the processing time of tasks and task groups (useful during the development stage). Additionally, it allows you to set maximum execution times to tasks, empowering proactive actions when tasks exceed their planned duration.",
"type": "library",
"keywords": [
"tomloprod",
"time-warden",
"execution time",
"debugging",
"monitoring",
"performance"
],
"license": "MIT",
"authors": [
{
"name": "Tomás López",
"email": "tomloprod@gmail.com"
}
],
"require": {
"php": "^8.2.0"
},
"require-dev": {
"laravel/pint": "^1.22.1",
"pestphp/pest": "^3.8.2",
"pestphp/pest-plugin-type-coverage": "^3.5.0",
"rector/rector": "^1.1.0"
},
"autoload": {
"psr-4": {
"Tomloprod\\TimeWarden\\": "src/"
},
"files": [
"src/Support/TimeWardenAlias.php"
]
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"minimum-stability": "dev",
"prefer-stable": true,
"config": {
"sort-packages": true,
"preferred-install": "dist",
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"scripts": {
"lint": "pint",
"refactor": "rector",
"test:lint": "pint --test",
"test:refactor": "rector --dry-run",
"test:types": "phpstan analyse",
"test:type-coverage": "pest --type-coverage --min=100",
"test:unit": "pest --coverage --min=100",
"test": [
"@test:lint",
"@test:refactor",
"@test:types",
"@test:type-coverage",
"@test:unit"
]
}
}
================================================
FILE: phpstan.neon.dist
================================================
parameters:
level: max
paths:
- src
reportUnmatchedIgnoredErrors: true
================================================
FILE: phpunit.xml.dist
================================================
./src./tests
================================================
FILE: pint.json
================================================
{
"preset": "laravel",
"rules": {
"array_push": true,
"backtick_to_shell_exec": true,
"date_time_immutable": true,
"declare_strict_types": true,
"lowercase_keywords": true,
"lowercase_static_reference": true,
"final_class": true,
"final_internal_class": true,
"final_public_method_for_abstract_class": true,
"fully_qualified_strict_types": true,
"global_namespace_import": {
"import_classes": true,
"import_constants": true,
"import_functions": true
},
"mb_str_functions": true,
"modernize_types_casting": true,
"new_with_parentheses": false,
"no_superfluous_elseif": true,
"no_useless_else": true,
"no_multiple_statements_per_line": true,
"ordered_class_elements": {
"order": [
"use_trait",
"case",
"constant",
"constant_public",
"constant_protected",
"constant_private",
"property_public",
"property_protected",
"property_private",
"construct",
"destruct",
"magic",
"phpunit",
"method_abstract",
"method_public_static",
"method_public",
"method_protected_static",
"method_protected",
"method_private_static",
"method_private"
],
"sort_algorithm": "none"
},
"ordered_interfaces": true,
"ordered_traits": true,
"protected_to_private": true,
"self_accessor": true,
"self_static_accessor": true,
"strict_comparison": true,
"visibility_required": true
}
}
================================================
FILE: rector.php
================================================
withPaths([
__DIR__.'/src',
__DIR__.'/tests',
])
->withSkip([])
->withPreparedSets(
deadCode: true,
codeQuality: true,
typeDeclarations: true,
privatization: true,
earlyReturn: true,
strictBooleans: true,
)
->withPhpSets();
================================================
FILE: src/Concerns/HasTasks.php
================================================
*/
private array $tasks = [];
public function createTask(string $taskName): Task
{
$task = new Task($taskName, $this);
$this->tasks[] = $task;
return $task;
}
/**
* @return array
*/
public function getTasks(): array
{
return $this->tasks;
}
/**
* @return float The duration time in milliseconds
*/
public function getDuration(): float
{
$duration = 0.0;
/** @var Task $task */
foreach ($this->getTasks() as $task) {
$duration += $task->getDuration();
}
return $duration;
}
public function getLastTask(): ?Task
{
/** @var Task|bool $lastTask */
$lastTask = end($this->tasks);
return ($lastTask instanceof Task) ? $lastTask : null;
}
public function toArray(): array
{
/** @var array $tasksInfo */
$tasksInfo = [];
/** @var Task $task */
foreach ($this->getTasks() as $task) {
$tasksInfo[] = $task->toArray();
}
return [
'name' => $this->name,
'duration' => $this->getDuration(),
'tasks' => $tasksInfo,
];
}
public function toJson(): string
{
$json = json_encode($this->toArray());
return ($json === false) ? '[]' : $json;
}
}
================================================
FILE: src/Contracts/Taskable.php
================================================
*/
public function getTasks(): array;
public function getLastTask(): ?Task;
public function getDuration(): float;
/** @return array */
public function toArray(): array;
public function toJson(): string;
}
================================================
FILE: src/Group.php
================================================
getLastTask();
if ($lastTask instanceof Task) {
$lastTask->start();
}
}
}
================================================
FILE: src/Services/TimeWardenManager.php
================================================
*/
private array $groups = [];
private function __construct() {}
public function __clone()
{
throw new Exception('Cannot clone singleton');
}
public function __wakeup()
{
throw new Exception('Cannot unserialize singleton');
}
/**
* Get the singleton instance of TimeWarden.
*/
public static function instance(): self
{
if (! isset(self::$instance)) {
self::$instance = new self();
}
return self::$instance;
}
public function reset(): self
{
self::$instance = new self();
return self::$instance;
}
public function group(string $groupName): self
{
$this->stop();
$group = $this->getLastGroup();
if ($group && ! $group->getLastTask() instanceof Task) {
$group->name = $groupName;
} else {
$this->groups[] = new Group($groupName);
}
return self::$instance;
}
public function task(string $taskName): self
{
/** @var Taskable $taskable */
$taskable = $this->getActiveTaskable();
/** @var Task|null $lastTask */
$lastTask = $taskable->getLastTask();
// If the last task was never started, we replace its name with `$taskName`
if ($lastTask instanceof Task && ! $lastTask->hasStarted()) {
$lastTask->name = $taskName;
} else {
// If there is a task, but it has already started, we stop it
if ($lastTask instanceof Task && $lastTask->hasStarted()) {
$lastTask->stop();
}
// And add the task to the taskable.
$taskable->createTask($taskName);
}
return self::$instance;
}
public function start(): self
{
/** @var Task|null $lastTask */
$lastTask = $this->getActiveTaskable()->getLastTask();
if ($lastTask instanceof Task) {
$lastTask->start();
}
return self::$instance;
}
public function stop(?callable $fn = null): self
{
/** @var Task|null $lastTask */
$lastTask = $this->getActiveTaskable()->getLastTask();
if ($lastTask instanceof Task) {
$lastTask->stop($fn);
}
return self::$instance;
}
/**
* Measure the execution time of a callable
*
* @param callable $fn The callable to measure
* @param string $taskName The task name. If not provided, will use 'callable' as default.
* @return float The duration time in milliseconds
*/
public function measure(callable $fn, string $taskName = 'callable'): float
{
// Create task and start
$this->task($taskName)->start();
try {
$fn();
} finally {
$this->stop();
}
// Get the duration from the last task
$lastTask = $this->getActiveTaskable()->getLastTask();
return $lastTask instanceof Task ? $lastTask->getDuration() : 0.0;
}
/**
* @return array
*/
public function getGroups(): array
{
return $this->groups;
}
public function getSummary(): TimeWardenSummary
{
$this->stop();
return new TimeWardenSummary();
}
public function output(): string
{
$this->stop();
/** @var string $output */
$output = '';
/** @var array $columns */
$columns = [
'GROUP',
'TASK',
'DURATION (MS)',
];
/** @var array $rows */
$rows = [];
$totalGroups = 0;
$totalTasks = 0;
$totalDuration = $this->getDuration();
/** @var Task $task */
foreach ($this->getTasks() as $iTask => $task) {
$rows[] = [
($iTask === 0) ? 'default ('.$this->getDuration().' ms)' : '',
$task->name,
$task->getDuration(),
];
$totalTasks++;
}
if ($totalTasks > 0) {
$rows[] = Table::separator();
}
/** @var Group|null $lastIterateGroup */
$lastIterateGroup = null;
/** @var Group $group */
foreach ($this->groups as $iGroup => $group) {
/** @var Task $task */
foreach ($group->getTasks() as $task) {
$rows[] = [
($lastIterateGroup !== $group) ? $group->name.' ('.$group->getDuration().' ms)' : '',
$task->name,
$task->getDuration(),
];
$lastIterateGroup = $group;
$totalTasks++;
}
if ($iGroup !== count($this->groups) - 1) {
$rows[] = Table::separator();
}
$totalDuration += $group->getDuration();
$totalGroups++;
}
$output = (new Table())
->setHeaders($columns)
->setRows($rows)
->setStyle('box-double')
->setFooterTitle('Total: '.$totalDuration.' ms')
->setHeaderTitle('TIMEWARDEN')
->render();
return PHP_EOL.$output;
}
private function getActiveTaskable(): Taskable
{
return $this->getLastGroup() ?? $this;
}
private function getLastGroup(): ?Group
{
/** @var Group|bool $lastGroup */
$lastGroup = end($this->groups);
return ($lastGroup instanceof Group) ? $lastGroup : null;
}
}
================================================
FILE: src/Support/Console/Table.php
================================================
[
'top_left' => '+',
'top_mid' => '+',
'top_right' => '+',
'mid_left' => '+',
'mid_mid' => '+',
'mid_right' => '+',
'bottom_left' => '+',
'bottom_mid' => '+',
'bottom_right' => '+',
'horizontal' => '-',
'vertical' => '|',
],
'box-double' => [
'top_left' => '╔',
'top_mid' => '╦',
'top_right' => '╗',
'mid_left' => '╠',
'mid_mid' => '╬',
'mid_right' => '╣',
'bottom_left' => '╚',
'bottom_mid' => '╩',
'bottom_right' => '╝',
'horizontal' => '═',
'vertical' => '║',
'column_separator' => '║',
'separator_left' => '╠',
'separator_mid' => '╬',
'separator_right' => '╣',
'separator_horizontal' => '═',
],
];
/**
* @var array
*/
private array $headers = [];
/**
* @var array
*/
private array $rows = [];
private string $style = 'default';
private string $headerTitle = '';
private string $footerTitle = '';
public static function separator(): string
{
return self::TABLE_SEPARATOR;
}
/**
* @param array $headers
*/
public function setHeaders(array $headers): self
{
$this->headers = $headers;
return $this;
}
/**
* @param array $rows
*/
public function setRows(array $rows): self
{
$this->rows = $rows;
return $this;
}
public function setStyle(string $style): self
{
$this->style = $style;
return $this;
}
public function setHeaderTitle(string $title): self
{
$this->headerTitle = $title;
return $this;
}
public function setFooterTitle(string $title): self
{
$this->footerTitle = $title;
return $this;
}
public function render(): string
{
if ($this->headers === [] && $this->rows === []) {
return '';
}
$output = [];
$styleChars = self::STYLES[$this->style] ?? self::STYLES['default'];
// Calculate column widths
$columnWidths = $this->calculateColumnWidths();
// Top border
$output[] = $this->renderTopBorder($columnWidths, $styleChars);
// Headers
if ($this->headers !== []) {
$output[] = $this->renderRow($this->headers, $columnWidths, $styleChars);
$output[] = $this->renderHeaderSeparator($columnWidths, $styleChars);
}
// Data rows
foreach ($this->rows as $row) {
if ($row === self::TABLE_SEPARATOR) {
$output[] = $this->renderSeparator($columnWidths, $styleChars);
} elseif (is_array($row)) {
$output[] = $this->renderRow($row, $columnWidths, $styleChars);
}
}
// Bottom border
$output[] = $this->renderBottomBorder($columnWidths, $styleChars);
return implode(PHP_EOL, $output);
}
/**
* @return array
*/
private function calculateColumnWidths(): array
{
$widths = [];
// Initialize with headers
foreach ($this->headers as $i => $header) {
$widths[$i] = mb_strlen((string) $header);
}
// Consider row content
foreach ($this->rows as $row) {
if ($row !== self::TABLE_SEPARATOR && is_array($row)) {
foreach ($row as $i => $cell) {
$cellLength = mb_strlen((string) $cell);
$widths[$i] = max($widths[$i] ?? 0, $cellLength);
}
}
}
return $widths;
}
/**
* @param array $columnWidths
* @param array $styleChars
*/
private function renderTopBorder(array $columnWidths, array $styleChars): string
{
$line = '';
if ($this->headerTitle !== '') {
// Create line with vertical separators crossing the title
$line .= $styleChars['top_left'];
// Calculate separator positions
$currentPos = 0;
$separatorPositions = [];
foreach ($columnWidths as $i => $width) {
$currentPos += $width + 2; // +2 for spaces
if ($i < count($columnWidths) - 1) {
$separatorPositions[] = $currentPos;
$currentPos += 1; // +1 for separator
}
}
$totalWidth = $currentPos;
$titleWithSpaces = ' '.$this->headerTitle.' ';
$titleLength = mb_strlen($titleWithSpaces);
if ($titleLength <= $totalWidth) {
$remainingWidth = $totalWidth - $titleLength;
$leftPadding = (int) ($remainingWidth / 2);
$rightPadding = $remainingWidth - $leftPadding;
// Build line character by character
for ($pos = 0; $pos < $totalWidth; $pos++) {
if (in_array($pos, $separatorPositions)) {
$line .= $styleChars['top_mid'];
} elseif ($pos >= $leftPadding && $pos < $leftPadding + $titleLength) {
$titleIndex = $pos - $leftPadding;
$line .= $titleWithSpaces[$titleIndex] ?? $styleChars['horizontal'];
} else {
$line .= $styleChars['horizontal'];
}
}
} else {
// If title is too long, use normal format
foreach ($columnWidths as $i => $width) {
$line .= str_repeat($styleChars['horizontal'], $width + 2); // +2 for spaces
if ($i < count($columnWidths) - 1) {
$line .= $styleChars['top_mid'];
}
}
}
$line .= $styleChars['top_right'];
} else {
$line .= $styleChars['top_left'];
foreach ($columnWidths as $i => $width) {
$line .= str_repeat($styleChars['horizontal'], $width + 2); // +2 for spaces
if ($i < count($columnWidths) - 1) {
$line .= $styleChars['top_mid'];
}
}
$line .= $styleChars['top_right'];
}
return $line;
}
/**
* @param array $columnWidths
* @param array $styleChars
*/
private function renderHeaderSeparator(array $columnWidths, array $styleChars): string
{
$line = $styleChars['mid_left'];
foreach ($columnWidths as $i => $width) {
$line .= str_repeat($styleChars['horizontal'], $width + 2); // +2 for spaces
if ($i < count($columnWidths) - 1) {
$line .= $styleChars['mid_mid'];
}
}
return $line.$styleChars['mid_right'];
}
/**
* @param array $columnWidths
* @param array $styleChars
*/
private function renderSeparator(array $columnWidths, array $styleChars): string
{
$separatorLeft = $styleChars['separator_left'] ?? $styleChars['mid_left'];
$separatorMid = $styleChars['separator_mid'] ?? $styleChars['mid_mid'];
$separatorRight = $styleChars['separator_right'] ?? $styleChars['mid_right'];
$separatorHorizontal = $styleChars['separator_horizontal'] ?? $styleChars['horizontal'];
$line = $separatorLeft;
foreach ($columnWidths as $i => $width) {
$line .= str_repeat($separatorHorizontal, $width + 2); // +2 for spaces
if ($i < count($columnWidths) - 1) {
$line .= $separatorMid;
}
}
return $line.$separatorRight;
}
/**
* @param array $row
* @param array $columnWidths
* @param array $styleChars
*/
private function renderRow(array $row, array $columnWidths, array $styleChars): string
{
$columnSeparator = $styleChars['column_separator'] ?? $styleChars['vertical'];
$line = $styleChars['vertical']; // Left border (double)
foreach ($columnWidths as $i => $width) {
$cellValue = $row[$i] ?? '';
$cell = is_scalar($cellValue) ? (string) $cellValue : '';
$cellPadding = $width - mb_strlen($cell);
$line .= ' '.$cell.str_repeat(' ', $cellPadding);
if ($i < count($columnWidths) - 1) {
$line .= ' '.$columnSeparator; // Separator between columns
}
} // Right border (double)
return $line.(' '.$styleChars['vertical']);
}
/**
* @param array $columnWidths
* @param array $styleChars
*/
private function renderBottomBorder(array $columnWidths, array $styleChars): string
{
$line = '';
if ($this->footerTitle !== '') {
// Create line with vertical separators crossing the title
$line .= $styleChars['bottom_left'];
// Calculate separator positions
$currentPos = 0;
$separatorPositions = [];
foreach ($columnWidths as $i => $width) {
$currentPos += $width + 2; // +2 for spaces
if ($i < count($columnWidths) - 1) {
$separatorPositions[] = $currentPos;
$currentPos += 1; // +1 for separator
}
}
$totalWidth = $currentPos;
$titleWithSpaces = ' '.$this->footerTitle.' ';
$titleLength = mb_strlen($titleWithSpaces);
if ($titleLength <= $totalWidth) {
$remainingWidth = $totalWidth - $titleLength;
$leftPadding = (int) ($remainingWidth / 2);
$rightPadding = $remainingWidth - $leftPadding;
// Build line character by character
for ($pos = 0; $pos < $totalWidth; $pos++) {
if (in_array($pos, $separatorPositions)) {
$line .= $styleChars['bottom_mid'];
} elseif ($pos >= $leftPadding && $pos < $leftPadding + $titleLength) {
$titleIndex = $pos - $leftPadding;
$line .= $titleWithSpaces[$titleIndex] ?? $styleChars['horizontal'];
} else {
$line .= $styleChars['horizontal'];
}
}
} else {
// If title is too long, use normal format
foreach ($columnWidths as $i => $width) {
$line .= str_repeat($styleChars['horizontal'], $width + 2); // +2 for spaces
if ($i < count($columnWidths) - 1) {
$line .= $styleChars['bottom_mid'];
}
}
}
$line .= $styleChars['bottom_right'];
} else {
$line .= $styleChars['bottom_left'];
foreach ($columnWidths as $i => $width) {
$line .= str_repeat($styleChars['horizontal'], $width + 2); // +2 for spaces
if ($i < count($columnWidths) - 1) {
$line .= $styleChars['bottom_mid'];
}
}
$line .= $styleChars['bottom_right'];
}
return $line;
}
}
================================================
FILE: src/Support/Facades/TimeWarden.php
================================================
getGroups()
* @method static string output()
*
* Taskable methods:
* @method static Task createTask(string $taskName)
* @method static array getTasks(string $taskName)
* @method static Task|null getLastTask()
* @method static float getDuration()
*/
final class TimeWarden
{
/**
* @param array $args
*/
public static function __callStatic(string $method, array $args): mixed
{
$instance = TimeWardenManager::instance();
return $instance->$method(...$args);
}
}
================================================
FILE: src/Support/TimeWardenAlias.php
================================================
hasStarted()) {
/**
* Force garbage collection before timing starts to ensure accurate
* measurements. This prevents random GC cycles from affecting
* benchmark results.
*/
gc_collect_cycles();
$this->startTimestamp = hrtime(true);
}
}
public function stop(?callable $fn = null): void
{
if (! $this->hasEnded()) {
$this->endTimestamp = hrtime(true);
}
if ($fn !== null) {
$fn($this);
}
}
public function onExceedsMilliseconds(float $milliseconds, callable $fn): ?self
{
$this->stop();
if ($this->getDuration() > $milliseconds) {
$fn($this);
}
return $this;
}
public function onExceedsSeconds(float $seconds, callable $fn): ?self
{
$this->stop();
$durationSeconds = $this->getDuration() / 1000;
if ($durationSeconds > $seconds) {
$fn($this);
}
return $this;
}
public function onExceedsMinutes(float $minutes, callable $fn): ?self
{
$this->stop();
$durationMinutes = $this->getDuration() / 1000 / 60;
if ($durationMinutes > $minutes) {
$fn($this);
}
return $this;
}
public function onExceedsHours(float $hours, callable $fn): ?self
{
$this->stop();
$durationHours = $this->getDuration() / 3600000;
if ($durationHours > $hours) {
$fn($this);
}
return $this;
}
public function getFriendlyDuration(): string
{
$durationInMs = $this->getDuration();
$units = [
'day' => 24 * 60 * 60 * 1000,
'h' => 60 * 60 * 1000,
'min' => 60 * 1000,
'sec' => 1000,
'ms' => 1,
];
$timeStrings = [];
foreach ($units as $name => $divisor) {
if ($durationInMs >= $divisor) {
$value = floor($durationInMs / $divisor);
$durationInMs %= $divisor;
$timeStrings[] = $value.$name;
}
}
return $timeStrings !== [] ? implode(' ', $timeStrings) : '0ms';
}
/**
* @return float The duration time in milliseconds
*/
public function getDuration(): float
{
// Convert nanoseconds to milliseconds
return ($this->endTimestamp - $this->startTimestamp) / 1_000_000;
}
public function getTaskable(): ?Taskable
{
return $this->taskable;
}
public function hasStarted(): bool
{
return $this->startTimestamp !== 0;
}
public function hasEnded(): bool
{
return $this->endTimestamp !== 0;
}
/**
* Get the start timestamp in nanoseconds
*/
public function getStartTimestamp(): int
{
return $this->startTimestamp;
}
/**
* Get the end timestamp in nanoseconds
*/
public function getEndTimestamp(): int
{
return $this->endTimestamp;
}
public function getStartDateTime(): ?DateTimeImmutable
{
if ($this->hasStarted()) {
// Convert nanoseconds to seconds for DateTime
$seconds = $this->startTimestamp / 1_000_000_000;
return new DateTimeImmutable('@'.number_format($seconds, 6, '.', ''));
}
return null;
}
public function getEndDateTime(): ?DateTimeImmutable
{
if ($this->hasEnded()) {
// Convert nanoseconds to seconds for DateTime
$seconds = $this->endTimestamp / 1_000_000_000;
return new DateTimeImmutable('@'.number_format($seconds, 6, '.', ''));
}
return null;
}
/**
* Set start timestamp for testing purposes
*
* @param int $nanoseconds Nanoseconds
*/
public function setTestStartTimestamp(int $nanoseconds): void
{
$this->startTimestamp = $nanoseconds;
}
/**
* Set end timestamp for testing purposes
*
* @param int $nanoseconds Nanoseconds
*/
public function setTestEndTimestamp(int $nanoseconds): void
{
$this->endTimestamp = $nanoseconds;
}
/** @return array */
public function toArray(): array
{
/** @var ?DateTimeImmutable $startDateTime */
$startDateTime = $this->getStartDateTime();
/** @var ?DateTimeImmutable $endDateTime */
$endDateTime = $this->getEndDateTime();
return [
'name' => $this->name,
'duration' => $this->getDuration(),
'friendly_duration' => $this->getFriendlyDuration(),
'start_timestamp' => $this->startTimestamp, // nanoseconds
'end_timestamp' => $this->endTimestamp, // nanoseconds
'start_datetime' => ($startDateTime instanceof DateTimeImmutable) ? $startDateTime->format(DateTime::ATOM) : null,
'end_datetime' => ($endDateTime instanceof DateTimeImmutable) ? $endDateTime->format(DateTime::ATOM) : null,
];
}
}
================================================
FILE: src/TimeWardenSummary.php
================================================
*/
public function toArray(): array
{
/** @var array $tasksInfo */
$tasksInfo = [];
if (timeWarden()->getTasks() !== []) {
$tasksInfo[] = timeWarden()->toArray();
}
/** @var Group $group */
foreach (timeWarden()->getGroups() as $group) {
$tasksInfo[] = $group->toArray();
}
return $tasksInfo;
}
public function toJson(): string
{
$json = json_encode($this->toArray());
return ($json === false) ? '[]' : $json;
}
}
================================================
FILE: tests/ArchTest.php
================================================
expect(['dd', 'dump', 'ray', 'die', 'var_dump', 'sleep', 'dispatch', 'dispatch_sync'])
->not->toBeUsed();
arch('contracts')
->expect('Tomloprod\TimeWarden\Contracts')
->toBeInterfaces();
arch('concerns')
->expect('Tomloprod\TimeWarden\Concerns')
->toBeTraits();
================================================
FILE: tests/Contracts/TaskableTest.php
================================================
tasksClass = new class implements Taskable
{
use HasTasks;
public string $name = 'default';
};
});
it('can add a task', function (): void {
$task = $this->tasksClass->createTask('TaskName');
expect($this->tasksClass->getTasks())
->toContain($task);
});
it('can retrieve the last task', function (): void {
$task1 = $this->tasksClass->createTask('TaskName1');
$task2 = $this->tasksClass->createTask('TaskName2');
expect($this->tasksClass->getLastTask())
->toBe($task2);
});
it('returns null when retrieving the last task if there are no tasks', function (): void {
expect($this->tasksClass->getLastTask())
->toBeNull();
});
it('can obtain an array/json', function (): void {
$task1 = $this->tasksClass->createTask('TaskName1');
$task1->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));
$task1->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0190000')));
$task2 = $this->tasksClass->createTask('TaskName2');
$task2->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));
$task2->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0230000')));
$summaryArray = [
'name' => 'default',
'duration' => 42.0,
'tasks' => [
[
'name' => 'TaskName1',
'duration' => 19.0,
'friendly_duration' => '19ms',
'start_timestamp' => 1496664000000000000,
'end_timestamp' => 1496664000019000000,
'start_datetime' => '2017-06-05T12:00:00+00:00',
'end_datetime' => '2017-06-05T12:00:00+00:00',
],
[
'name' => 'TaskName2',
'duration' => 23.0,
'friendly_duration' => '23ms',
'start_timestamp' => 1496664000000000000,
'end_timestamp' => 1496664000023000000,
'start_datetime' => '2017-06-05T12:00:00+00:00',
'end_datetime' => '2017-06-05T12:00:00+00:00',
],
],
];
expect($this->tasksClass->toArray())->toBe($summaryArray);
expect($this->tasksClass->toJson())->toBe(json_encode($summaryArray));
});
================================================
FILE: tests/GroupTest.php
================================================
name)->toBe('GroupName');
});
it('can add a task', function (): void {
$group = new Group('GroupName');
$task = $group->createTask('TaskName');
expect($group->getTasks())->toContain($task);
expect($task->getTaskable())->toBe($group);
});
it('can start the last task if it exists', function (): void {
$group = new Group('GroupName');
$task = $group->createTask('TaskName');
$group->start();
expect($task->hasStarted())->toBeTrue();
});
it('does not start any task if no tasks exist', function (): void {
$group = new Group('GroupName');
$group->start();
expect($group->getLastTask())->toBeNull();
});
================================================
FILE: tests/Services/TimeWardenManagerTest.php
================================================
reset();
});
it('throws exception on clone', function (): void {
$instance = TimeWardenManager::instance();
$closure = fn (): mixed => clone $instance;
expect($closure)->toThrow(Exception::class, 'Cannot clone singleton');
});
it('throws exception on unserialize', function (): void {
$instance = TimeWardenManager::instance();
$closure = fn (): mixed => unserialize(serialize($instance));
expect($closure)->toThrow(Exception::class, 'Cannot unserialize singleton');
});
it('returns the same instance', function (): void {
$instance1 = TimeWardenManager::instance();
$instance2 = TimeWardenManager::instance();
expect($instance1)->toBe($instance2);
});
it('resets the singleton instance', function (): void {
$instance1 = TimeWardenManager::instance();
$instance1->group('Group1');
$instance1->reset();
$instance2 = TimeWardenManager::instance();
$groups = $instance2->getGroups();
expect($groups)
->toBeEmpty()
->not->toBe($instance1);
});
it('can create and retrieve groups', function (): void {
$instance = TimeWardenManager::instance();
$instance->group('Group1')->task('foo');
$instance->group('Group2')->task('bar');
$groups = $instance->getGroups();
expect($groups)->toHaveCount(2);
expect($groups[0])->toBeInstanceOf(Group::class);
expect($groups[1])->toBeInstanceOf(Group::class);
expect($groups[0]->name)->toBe('Group1');
expect($groups[1]->name)->toBe('Group2');
});
it('overwrite last group if doesn\'t have tasks when a new group is created', function (): void {
$instance = TimeWardenManager::instance();
$instance->group('Group1');
$instance->group('Group2');
$instance->group('Group3');
$groups = $instance->getGroups();
expect($groups)->toHaveCount(1);
expect($groups[0]->name)->toBe('Group3');
expect($groups[0])->toBeInstanceOf(Group::class);
});
it('can create tasks of timewarden instance', function (): void {
$instance = TimeWardenManager::instance();
$instance->task('Task1');
$tasks = $instance->getTasks();
expect($tasks)->toHaveCount(1);
expect($tasks[0])->toBeInstanceOf(Task::class);
});
it('can create tasks inside group', function (): void {
$instance = TimeWardenManager::instance();
$instance->group('Group1')->task('Task1');
$tasks = $instance->getGroups()[0]->getTasks();
$timewardenTasks = $instance->getTasks();
expect($tasks)->toHaveCount(1);
expect($tasks[0])->toBeInstanceOf(Task::class);
expect($timewardenTasks)->toHaveCount(0);
});
it('overwrite last task if was never started when a new task is created', function (): void {
$instance = TimeWardenManager::instance();
$instance->task('Task1')->task('Task2');
$tasks = $instance->getTasks();
expect($tasks)->toHaveCount(1);
expect($tasks[0]->name)->toBe('Task2');
expect($tasks[0])->toBeInstanceOf(Task::class);
});
it('stop last task if was never ended when a new task is created', function (): void {
$instance = TimeWardenManager::instance();
$instance->task('Task1')->start()->task('Task2');
$tasks = $instance->getTasks();
expect($tasks)->toHaveCount(2);
// Task 1
expect($tasks[0]->name)->toBe('Task1');
expect($tasks[0]->hasStarted())->toBeTrue();
expect($tasks[0]->hasEnded())->toBeTrue();
expect($tasks[0])->toBeInstanceOf(Task::class);
// Task 2
expect($tasks[1]->name)->toBe('Task2');
expect($tasks[1]->hasStarted())->toBeFalse();
expect($tasks[1]->hasEnded())->toBeFalse();
expect($tasks[1])->toBeInstanceOf(Task::class);
$instance->start();
expect($tasks[1]->hasStarted())->toBeTrue();
expect($tasks[1]->hasEnded())->toBeFalse();
$instance->stop();
expect($tasks[1]->hasStarted())->toBeTrue();
expect($tasks[1]->hasEnded())->toBeTrue();
});
test('output returns tasks and groups', function (): void {
timeWarden()->task('Task 1')->start();
timeWarden()->task('Task 2')->start();
timeWarden()->stop();
timeWarden()->group('Group 1')->task('G1 - Task 1')->start();
timeWarden()->task('G1 - Task 2')->start();
timeWarden()->task('G1 - Task 3')->start();
timeWarden()->group('Group 2')->task('G2 - Task 1')->start();
$output = timeWarden()->output();
expect($output)
->toBeString()
->toContain('default')
->toContain('Task 1')
->toContain('Task 2')
->toContain('G1 - Task 1')
->toContain('G1 - Task 2')
->toContain('G1 - Task 3')
->toContain('Group 1')
->toContain('Group 2')
->toContain('G2 - Task 1');
});
it('can obtain a TimeWardenSummary', function (): void {
$instance = TimeWardenManager::instance();
$instance->task('Task1')->task('Task2');
expect($instance->getSummary())->toBeInstanceOf(TimeWardenSummary::class);
});
it('can measure execution time of a callable with default task name', function (): void {
$instance = TimeWardenManager::instance();
$executed = false;
$duration = $instance->measure(function () use (&$executed): void {
$executed = true;
});
expect($duration)->toBeFloat();
expect($executed)->toBeTrue();
$tasks = $instance->getTasks();
expect($tasks)->toHaveCount(1);
expect($tasks[0]->name)->toBe('callable');
expect($tasks[0])->toBeInstanceOf(Task::class);
});
it('can measure execution time of a callable with custom task name', function (): void {
$instance = TimeWardenManager::instance();
$executed = false;
$duration = $instance->measure(function () use (&$executed): void {
$executed = true;
}, 'custom-task');
expect($duration)->toBeFloat();
expect($executed)->toBeTrue();
$tasks = $instance->getTasks();
expect($tasks)->toHaveCount(1);
expect($tasks[0]->name)->toBe('custom-task');
expect($tasks[0])->toBeInstanceOf(Task::class);
});
it('can measure execution time inside a group', function (): void {
$instance = TimeWardenManager::instance();
$instance->group('Test Group');
$executed = false;
$duration = $instance->measure(function () use (&$executed): void {
$executed = true;
}, 'group-task');
expect($duration)->toBeFloat();
expect($executed)->toBeTrue();
$groups = $instance->getGroups();
expect($groups)->toHaveCount(1);
$tasks = $groups[0]->getTasks();
expect($tasks)->toHaveCount(1);
expect($tasks[0]->name)->toBe('group-task');
expect($tasks[0])->toBeInstanceOf(Task::class);
// TimeWarden instance should have no tasks (they're in the group)
expect($instance->getTasks())->toHaveCount(0);
});
it('ensures task is stopped even if callable throws exception', function (): void {
$instance = TimeWardenManager::instance();
$exceptionThrown = false;
try {
$instance->measure(function (): void {
throw new Exception('Test exception');
}, 'exception-task');
} catch (Exception $e) {
$exceptionThrown = true;
expect($e->getMessage())->toBe('Test exception');
}
expect($exceptionThrown)->toBeTrue();
$tasks = $instance->getTasks();
expect($tasks)->toHaveCount(1);
expect($tasks[0]->name)->toBe('exception-task');
expect($tasks[0])->toBeInstanceOf(Task::class);
});
================================================
FILE: tests/Support/Facades/TimeWardenTest.php
================================================
toBe($instance2);
});
it('can create tasks using TimeWarden facade', function (): void {
TimeWarden::task('Task1');
$tasks = TimeWarden::instance()->getTasks();
expect($tasks)->toHaveCount(1);
expect($tasks[0])->toBeInstanceOf(Task::class);
});
================================================
FILE: tests/Support/TimeWardenAliasTest.php
================================================
toBeInstanceOf(TimeWardenManager::class);
});
test('timewarden (w lowercase) alias return instance of TimeWarden', function (): void {
expect(timewarden())
->toBeInstanceOf(TimeWardenManager::class);
});
================================================
FILE: tests/TaskTest.php
================================================
name)->toBe('TaskName');
});
it('starts and stops task', function (): void {
$task = new Task('TaskName');
expect($task->hasStarted())->toBeFalse();
expect($task->hasEnded())->toBeFalse();
$task->start();
expect($task->hasStarted())->toBeTrue();
expect($task->hasEnded())->toBeFalse();
$task->stop();
expect($task->hasStarted())->toBeTrue();
expect($task->hasEnded())->toBeTrue();
});
it('stop task with callable when does not exceed execution time', function (): void {
$task = new Task('TaskName');
$task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));
$task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0190000')));
/** @var bool $callableIsExecuted */
$callableIsExecuted = false;
$task->stop(static function (Task $task) use (&$callableIsExecuted): void {
$task->onExceedsMilliseconds(20, static function () use (&$callableIsExecuted): void {
$callableIsExecuted = true;
});
});
expect($task->getDuration())->toBeLessThan(20);
expect($callableIsExecuted)->toBeFalse();
});
it('stop task with callable when exceeds execution time', function (): void {
$task = new Task('TaskName');
$task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));
$task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0210000')));
/** @var bool $callableIsExecuted */
$callableIsExecuted = false;
$task->stop(static function (Task $task) use (&$callableIsExecuted): void {
$task->onExceedsMilliseconds(20, static function () use (&$callableIsExecuted): void {
$callableIsExecuted = true;
});
});
expect($task->getDuration())->toBe((float) 21);
expect($callableIsExecuted)->toBeTrue();
});
it('calculates duration correctly (without usleep)', function (): void {
$task = new Task('TaskName');
$task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.000000')));
$task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.020000')));
expect($task->getDuration())
->toBeGreaterThanOrEqual(20)
->toBeLessThanOrEqual(21);
});
it('calculates duration correctly (with usleep)', function (): void {
$task = new Task('TaskName');
$task->start();
// Sleep 7ms.
usleep(7 * 1000);
$task->stop();
expect($task->getDuration())
->toBeGreaterThanOrEqual(7)
->toBeLessThanOrEqual(8);
})->onlyOnLinux();
it('returns duration as 0 if not started or not stopped', function (): void {
$task = new Task('TaskName');
$duration = $task->getDuration();
expect($duration)->toBe(0.0);
});
test('getFriendlyDuration', function (): void {
$task = new Task('Task');
$task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.000000')));
$task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 13:10:20.030000')));
expect($task->getFriendlyDuration())->toContain('1h 10min 20sec 30ms');
});
/**
* Convert DateTime to nanoseconds for testing
*
* @return int Nanoseconds
*/
function dateTimeToTimestamp(DateTimeImmutable $datetime): int
{
// Convert DateTime to nanoseconds for testing
$seconds = $datetime->getTimestamp();
$microseconds = (int) $datetime->format('u');
return ($seconds * 1_000_000_000) + ($microseconds * 1000);
}
test('onExceedsMilliseconds (exceeds test)', function (): void {
$task = new Task('Task');
$task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.1000000')));
$task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.102000')));
/** @var bool $timeExceeds */
$timeExceeds = false;
$task->onExceedsMilliseconds(1, static function () use (&$timeExceeds): void {
$timeExceeds = true;
});
expect($timeExceeds)->toBeTrue();
});
test('onExceedsMilliseconds (does not exceeds test)', function (): void {
$task2 = new Task('Task');
$task2->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.1000000')));
$task2->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.101000')));
/** @var bool $timeExceeds */
$timeExceeds = false;
$task2->onExceedsMilliseconds(1, static function () use (&$timeExceeds): void {
$timeExceeds = true;
});
expect($timeExceeds)->toBeFalse();
});
test('onExceedsSeconds (exceeds test)', function (): void {
$task = new Task('Task');
$task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));
$task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:10.0000000')));
/** @var bool $timeExceeds */
$timeExceeds = false;
$task->onExceedsSeconds(9, static function () use (&$timeExceeds): void {
$timeExceeds = true;
});
expect($timeExceeds)->toBeTrue();
});
test('onExceedsSeconds (does not exceeds test)', function (): void {
$task2 = new Task('Task');
$task2->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));
$task2->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:10.0000000')));
/** @var bool $timeExceeds */
$timeExceeds = false;
$task2->onExceedsSeconds(10, static function () use (&$timeExceeds): void {
$timeExceeds = true;
});
expect($timeExceeds)->toBeFalse();
});
test('onExceedsMinutes (exceeds test)', function (): void {
$task = new Task('Task 1 exceeds execution time');
$task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));
$task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:10:00.0000000')));
/** @var bool $timeExceeds */
$timeExceeds = false;
$task->onExceedsMinutes(9, static function () use (&$timeExceeds): void {
$timeExceeds = true;
});
expect($timeExceeds)->toBeTrue();
});
test('onExceedsMinutes (does not exceeds test)', function (): void {
$task2 = new Task('Task');
$task2->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));
$task2->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:10:00.0000000')));
/** @var bool $timeExceeds */
$timeExceeds = false;
$task2->onExceedsMinutes(10, static function () use (&$timeExceeds): void {
$timeExceeds = true;
});
expect($timeExceeds)->toBeFalse();
});
test('onExceedsHours (exceeds test)', function (): void {
$task = new Task('Task');
$task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.000000')));
$task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 14:01:15.000000')));
/** @var bool $timeExceeds */
$timeExceeds = false;
$task->onExceedsHours(2, static function () use (&$timeExceeds): void {
$timeExceeds = true;
});
expect($timeExceeds)->toBeTrue();
});
test('onExceedsHours (does not exceeds test)', function (): void {
$task2 = new Task('Task');
$task2->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.000000')));
$task2->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:01:15.000000')));
/** @var bool $timeExceeds */
$timeExceeds = false;
$task2->onExceedsHours(2, static function () use (&$timeExceeds): void {
$timeExceeds = true;
});
expect($timeExceeds)->toBeFalse();
});
test('getTaskable', function (): void {
$group = new Group('GroupName');
$task = new Task('TaskName', $group);
expect($task->getTaskable())->toBe($group);
});
test('getters start and end timestamp', function (): void {
$task = new Task('Task getStartTimestamp');
$startTimestamp = dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.000000'));
$endTimestamp = dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.000000'));
$task->setTestStartTimestamp($startTimestamp);
$task->setTestEndTimestamp($endTimestamp);
expect($task->getStartTimestamp())->toBe($startTimestamp);
expect($task->getEndTimestamp())->toBe($endTimestamp);
});
test('getStartDateTime return DateTime on started tasks', function (): void {
$task = new Task('Task getStartDateTime -> DateTime');
$date = '2017-06-05 12:00:00.000000';
$startTimestamp = dateTimeToTimestamp(new DateTimeImmutable($date));
$task->setTestStartTimestamp($startTimestamp);
$startDateTime = $task->getStartDateTime();
expect($startDateTime)->toBeInstanceOf(DateTimeImmutable::class);
expect($startDateTime->format('Y-m-d H:i:s.u'))->toBe($date);
});
test('getStartDateTime return null on non started tasks', function (): void {
$task = new Task('Task getStartDateTime -> null');
$endDateTime = $task->getStartDateTime();
expect($endDateTime)->toBeNull();
});
test('getEndDateTime returns DateTime on ended tasks', function (): void {
$task = new Task('Task getEndDateTime -> DateTime');
$date = '2017-06-05 12:00:00.000000';
$timestamp = dateTimeToTimestamp(new DateTimeImmutable($date));
$task->setTestStartTimestamp($timestamp);
$task->setTestEndTimestamp($timestamp);
$endDateTime = $task->getEndDateTime();
expect($endDateTime)->toBeInstanceOf(DateTimeImmutable::class);
expect($endDateTime->format('Y-m-d H:i:s.u'))->toBe($date);
});
test('getEndDateTime returns null on non started tasks', function (): void {
$task = new Task('Task getEndDateTime -> null');
$endDateTime = $task->getEndDateTime();
expect($endDateTime)->toBeNull();
});
================================================
FILE: tests/TimeWardenSummaryTest.php
================================================
reset();
timeWarden()->task('Generic Task')->start()->stop();
$task = timeWarden()->getTasks()[0];
$task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));
$task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));
timeWarden()->group('Group1')->task('TaskName1')->start()->stop();
$groupTask = timeWarden()->getGroups()[0]->getTasks()[0];
$groupTask->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));
$groupTask->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0320000')));
/** @var TimeWardenSummary $summary */
$summary = timeWarden()->getSummary();
$summaryArray = [
[
'name' => 'default',
'duration' => 0.0,
'tasks' => [
[
'name' => 'Generic Task',
'duration' => 0.0,
'friendly_duration' => '0ms',
'start_timestamp' => 1496664000000000000,
'end_timestamp' => 1496664000000000000,
'start_datetime' => '2017-06-05T12:00:00+00:00',
'end_datetime' => '2017-06-05T12:00:00+00:00',
],
],
],
[
'name' => 'Group1',
'duration' => 32.0,
'tasks' => [
[
'name' => 'TaskName1',
'duration' => 32.0,
'friendly_duration' => '32ms',
'start_timestamp' => 1496664000000000000,
'end_timestamp' => 1496664000032000000,
'start_datetime' => '2017-06-05T12:00:00+00:00',
'end_datetime' => '2017-06-05T12:00:00+00:00',
],
],
],
];
expect($summary->toArray())->toBe($summaryArray);
expect($summary->toJson())->toBe(json_encode($summaryArray));
});