diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..26fa993 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +; 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 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3ee7ee0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +/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 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..a1f77c4 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: https://www.paypal.com/paypalme/tomloprod diff --git a/.github/workflows/formats.yml b/.github/workflows/formats.yml new file mode 100644 index 0000000..393394e --- /dev/null +++ b/.github/workflows/formats.yml @@ -0,0 +1,49 @@ +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 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..50a3090 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,45 @@ +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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f288c8c --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/.phpunit.result.cache +/.phpunit.cache +/.php-cs-fixer.cache +/.php-cs-fixer.php +/composer.lock +/phpunit.xml +/vendor/ +*.swp +*.swo \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..764cea5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +## Version 1.0.0 +> XX May, 2024 + +- First TimeWarden version diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5e743aa --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,38 @@ +# πŸ§‘β€πŸ€β€πŸ§‘ 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 +``` diff --git a/LICENSE.md b/LICENSE.md new file mode 100755 index 0000000..d3fa7fc --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b80eae5 --- /dev/null +++ b/README.md @@ -0,0 +1,270 @@ +

+

+ GitHub Workflow Status (master) + Total Downloads + Latest Version + License +

+

+ +------ +## ⏱️ **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 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 () { ... }); +``` + +### 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... +} + +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(); + +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 + +// Obtains all the created groups +TimeWarden::getGroups(): array + +// 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 + +$task->getStartTimestamp(): float +$task->getEndTimestamp(): float + +// Reactive execution time methods +$task->onExceedsMilliseconds(float $milliseconds, callable $fn): ?self +$task->onExceedsSeconds(float $seconds, callable $fn): ?self +$task->onExceedsMinutes(float $minutes, callable $fn): ?self +$task->onExceedsHours(float $hours, callable $fn): ?self +``` + +### `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; + +// Remove the last task from the taskable and add another in its place. +$taskable->replaceLastTask(Task $task): void; + +$taskable->getTasks(): array; + +$taskable->getLastTask(): ?Task; + +// Return the total time in milliseconds of all tasks within the taskable. +$taskable->getDuration(): float; +``` + +## **πŸš€ 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)**. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..33a0718 --- /dev/null +++ b/composer.json @@ -0,0 +1,69 @@ +{ + "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", + "symfony/console": "^7.0" + }, + "require-dev": { + "laravel/pint": "^1.15.2", + "pestphp/pest": "^2.34", + "pestphp/pest-plugin-type-coverage": "^2.8", + "rector/rector": "^1.0.4" + }, + "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" + ], + "timewarden:start": "Tomloprod\\TimeWarden\\Console\\OutputCommand::start" + } +} \ No newline at end of file diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..5fd25fc --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,6 @@ +parameters: + level: max + paths: + - src + + reportUnmatchedIgnoredErrors: true diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..35ce165 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,16 @@ + + + + + ./src + + + + + ./tests + + + diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..e437ddf --- /dev/null +++ b/pint.json @@ -0,0 +1,58 @@ +{ + "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 + } +} \ No newline at end of file diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..0c54648 --- /dev/null +++ b/rector.php @@ -0,0 +1,24 @@ +withPaths([ + __DIR__.'/src', + __DIR__.'/tests', + ]) + ->withSkip([ + AddOverrideAttributeToOverriddenMethodsRector::class, + ]) + ->withPreparedSets( + deadCode: true, + codeQuality: true, + typeDeclarations: true, + privatization: true, + earlyReturn: true, + strictBooleans: true, + ) + ->withPhpSets(); diff --git a/src/Concerns/HasTasks.php b/src/Concerns/HasTasks.php new file mode 100644 index 0000000..3fedc8c --- /dev/null +++ b/src/Concerns/HasTasks.php @@ -0,0 +1,62 @@ + + */ + private array $tasks = []; + + public function createTask(string $taskName): Task + { + $task = new Task($taskName, $this); + + $this->tasks[] = $task; + + return $task; + } + + public function replaceLastTask(Task $task): void + { + array_pop($this->tasks); + + $this->tasks[] = $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 > 0) ? round($duration, 2) : 0.0; + } + + public function getLastTask(): ?Task + { + /** @var Task|bool $lastTask */ + $lastTask = end($this->tasks); + + return ($lastTask instanceof Task) ? $lastTask : null; + } +} diff --git a/src/Contracts/Taskable.php b/src/Contracts/Taskable.php new file mode 100644 index 0000000..1cd2463 --- /dev/null +++ b/src/Contracts/Taskable.php @@ -0,0 +1,23 @@ + + */ + public function getTasks(): array; + + public function getLastTask(): ?Task; + + public function getDuration(): float; +} diff --git a/src/Group.php b/src/Group.php new file mode 100644 index 0000000..cc26cff --- /dev/null +++ b/src/Group.php @@ -0,0 +1,27 @@ +getLastTask(); + + if ($lastTask instanceof Task) { + $lastTask->start(); + } + } +} diff --git a/src/Services/TimeWardenManager.php b/src/Services/TimeWardenManager.php new file mode 100644 index 0000000..e0e02e0 --- /dev/null +++ b/src/Services/TimeWardenManager.php @@ -0,0 +1,226 @@ + + */ + 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(); + + /** @todo do the same as task(). overwrite empty groups, avoid groups with same name (in this case... active group with name?) */ + $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 overwrite it + if ($lastTask instanceof Task && ! $lastTask->hasStarted()) { + $taskable->replaceLastTask(new Task($taskName, $taskable)); + + } 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; + } + + /** + * @return array + */ + public function getGroups(): array + { + return $this->groups; + } + + 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[] = new TableSeparator(); + } + + /** @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[] = new TableSeparator(); + } + + $totalDuration += $group->getDuration(); + $totalGroups++; + } + + // Footer + //$rows[] = new TableSeparator(); + //$rows[] = ['NΒΊ groups', 'NΒΊ tasks', 'Total duration']; + //$rows[] = [$totalGroups, $totalTasks, $totalDuration]; + //$rows[] = ['', '', 'Total ' . $totalDuration]; + + $output = new BufferedOutput(); + $table = new Table($output); + + $table + ->setHeaders($columns) + ->setRows($rows) + ->setStyle('box-double') + // ->setFooterTitle('Thanks for using TimeWarden') + ->setFooterTitle('Total: '.round($totalDuration, 2).' ms') + + ->setHeaderTitle('TIMEWARDEN'); + + $table->render(); + + $output = $output->fetch(); + + return "\n".$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; + } +} diff --git a/src/Support/Facades/TimeWarden.php b/src/Support/Facades/TimeWarden.php new file mode 100644 index 0000000..5cef184 --- /dev/null +++ b/src/Support/Facades/TimeWarden.php @@ -0,0 +1,38 @@ + getGroups() + * @method static string output() + * + * Taskable methods: + * @method static Task createTask(string $taskName) + * @method static void replaceLastTask(Task $task) + * @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); + } +} diff --git a/src/Support/TimeWardenAlias.php b/src/Support/TimeWardenAlias.php new file mode 100644 index 0000000..f104334 --- /dev/null +++ b/src/Support/TimeWardenAlias.php @@ -0,0 +1,19 @@ +hasStarted()) { + $this->startTimestamp = (float) microtime(true); + } + } + + public function stop(?callable $fn = null): void + { + if (! $this->hasEnded()) { + $this->endTimestamp = (float) microtime(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 + { + $duration = ($this->endTimestamp - $this->startTimestamp) * 1000; + + return ($duration > 0) ? round($duration, 2) : 0.0; + } + + public function getTaskable(): ?Taskable + { + return $this->taskable; + } + + public function hasStarted(): bool + { + return ((int) $this->startTimestamp) !== 0; + } + + public function hasEnded(): bool + { + return ((int) $this->endTimestamp) !== 0; + } + + public function getStartTimestamp(): float + { + return $this->startTimestamp; + } + + public function getEndTimestamp(): float + { + return $this->endTimestamp; + } + + public function getStartDateTime(): ?DateTimeImmutable + { + if ($this->hasStarted()) { + return new DateTimeImmutable('@'.$this->startTimestamp); + } + + return null; + } + + public function getEndDateTime(): ?DateTimeImmutable + { + if ($this->hasEnded()) { + return new DateTimeImmutable('@'.$this->endTimestamp); + } + + return null; + } + + public function setTestStartTimestamp(float $microtime): void + { + $this->startTimestamp = $microtime; + } + + public function setTestEndTimestamp(float $microtime): void + { + $this->endTimestamp = $microtime; + } +} diff --git a/tests/ArchTest.php b/tests/ArchTest.php new file mode 100644 index 0000000..552ccf2 --- /dev/null +++ b/tests/ArchTest.php @@ -0,0 +1,15 @@ +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(); diff --git a/tests/Contracts/TaskableTest.php b/tests/Contracts/TaskableTest.php new file mode 100644 index 0000000..af27b41 --- /dev/null +++ b/tests/Contracts/TaskableTest.php @@ -0,0 +1,48 @@ +tasksClass = new class implements Taskable + { + use HasTasks; + }; +}); + +it('can add a task', function (): void { + $task = $this->tasksClass->createTask('TaskName'); + + expect($this->tasksClass->getTasks()) + ->toContain($task); +}); + +it('can replace the last task', function (): void { + $task1 = $this->tasksClass->createTask('TaskName1'); + + $task2 = new Task('TaskName2', $this->tasksClass); + + $this->tasksClass->replaceLastTask($task2); + + expect($this->tasksClass->getTasks()) + ->not->toContain($task1); + + expect($this->tasksClass->getTasks()) + ->toContain($task2); +}); + +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(); +}); diff --git a/tests/GroupTest.php b/tests/GroupTest.php new file mode 100644 index 0000000..3b9d3ab --- /dev/null +++ b/tests/GroupTest.php @@ -0,0 +1,57 @@ +name) + ->toBe('GroupName'); +}); + +it('can add a task', function (): void { + $group = new Group('GroupName'); + $task = $group->createTask('TaskName'); + + expect($group->getTasks()) + ->toContain($task); + + /** @todo comprobar que task tenga el grupo (taskable) */ +}); + +it('can replace the last task', function (): void { + $group = new Group('GroupName'); + + $task1 = $group->createTask('TaskName1'); + $task2 = new Task('TaskName2', $group); + + $group->replaceLastTask($task2); + + expect($group->getTasks()) + ->not->toContain($task1); + + expect($group->getTasks()) + ->toContain($task2); +}); + +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(); +}); diff --git a/tests/Services/TimeWardenManagerTest.php b/tests/Services/TimeWardenManagerTest.php new file mode 100644 index 0000000..da6f20d --- /dev/null +++ b/tests/Services/TimeWardenManagerTest.php @@ -0,0 +1,177 @@ +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'); + $instance->group('Group2'); + + $groups = $instance->getGroups(); + + expect($groups)->toHaveCount(2); + + expect($groups[0])->toBeInstanceOf(Group::class); + + expect($groups[0]->name)->toBe('Group1'); + + expect($groups[1]->name)->toBe('Group2'); +}); + +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'); +}); diff --git a/tests/Support/Facades/TimeWardenTest.php b/tests/Support/Facades/TimeWardenTest.php new file mode 100644 index 0000000..dbe92b7 --- /dev/null +++ b/tests/Support/Facades/TimeWardenTest.php @@ -0,0 +1,28 @@ +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); +}); diff --git a/tests/Support/TimeWardenAliasTest.php b/tests/Support/TimeWardenAliasTest.php new file mode 100644 index 0000000..cafa919 --- /dev/null +++ b/tests/Support/TimeWardenAliasTest.php @@ -0,0 +1,15 @@ +toBeInstanceOf(TimeWardenManager::class); +}); + +test('timewarden (w lowercase) alias return instance of TimeWarden', function (): void { + expect(timewarden()) + ->toBeInstanceOf(TimeWardenManager::class); +}); diff --git a/tests/TaskTest.php b/tests/TaskTest.php new file mode 100644 index 0000000..0546ba9 --- /dev/null +++ b/tests/TaskTest.php @@ -0,0 +1,300 @@ +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'); +}); + +function dateTimeToTimestamp(DateTimeImmutable $datetime): float +{ + return (float) $datetime->getTimestamp() + ((int) $datetime->format('u') / 1000000); +} + +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(); +});