[
  {
    "path": ".editorconfig",
    "content": "; This file is for unifying the coding style for different editors and IDEs.\n; More information at http://editorconfig.org\n\nroot = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 4\ntrim_trailing_whitespace = true\n\n[*.md]\ntrim_trailing_whitespace = false\n\n[*.yml]\nindent_size = 2"
  },
  {
    "path": ".gitattributes",
    "content": "/docs              export-ignore\n/tests             export-ignore\n/scripts           export-ignore\n/.github           export-ignore\n/.php_cs           export-ignore\n.editorconfig      export-ignore\n.gitattributes     export-ignore\n.gitignore         export-ignore\nphpstan.neon.dist  export-ignore\nphpunit.xml.dist   export-ignore\nrector.php         export-ignore\nCHANGELOG.md       export-ignore\nCONTRIBUTING.md    export-ignore\nREADME.md          export-ignore\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "custom: https://www.paypal.com/paypalme/tomloprod\n"
  },
  {
    "path": ".github/workflows/formats.yml",
    "content": "name: Formats\n\non: ['push', 'pull_request']\n\njobs:\n  ci:\n    runs-on: ${{ matrix.os }}\n\n    strategy:\n      fail-fast: true\n      matrix:\n        os: [ubuntu-latest]\n        php: [8.2]\n        dependency-version: [prefer-lowest, prefer-stable]\n\n    name: Formats P${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }}\n\n    steps:\n\n    - name: Checkout\n      uses: actions/checkout@v3\n\n    - name: Setup PHP\n      uses: shivammathur/setup-php@v2\n      with:\n        php-version: ${{ matrix.php }}\n        extensions: dom, mbstring, zip\n        coverage: pcov\n\n    - name: Get Composer cache directory\n      id: composer-cache\n      shell: bash\n      run: echo \"dir=$(composer config cache-files-dir)\" >> $GITHUB_OUTPUT\n\n    - name: Cache dependencies\n      uses: actions/cache@v3\n      with:\n        path: ${{ steps.composer-cache.outputs.dir }}\n        key: dependencies-php-${{ matrix.php }}-os-${{ matrix.os }}-version-${{ matrix.dependency-version }}-composer-${{ hashFiles('composer.json') }}\n        restore-keys: dependencies-php-${{ matrix.php }}-os-${{ matrix.os }}-version-${{ matrix.dependency-version }}-composer-\n\n    - name: Install Composer dependencies\n      run: composer update --${{ matrix.dependency-version }} --no-interaction --prefer-dist\n\n    - name: Coding Style Checks\n      run: composer test:lint\n\n    - name: Type Checks\n      run: composer test:types\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: Tests\n\non: ['push', 'pull_request']\n\njobs:\n  ci:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      fail-fast: true\n      matrix:\n        os: [ubuntu-latest, macos-latest, windows-latest]\n        php: [8.2, 8.3]\n        dependency-version: [prefer-lowest, prefer-stable]\n\n    name: Tests P${{ matrix.php }} - ${{ matrix.os }} - ${{ matrix.dependency-version }}\n\n    steps:\n\n    - name: Checkout\n      uses: actions/checkout@v3\n\n    - name: Setup PHP\n      uses: shivammathur/setup-php@v2\n      with:\n        php-version: ${{ matrix.php }}\n        extensions: dom, mbstring, zip\n        coverage: none\n\n    - name: Get Composer cache directory\n      id: composer-cache\n      shell: bash\n      run: echo \"dir=$(composer config cache-files-dir)\" >> $GITHUB_OUTPUT\n\n    - name: Cache dependencies\n      uses: actions/cache@v3\n      with:\n        path: ${{ steps.composer-cache.outputs.dir }}\n        key: dependencies-php-${{ matrix.php }}-os-${{ matrix.os }}-version-${{ matrix.dependency-version }}-composer-${{ hashFiles('composer.json') }}\n        restore-keys: dependencies-php-${{ matrix.php }}-os-${{ matrix.os }}-version-${{ matrix.dependency-version }}-composer-\n\n    - name: Install Composer dependencies\n      run: composer update --${{ matrix.dependency-version }} --no-interaction --prefer-dist\n\n    - name: Integration Tests\n      run: php ./vendor/bin/pest\n"
  },
  {
    "path": ".gitignore",
    "content": "/.phpunit.result.cache\n/.phpunit.cache\n/.php-cs-fixer.cache\n/.php-cs-fixer.php\n/composer.lock\n/phpunit.xml\n/vendor/\n*.swp\n*.swo"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## Version 1.0.1\n> 20 May, 2024\n\n- test: created task belongs to group (`getTaskable()`) by @tomloprod in https://github.com/tomloprod/time-warden/pull/1\n- ref: replace task substitution system by @tomloprod in https://github.com/tomloprod/time-warden/pull/2\n\n## Version 1.0.0\n> 20 May, 2024\n\n- First TimeWarden version\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# 🧑‍🤝‍🧑 Contributing\n\nContributions are welcome, and are accepted via pull requests.\nPlease review these guidelines before submitting any pull requests.\n\n## Process\n\n1. Fork the project\n1. Create a new branch\n1. Code, test, commit and push\n1. Open a pull request detailing your changes.\n\n## Guidelines\n\nTime 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.\n\nIn 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.\n\nYou run these tools individually using the following commands:\n\n```bash\n# Lint the code using Pint\ncomposer lint\ncomposer test:lint\n\n# Refactor the code using Rector\ncomposer refactor\ncomposer test:refactor\n\n# Run PHPStan\ncomposer test:types\n\n# Run the test suite\ncomposer test:unit\n\n# Run all the tools\ncomposer test\n```\n"
  },
  {
    "path": "LICENSE.md",
    "content": "The MIT License (MIT)\n\nCopyright (c) Tomás López <tomloprod@gmail.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n    <p align=\"center\">\n        <a href=\"https://github.com/tomloprod/time-warden/actions\"><img alt=\"GitHub Workflow Status (master)\" src=\"https://github.com/tomloprod/time-warden/actions/workflows/tests.yml/badge.svg\"></a>\n        <a href=\"https://packagist.org/packages/tomloprod/time-warden\"><img alt=\"Total Downloads\" src=\"https://img.shields.io/packagist/dt/tomloprod/time-warden\"></a>\n        <a href=\"https://packagist.org/packages/tomloprod/time-warden\"><img alt=\"Latest Version\" src=\"https://img.shields.io/packagist/v/tomloprod/time-warden\"></a>\n        <a href=\"https://packagist.org/packages/tomloprod/time-warden\"><img alt=\"License\" src=\"https://img.shields.io/packagist/l/tomloprod/time-warden\"></a>\n    </p>\n</p>\n\n------\n## ⏱️ **About TimeWarden**\n\nTimeWarden 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.\n\nTimeWarden uses **high-resolution timing** (`hrtime`) for **nanosecond precision**, ensuring accurate measurements even for very fast operations.\n\nTimeWarden 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.\n\n## **✨ Getting Started**\n\n### Reactive Actions\nYou 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.*).\n\n#### Example\n```php\ntimeWarden()->task('Checking articles')->start();\n\nforeach ($articles as $article) {\n    // Perform long process... 🕒 \n}\n\n// Using traditional anonymous function\ntimeWarden()->stop(static function (Task $task): void {\n    $task->onExceedsMilliseconds(500, static function (Task $task): void {\n        // Do what you need, for example, send an email 🙂\n        Mail::to('foo@bar.com')->queue(\n            new SlowArticleProcess($task)\n        );\n    });\n});\n\n// Or using an arrow function\ntimeWarden()->stop(static function (Task $task): void {\n    $task->onExceedsMilliseconds(500, fn (Task $task) => Log::error($task->name.' has taken too long'));\n});\n```\n\n#### Available methods\n\nIf you're not convinced about using `onExceedsMilliseconds`, you have other options:\n```php\n$task->onExceedsSeconds(10, function () { ... });\n$task->onExceedsMinutes(5, function () { ... });\n$task->onExceedsHours(2, function () { ... });\n```\n\n### Quick Measurement\nTimeWarden 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.\n\n#### Example\n```php\n// Simple measurement\n$duration = timeWarden()->measure(function() {\n    // Your code here\n    sleep(1);\n    processData();\n});\n\necho \"Execution took: {$duration} ms\";\n\n// With custom task name\n$duration = timeWarden()->measure(function() {\n    // Your code here\n    processArticles();\n}, 'Processing Articles');\n\n// The task will appear in your TimeWarden output with the specified name\necho timeWarden()->output();\n```\n\nThe `measure()` method:\n- Automatically creates a task with the provided name (or 'callable' by default)\n- Starts timing before execution\n- Executes your callable\n- Stops timing after execution (even if an exception occurs)\n- Returns the duration in milliseconds\n- Integrates with groups and the TimeWarden workflow\n\n### Execution Time Debugging\nIt allows you to measure the execution time of tasks in your application, as well as the possibility of adding those tasks to a group.\n\n#### Simple tasks\n\n```php\ntimeWarden()->task('Articles task');\n\nforeach ($articles as $article) {\n    // Perform long process...\n}\n\n// Previous task is automatically stopped when a new task is created\ntimeWarden()->task('Customers task');\n\nforeach ($customers as $customer) {\n    // Perform long process...\n}\n\n/**\n * You can print the results directly or obtain a \n * summary with the `getSummary()` method\n */\necho timeWarden()->output();\n```\n**Result:**\n```log\n╔═════════════════════ TIMEWARDEN ═════╤═══════════════╗\n║ GROUP               │ TASK           │ DURATION (MS) ║\n╠═════════════════════╪════════════════╪═══════════════╣\n║ default (320.37 ms) │ Articles task  │ 70.23         ║\n║                     │ Customers task │ 250.14        ║\n╚══════════════════ Total: 320.37 ms ══╧═══════════════╝\n```\n\n#### Grouped tasks\n\n```php\ntimeWarden()->group('Articles')->task('Loop of articles')->start();\n\nforeach ($articles as $article) {\n    // Perform first operations\n}\n\ntimeWarden()->task('Other articles process')->start();\nFoo::bar();\n\n// Previous task is automatically stopped when a new task is created\ntimeWarden()->group('Customers')->task('Customers task')->start();\n\nforeach ($customers as $customer) {\n    // Perform long process...\n}\n\ntimeWarden()->task('Other customer process')->start();\nBar::foo();\n\n/**\n * You can print the results directly or obtain a \n * summary with the `getSummary()` method\n */\necho timeWarden()->output();\n```\n**Result:**\n```log\n╔═══════════════════════╤══ TIMEWARDEN ══════════╤═══════════════╗\n║ GROUP                 │ TASK                   │ DURATION (MS) ║\n╠═══════════════════════╪════════════════════════╪═══════════════╣\n║ Articles (85.46 ms)   │ Loop of articles       │ 70.24         ║\n║                       │ Other articles process │ 15.22         ║\n╟───────────────────────┼────────────────────────┼───────────────╢\n║ Customers (280.46 ms) │ Customers task         │ 250.22        ║\n║                       │ Other customer process │ 30.24         ║\n╚═══════════════════════ Total: 365.92 ms ═══════╧═══════════════╝\n```\n\n#### 🧙 Tip\n\nIf your application has any logging system, it would be a perfect place to send the output. \n```php \nif (app()->environment('local')) {\n    Log::debug(timeWarden()->output());\n}\n```\n\n### Ways of using TimeWarden\nYou can use TimeWarden either with the aliases `timeWarden()` (or `timewarden()`):\n```php\ntimeWarden()->task('Task 1')->start();\n```\n\nor by directly invoking the static methods of the `TimeWarden` facade:\n\n```php\nTimeWarden::task('Task 1')->start();\n```\nYou decide how to use it 🙂\n\n## **🧱 Architecture**\nTimeWarden is composed of several types of elements. Below are some features of each of these elements.\n\n### `TimeWarden`\n\n`Tomloprod\\TimeWarden\\Support\\Facades\\TimeWarden` is a facade that acts as a simplified interface for using the rest of the TimeWarden elements.\n\n#### Methods\nMost methods in this class return their own instance, allowing fluent syntax through method chaining.\n\n```php\n// Destroys the TimeWarden instance and returns a new one.\nTimeWarden::reset(): TimeWarden\n\n// Creates a new group.\nTimeWarden::group(string $groupName): TimeWarden\n\n/**\n * Creates a new task inside the last created group\n * or within the TimeWarden instance itself.\n */\nTimeWarden::task(string $taskName): TimeWarden\n\n// Starts the last created task\nTimeWarden::start(): TimeWarden\n\n// Stops the last created task\nTimeWarden::stop(): TimeWarden\n\n// Measures the execution time of a callable and returns duration in milliseconds\nTimeWarden::measure(callable $fn, ?string $taskName = null): float\n\n// Obtains all the created groups\nTimeWarden::getGroups(): array\n\n/**\n * It allows you to obtain a TimeWardenSummary instance, \n * which is useful for getting a summary of all groups \n * and tasks generated by TimeWarden. \n * \n * Through that instance, you can retrieve the summary \n * in array or string (JSON) format.\n */\nTimeWarden::getSummary(): TimeWardenSummary\n\n/**\n * Returns a table with execution time debugging info \n * (ideal for displaying in the console).\n */\nTimeWarden::output(): string\n```\nAdditionally, it has all the methods of the [Taskable](#taskable) interface.\n\n### `Task`\nAll tasks you create are instances of `Tomloprod\\TimeWarden\\Task`.\nThe most useful methods and properties of a task are the following:\n\n#### Properties\n- `name`\n\n#### Methods\n```php\n$task = new Task('Task 1');\n\n$task->start(): void\n$task->stop(?callable $fn = null): void\n\n// Returns the duration of the task in a human-readable format. Example: *1day 10h 20min 30sec 150ms*\n$task->getFriendlyDuration(): string\n// Returns the duration of the task in milliseconds\n$task->getDuration(): float\n\n// Returns the taskable element to which the task belongs.\n$task->getTaskable(): ?Taskable\n\n$task->hasStarted(): bool\n$task->hasEnded(): bool\n\n$task->getStartDateTime(): ?DateTimeImmutable\n$task->getEndDateTime(): ?DateTimeImmutable\n\n// Returns the start and end timestamps in nanoseconds (high precision)\n$task->getStartTimestamp(): int\n$task->getEndTimestamp(): int\n\n/** @return array<string, mixed> */\n$task->toArray(): array\n\n// Reactive execution time methods\n$task->onExceedsMilliseconds(float $milliseconds, callable $fn): ?Task\n$task->onExceedsSeconds(float $seconds, callable $fn): ?Task\n$task->onExceedsMinutes(float $minutes, callable $fn): ?Task\n$task->onExceedsHours(float $hours, callable $fn): ?Task\n```\n\n### `Group`\nAll groups you create are instances of the `Tomloprod\\TimeWarden\\Group` object.\nThe most useful methods and properties of a group are the following:\n\n#### Properties\n- `name`\n\n#### Methods\n```php\n\n// Starts the last created task inside this group\n$group->start(): void\n```\nAdditionally, it has all the methods of the [Taskable](#taskable) interface.\n\n### `Taskable`\n`Tomloprod\\TimeWarden\\Contracts\\Taskable` is the interface used by the **TimeWarden** instance as well as by each task **group**\n\n#### Methods\n```php\n// Create a new task within the taskable.\n$taskable->createTask(string $taskName): Task\n\n$taskable->getTasks(): array\n\n$taskable->getLastTask(): ?Task\n\n// Return the total time in milliseconds of all tasks within the taskable.\n$taskable->getDuration(): float\n\n$taskable->toArray(): array\n\n$taskable->toJson(): string\n```\n\n### `TimeWardenSummary`\n`Tomloprod\\TimeWarden\\TimeWardenSummary` is a class that allows obtaining a general summary of groups and their tasks generated with TimeWarden.\n\nIt is useful for obtaining a summary in array or string (JSON) format.\n\nYou can obtain an instance of `TimeWardenSummary` as follows:\n```php\n/** @var Tomloprod\\TimeWarden\\TimeWardenSummary $timeWardenSummary */\n$timeWardenSummary = timeWarden()->getSummary();\n```\n\n#### Methods\n```php\n\n$timeWardenSummary->toArray(): array\n$timeWardenSummary->toJson(): string\n```\n\nHere is an example of the data returned in array format:\n\n```php\n$summaryArray = [\n    [\n        'name' => 'default',\n        'duration' => 42.0,\n        'tasks' => [\n            [\n                'name' => 'TaskName1',\n                'duration' => 19.0,\n                'friendly_duration' => '19ms',\n                'start_timestamp' => 1496664000000000000, // nanoseconds\n                'end_timestamp' => 1496664000019000000,   // nanoseconds\n                'start_datetime' => '2017-06-05T12:00:00+00:00',\n                'end_datetime' => '2017-06-05T12:00:00+00:00',\n            ],\n            [\n                'name' => 'TaskName2',\n                'duration' => 23.0,\n                'friendly_duration' => '23ms',\n                'start_timestamp' => 1496664000000000000, // nanoseconds\n                'end_timestamp' => 1496664000023000000,   // nanoseconds\n                'start_datetime' => '2017-06-05T12:00:00+00:00',\n                'end_datetime' => '2017-06-05T12:00:00+00:00',\n            ],\n        ],\n    ],\n    [ // Others groups... ],\n];\n```\n\n## **🚀 Installation & Requirements**\n\n> **Requires [PHP 8.2+](https://php.net/releases/)**\n\nYou may use [Composer](https://getcomposer.org) to install TimeWarden into your PHP project:\n\n```bash\ncomposer require tomloprod/time-warden\n```\n\n## **🧑‍🤝‍🧑 Contributing**\n\nContributions are welcome, and are accepted via pull requests.\nPlease [review these guidelines](./CONTRIBUTING.md) before submitting any pull requests.\n\n------\n\n**TimeWarden** was created by **[Tomás López](https://twitter.com/tomloprod)** and open-sourced under the **[MIT license](https://opensource.org/licenses/MIT)**.\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"tomloprod/time-warden\",\n    \"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.\",\n    \"type\": \"library\",\n    \"keywords\": [\n        \"tomloprod\",\n        \"time-warden\",\n        \"execution time\",\n        \"debugging\",\n        \"monitoring\",\n        \"performance\"\n    ],\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Tomás López\",\n            \"email\": \"tomloprod@gmail.com\"\n        }\n    ],\n    \"require\": {\n        \"php\": \"^8.2.0\"\n    },\n    \"require-dev\": {\n        \"laravel/pint\": \"^1.22.1\",\n        \"pestphp/pest\": \"^3.8.2\",\n        \"pestphp/pest-plugin-type-coverage\": \"^3.5.0\",\n        \"rector/rector\": \"^1.1.0\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Tomloprod\\\\TimeWarden\\\\\": \"src/\"\n        },\n        \"files\": [\n            \"src/Support/TimeWardenAlias.php\"\n        ]\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Tests\\\\\": \"tests/\"\n        }\n    },\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"config\": {\n        \"sort-packages\": true,\n        \"preferred-install\": \"dist\",\n        \"allow-plugins\": {\n            \"pestphp/pest-plugin\": true\n        }\n    },\n    \"scripts\": {\n        \"lint\": \"pint\",\n        \"refactor\": \"rector\",\n        \"test:lint\": \"pint --test\",\n        \"test:refactor\": \"rector --dry-run\",\n        \"test:types\": \"phpstan analyse\",\n        \"test:type-coverage\": \"pest --type-coverage --min=100\",\n        \"test:unit\": \"pest --coverage --min=100\",\n        \"test\": [\n            \"@test:lint\",\n            \"@test:refactor\",\n            \"@test:types\",\n            \"@test:type-coverage\",\n            \"@test:unit\"\n        ]\n    }\n}\n"
  },
  {
    "path": "phpstan.neon.dist",
    "content": "parameters:\n    level: max\n    paths:\n        - src\n\n    reportUnmatchedIgnoredErrors: true\n"
  },
  {
    "path": "phpunit.xml.dist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n    xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/10.0/phpunit.xsd\"\n    colors=\"true\"\n    cacheDirectory=\".phpunit.cache\">\n    <source>\n        <include>\n            <directory suffix=\".php\">./src</directory>\n        </include>\n    </source>\n    <testsuites>\n        <testsuite name=\"default\">\n            <directory suffix=\".php\">./tests</directory>\n        </testsuite>\n    </testsuites>\n</phpunit>\n"
  },
  {
    "path": "pint.json",
    "content": "{\n    \"preset\": \"laravel\",\n    \"rules\": {\n        \"array_push\": true,\n        \"backtick_to_shell_exec\": true,\n        \"date_time_immutable\": true,\n        \"declare_strict_types\": true,\n        \"lowercase_keywords\": true,\n        \"lowercase_static_reference\": true,\n        \"final_class\": true,\n        \"final_internal_class\": true,\n        \"final_public_method_for_abstract_class\": true,\n        \"fully_qualified_strict_types\": true,\n        \"global_namespace_import\": {\n            \"import_classes\": true,\n            \"import_constants\": true,\n            \"import_functions\": true\n        },\n        \"mb_str_functions\": true,\n        \"modernize_types_casting\": true,\n        \"new_with_parentheses\": false,\n        \"no_superfluous_elseif\": true,\n        \"no_useless_else\": true,\n        \"no_multiple_statements_per_line\": true,\n        \"ordered_class_elements\": {\n            \"order\": [\n                \"use_trait\",\n                \"case\",\n                \"constant\",\n                \"constant_public\",\n                \"constant_protected\",\n                \"constant_private\",\n                \"property_public\",\n                \"property_protected\",\n                \"property_private\",\n                \"construct\",\n                \"destruct\",\n                \"magic\",\n                \"phpunit\",\n                \"method_abstract\",\n                \"method_public_static\",\n                \"method_public\",\n                \"method_protected_static\",\n                \"method_protected\",\n                \"method_private_static\",\n                \"method_private\"\n            ],\n            \"sort_algorithm\": \"none\"\n        },\n        \"ordered_interfaces\": true,\n        \"ordered_traits\": true,\n        \"protected_to_private\": true,\n        \"self_accessor\": true,\n        \"self_static_accessor\": true,\n        \"strict_comparison\": true,\n        \"visibility_required\": true\n    }\n}"
  },
  {
    "path": "rector.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse Rector\\Config\\RectorConfig;\n\nreturn RectorConfig::configure()\n    ->withPaths([\n        __DIR__.'/src',\n        __DIR__.'/tests',\n    ])\n    ->withSkip([])\n    ->withPreparedSets(\n        deadCode: true,\n        codeQuality: true,\n        typeDeclarations: true,\n        privatization: true,\n        earlyReturn: true,\n        strictBooleans: true,\n    )\n    ->withPhpSets();\n"
  },
  {
    "path": "src/Concerns/HasTasks.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Tomloprod\\TimeWarden\\Concerns;\n\nuse Tomloprod\\TimeWarden\\Task;\n\ntrait HasTasks\n{\n    /**\n     * @var array<Task>\n     */\n    private array $tasks = [];\n\n    public function createTask(string $taskName): Task\n    {\n        $task = new Task($taskName, $this);\n\n        $this->tasks[] = $task;\n\n        return $task;\n    }\n\n    /**\n     * @return array<Task>\n     */\n    public function getTasks(): array\n    {\n        return $this->tasks;\n    }\n\n    /**\n     * @return float The duration time in milliseconds\n     */\n    public function getDuration(): float\n    {\n        $duration = 0.0;\n\n        /** @var Task $task */\n        foreach ($this->getTasks() as $task) {\n            $duration += $task->getDuration();\n        }\n\n        return $duration;\n    }\n\n    public function getLastTask(): ?Task\n    {\n        /** @var Task|bool $lastTask */\n        $lastTask = end($this->tasks);\n\n        return ($lastTask instanceof Task) ? $lastTask : null;\n    }\n\n    public function toArray(): array\n    {\n        /** @var array<string, mixed> $tasksInfo */\n        $tasksInfo = [];\n\n        /** @var Task $task */\n        foreach ($this->getTasks() as $task) {\n            $tasksInfo[] = $task->toArray();\n        }\n\n        return [\n            'name' => $this->name,\n            'duration' => $this->getDuration(),\n            'tasks' => $tasksInfo,\n        ];\n    }\n\n    public function toJson(): string\n    {\n        $json = json_encode($this->toArray());\n\n        return ($json === false) ? '[]' : $json;\n    }\n}\n"
  },
  {
    "path": "src/Contracts/Taskable.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Tomloprod\\TimeWarden\\Contracts;\n\nuse Tomloprod\\TimeWarden\\Task;\n\ninterface Taskable\n{\n    public function createTask(string $taskName): Task;\n\n    /**\n     * @return array<Task>\n     */\n    public function getTasks(): array;\n\n    public function getLastTask(): ?Task;\n\n    public function getDuration(): float;\n\n    /** @return array<string, mixed> */\n    public function toArray(): array;\n\n    public function toJson(): string;\n}\n"
  },
  {
    "path": "src/Group.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Tomloprod\\TimeWarden;\n\nuse Tomloprod\\TimeWarden\\Concerns\\HasTasks;\nuse Tomloprod\\TimeWarden\\Contracts\\Taskable;\n\nfinal class Group implements Taskable\n{\n    use HasTasks;\n\n    public function __construct(public string $name) {}\n\n    public function start(): void\n    {\n        /** @var Task|null $lastTask */\n        $lastTask = $this->getLastTask();\n\n        if ($lastTask instanceof Task) {\n            $lastTask->start();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Services/TimeWardenManager.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Tomloprod\\TimeWarden\\Services;\n\nuse Exception;\nuse Tomloprod\\TimeWarden\\Concerns\\HasTasks;\nuse Tomloprod\\TimeWarden\\Contracts\\Taskable;\nuse Tomloprod\\TimeWarden\\Group;\nuse Tomloprod\\TimeWarden\\Support\\Console\\Table;\nuse Tomloprod\\TimeWarden\\Task;\nuse Tomloprod\\TimeWarden\\TimeWardenSummary;\n\nfinal class TimeWardenManager implements Taskable\n{\n    use HasTasks;\n\n    public string $name = 'default';\n\n    private static TimeWardenManager $instance;\n\n    /**\n     * @var array<Group>\n     */\n    private array $groups = [];\n\n    private function __construct() {}\n\n    public function __clone()\n    {\n        throw new Exception('Cannot clone singleton');\n    }\n\n    public function __wakeup()\n    {\n        throw new Exception('Cannot unserialize singleton');\n    }\n\n    /**\n     * Get the singleton instance of TimeWarden.\n     */\n    public static function instance(): self\n    {\n        if (! isset(self::$instance)) {\n            self::$instance = new self();\n        }\n\n        return self::$instance;\n    }\n\n    public function reset(): self\n    {\n        self::$instance = new self();\n\n        return self::$instance;\n    }\n\n    public function group(string $groupName): self\n    {\n        $this->stop();\n\n        $group = $this->getLastGroup();\n        if ($group && ! $group->getLastTask() instanceof Task) {\n            $group->name = $groupName;\n        } else {\n            $this->groups[] = new Group($groupName);\n        }\n\n        return self::$instance;\n    }\n\n    public function task(string $taskName): self\n    {\n        /** @var Taskable $taskable */\n        $taskable = $this->getActiveTaskable();\n\n        /** @var Task|null $lastTask */\n        $lastTask = $taskable->getLastTask();\n\n        // If the last task was never started, we replace its name with `$taskName`\n        if ($lastTask instanceof Task && ! $lastTask->hasStarted()) {\n            $lastTask->name = $taskName;\n        } else {\n            // If there is a task, but it has already started, we stop it\n            if ($lastTask instanceof Task && $lastTask->hasStarted()) {\n                $lastTask->stop();\n            }\n\n            // And add the task to the taskable.\n            $taskable->createTask($taskName);\n        }\n\n        return self::$instance;\n    }\n\n    public function start(): self\n    {\n        /** @var Task|null $lastTask */\n        $lastTask = $this->getActiveTaskable()->getLastTask();\n\n        if ($lastTask instanceof Task) {\n            $lastTask->start();\n        }\n\n        return self::$instance;\n    }\n\n    public function stop(?callable $fn = null): self\n    {\n        /** @var Task|null $lastTask */\n        $lastTask = $this->getActiveTaskable()->getLastTask();\n\n        if ($lastTask instanceof Task) {\n            $lastTask->stop($fn);\n        }\n\n        return self::$instance;\n    }\n\n    /**\n     * Measure the execution time of a callable\n     *\n     * @param  callable  $fn  The callable to measure\n     * @param  string  $taskName  The task name. If not provided, will use 'callable' as default.\n     * @return float The duration time in milliseconds\n     */\n    public function measure(callable $fn, string $taskName = 'callable'): float\n    {\n        // Create task and start\n        $this->task($taskName)->start();\n\n        try {\n            $fn();\n        } finally {\n            $this->stop();\n        }\n\n        // Get the duration from the last task\n        $lastTask = $this->getActiveTaskable()->getLastTask();\n\n        return $lastTask instanceof Task ? $lastTask->getDuration() : 0.0;\n    }\n\n    /**\n     * @return array<Group>\n     */\n    public function getGroups(): array\n    {\n        return $this->groups;\n    }\n\n    public function getSummary(): TimeWardenSummary\n    {\n        $this->stop();\n\n        return new TimeWardenSummary();\n    }\n\n    public function output(): string\n    {\n        $this->stop();\n\n        /** @var string $output */\n        $output = '';\n\n        /** @var array<string> $columns */\n        $columns = [\n            'GROUP',\n            'TASK',\n            'DURATION (MS)',\n        ];\n\n        /** @var array<string|float> $rows */\n        $rows = [];\n\n        $totalGroups = 0;\n        $totalTasks = 0;\n        $totalDuration = $this->getDuration();\n\n        /** @var Task $task */\n        foreach ($this->getTasks() as $iTask => $task) {\n            $rows[] = [\n                ($iTask === 0) ? 'default ('.$this->getDuration().' ms)' : '',\n                $task->name,\n                $task->getDuration(),\n            ];\n\n            $totalTasks++;\n        }\n\n        if ($totalTasks > 0) {\n            $rows[] = Table::separator();\n        }\n\n        /** @var Group|null $lastIterateGroup */\n        $lastIterateGroup = null;\n\n        /** @var Group $group */\n        foreach ($this->groups as $iGroup => $group) {\n\n            /** @var Task $task */\n            foreach ($group->getTasks() as $task) {\n                $rows[] = [\n                    ($lastIterateGroup !== $group) ? $group->name.' ('.$group->getDuration().' ms)' : '',\n                    $task->name,\n                    $task->getDuration(),\n                ];\n\n                $lastIterateGroup = $group;\n                $totalTasks++;\n            }\n\n            if ($iGroup !== count($this->groups) - 1) {\n                $rows[] = Table::separator();\n            }\n\n            $totalDuration += $group->getDuration();\n            $totalGroups++;\n        }\n\n        $output = (new Table())\n            ->setHeaders($columns)\n            ->setRows($rows)\n            ->setStyle('box-double')\n            ->setFooterTitle('Total: '.$totalDuration.' ms')\n            ->setHeaderTitle('TIMEWARDEN')\n            ->render();\n\n        return PHP_EOL.$output;\n    }\n\n    private function getActiveTaskable(): Taskable\n    {\n        return $this->getLastGroup() ?? $this;\n    }\n\n    private function getLastGroup(): ?Group\n    {\n        /** @var Group|bool $lastGroup */\n        $lastGroup = end($this->groups);\n\n        return ($lastGroup instanceof Group) ? $lastGroup : null;\n    }\n}\n"
  },
  {
    "path": "src/Support/Console/Table.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Tomloprod\\TimeWarden\\Support\\Console;\n\n/**\n * @codeCoverageIgnore\n */\nfinal class Table\n{\n    private const TABLE_SEPARATOR = '---SEPARATOR---';\n\n    private const STYLES = [\n        'default' => [\n            'top_left' => '+',\n            'top_mid' => '+',\n            'top_right' => '+',\n            'mid_left' => '+',\n            'mid_mid' => '+',\n            'mid_right' => '+',\n            'bottom_left' => '+',\n            'bottom_mid' => '+',\n            'bottom_right' => '+',\n            'horizontal' => '-',\n            'vertical' => '|',\n        ],\n        'box-double' => [\n            'top_left' => '╔',\n            'top_mid' => '╦',\n            'top_right' => '╗',\n            'mid_left' => '╠',\n            'mid_mid' => '╬',\n            'mid_right' => '╣',\n            'bottom_left' => '╚',\n            'bottom_mid' => '╩',\n            'bottom_right' => '╝',\n            'horizontal' => '═',\n            'vertical' => '║',\n            'column_separator' => '║',\n            'separator_left' => '╠',\n            'separator_mid' => '╬',\n            'separator_right' => '╣',\n            'separator_horizontal' => '═',\n        ],\n    ];\n\n    /**\n     * @var array<string>\n     */\n    private array $headers = [];\n\n    /**\n     * @var array<mixed>\n     */\n    private array $rows = [];\n\n    private string $style = 'default';\n\n    private string $headerTitle = '';\n\n    private string $footerTitle = '';\n\n    public static function separator(): string\n    {\n        return self::TABLE_SEPARATOR;\n    }\n\n    /**\n     * @param  array<string>  $headers\n     */\n    public function setHeaders(array $headers): self\n    {\n        $this->headers = $headers;\n\n        return $this;\n    }\n\n    /**\n     * @param  array<mixed>  $rows\n     */\n    public function setRows(array $rows): self\n    {\n        $this->rows = $rows;\n\n        return $this;\n    }\n\n    public function setStyle(string $style): self\n    {\n        $this->style = $style;\n\n        return $this;\n    }\n\n    public function setHeaderTitle(string $title): self\n    {\n        $this->headerTitle = $title;\n\n        return $this;\n    }\n\n    public function setFooterTitle(string $title): self\n    {\n        $this->footerTitle = $title;\n\n        return $this;\n    }\n\n    public function render(): string\n    {\n        if ($this->headers === [] && $this->rows === []) {\n            return '';\n        }\n\n        $output = [];\n\n        $styleChars = self::STYLES[$this->style] ?? self::STYLES['default'];\n\n        // Calculate column widths\n        $columnWidths = $this->calculateColumnWidths();\n\n        // Top border\n        $output[] = $this->renderTopBorder($columnWidths, $styleChars);\n\n        // Headers\n        if ($this->headers !== []) {\n            $output[] = $this->renderRow($this->headers, $columnWidths, $styleChars);\n            $output[] = $this->renderHeaderSeparator($columnWidths, $styleChars);\n        }\n\n        // Data rows\n        foreach ($this->rows as $row) {\n            if ($row === self::TABLE_SEPARATOR) {\n                $output[] = $this->renderSeparator($columnWidths, $styleChars);\n            } elseif (is_array($row)) {\n                $output[] = $this->renderRow($row, $columnWidths, $styleChars);\n            }\n        }\n\n        // Bottom border\n        $output[] = $this->renderBottomBorder($columnWidths, $styleChars);\n\n        return implode(PHP_EOL, $output);\n    }\n\n    /**\n     * @return array<int>\n     */\n    private function calculateColumnWidths(): array\n    {\n        $widths = [];\n\n        // Initialize with headers\n        foreach ($this->headers as $i => $header) {\n            $widths[$i] = mb_strlen((string) $header);\n        }\n\n        // Consider row content\n        foreach ($this->rows as $row) {\n            if ($row !== self::TABLE_SEPARATOR && is_array($row)) {\n                foreach ($row as $i => $cell) {\n                    $cellLength = mb_strlen((string) $cell);\n                    $widths[$i] = max($widths[$i] ?? 0, $cellLength);\n                }\n            }\n        }\n\n        return $widths;\n    }\n\n    /**\n     * @param  array<int>  $columnWidths\n     * @param  array<string, string>  $styleChars\n     */\n    private function renderTopBorder(array $columnWidths, array $styleChars): string\n    {\n        $line = '';\n\n        if ($this->headerTitle !== '') {\n            // Create line with vertical separators crossing the title\n            $line .= $styleChars['top_left'];\n\n            // Calculate separator positions\n            $currentPos = 0;\n            $separatorPositions = [];\n            foreach ($columnWidths as $i => $width) {\n                $currentPos += $width + 2; // +2 for spaces\n                if ($i < count($columnWidths) - 1) {\n                    $separatorPositions[] = $currentPos;\n                    $currentPos += 1; // +1 for separator\n                }\n            }\n\n            $totalWidth = $currentPos;\n            $titleWithSpaces = ' '.$this->headerTitle.' ';\n            $titleLength = mb_strlen($titleWithSpaces);\n\n            if ($titleLength <= $totalWidth) {\n                $remainingWidth = $totalWidth - $titleLength;\n                $leftPadding = (int) ($remainingWidth / 2);\n                $rightPadding = $remainingWidth - $leftPadding;\n\n                // Build line character by character\n                for ($pos = 0; $pos < $totalWidth; $pos++) {\n                    if (in_array($pos, $separatorPositions)) {\n                        $line .= $styleChars['top_mid'];\n                    } elseif ($pos >= $leftPadding && $pos < $leftPadding + $titleLength) {\n                        $titleIndex = $pos - $leftPadding;\n                        $line .= $titleWithSpaces[$titleIndex] ?? $styleChars['horizontal'];\n                    } else {\n                        $line .= $styleChars['horizontal'];\n                    }\n                }\n            } else {\n                // If title is too long, use normal format\n                foreach ($columnWidths as $i => $width) {\n                    $line .= str_repeat($styleChars['horizontal'], $width + 2); // +2 for spaces\n                    if ($i < count($columnWidths) - 1) {\n                        $line .= $styleChars['top_mid'];\n                    }\n                }\n            }\n\n            $line .= $styleChars['top_right'];\n        } else {\n            $line .= $styleChars['top_left'];\n            foreach ($columnWidths as $i => $width) {\n                $line .= str_repeat($styleChars['horizontal'], $width + 2); // +2 for spaces\n                if ($i < count($columnWidths) - 1) {\n                    $line .= $styleChars['top_mid'];\n                }\n            }\n            $line .= $styleChars['top_right'];\n        }\n\n        return $line;\n    }\n\n    /**\n     * @param  array<int>  $columnWidths\n     * @param  array<string, string>  $styleChars\n     */\n    private function renderHeaderSeparator(array $columnWidths, array $styleChars): string\n    {\n        $line = $styleChars['mid_left'];\n        foreach ($columnWidths as $i => $width) {\n            $line .= str_repeat($styleChars['horizontal'], $width + 2); // +2 for spaces\n            if ($i < count($columnWidths) - 1) {\n                $line .= $styleChars['mid_mid'];\n            }\n        }\n\n        return $line.$styleChars['mid_right'];\n    }\n\n    /**\n     * @param  array<int>  $columnWidths\n     * @param  array<string, string>  $styleChars\n     */\n    private function renderSeparator(array $columnWidths, array $styleChars): string\n    {\n        $separatorLeft = $styleChars['separator_left'] ?? $styleChars['mid_left'];\n        $separatorMid = $styleChars['separator_mid'] ?? $styleChars['mid_mid'];\n        $separatorRight = $styleChars['separator_right'] ?? $styleChars['mid_right'];\n        $separatorHorizontal = $styleChars['separator_horizontal'] ?? $styleChars['horizontal'];\n\n        $line = $separatorLeft;\n        foreach ($columnWidths as $i => $width) {\n            $line .= str_repeat($separatorHorizontal, $width + 2); // +2 for spaces\n            if ($i < count($columnWidths) - 1) {\n                $line .= $separatorMid;\n            }\n        }\n\n        return $line.$separatorRight;\n    }\n\n    /**\n     * @param  array<mixed>  $row\n     * @param  array<int>  $columnWidths\n     * @param  array<string, string>  $styleChars\n     */\n    private function renderRow(array $row, array $columnWidths, array $styleChars): string\n    {\n        $columnSeparator = $styleChars['column_separator'] ?? $styleChars['vertical'];\n\n        $line = $styleChars['vertical']; // Left border (double)\n        foreach ($columnWidths as $i => $width) {\n            $cellValue = $row[$i] ?? '';\n            $cell = is_scalar($cellValue) ? (string) $cellValue : '';\n            $cellPadding = $width - mb_strlen($cell);\n            $line .= ' '.$cell.str_repeat(' ', $cellPadding);\n            if ($i < count($columnWidths) - 1) {\n                $line .= ' '.$columnSeparator; // Separator between columns\n            }\n        } // Right border (double)\n\n        return $line.(' '.$styleChars['vertical']);\n    }\n\n    /**\n     * @param  array<int>  $columnWidths\n     * @param  array<string, string>  $styleChars\n     */\n    private function renderBottomBorder(array $columnWidths, array $styleChars): string\n    {\n        $line = '';\n\n        if ($this->footerTitle !== '') {\n            // Create line with vertical separators crossing the title\n            $line .= $styleChars['bottom_left'];\n\n            // Calculate separator positions\n            $currentPos = 0;\n            $separatorPositions = [];\n            foreach ($columnWidths as $i => $width) {\n                $currentPos += $width + 2; // +2 for spaces\n                if ($i < count($columnWidths) - 1) {\n                    $separatorPositions[] = $currentPos;\n                    $currentPos += 1; // +1 for separator\n                }\n            }\n\n            $totalWidth = $currentPos;\n            $titleWithSpaces = ' '.$this->footerTitle.' ';\n            $titleLength = mb_strlen($titleWithSpaces);\n\n            if ($titleLength <= $totalWidth) {\n                $remainingWidth = $totalWidth - $titleLength;\n                $leftPadding = (int) ($remainingWidth / 2);\n                $rightPadding = $remainingWidth - $leftPadding;\n\n                // Build line character by character\n                for ($pos = 0; $pos < $totalWidth; $pos++) {\n                    if (in_array($pos, $separatorPositions)) {\n                        $line .= $styleChars['bottom_mid'];\n                    } elseif ($pos >= $leftPadding && $pos < $leftPadding + $titleLength) {\n                        $titleIndex = $pos - $leftPadding;\n                        $line .= $titleWithSpaces[$titleIndex] ?? $styleChars['horizontal'];\n                    } else {\n                        $line .= $styleChars['horizontal'];\n                    }\n                }\n            } else {\n                // If title is too long, use normal format\n                foreach ($columnWidths as $i => $width) {\n                    $line .= str_repeat($styleChars['horizontal'], $width + 2); // +2 for spaces\n                    if ($i < count($columnWidths) - 1) {\n                        $line .= $styleChars['bottom_mid'];\n                    }\n                }\n            }\n\n            $line .= $styleChars['bottom_right'];\n        } else {\n            $line .= $styleChars['bottom_left'];\n            foreach ($columnWidths as $i => $width) {\n                $line .= str_repeat($styleChars['horizontal'], $width + 2); // +2 for spaces\n                if ($i < count($columnWidths) - 1) {\n                    $line .= $styleChars['bottom_mid'];\n                }\n            }\n            $line .= $styleChars['bottom_right'];\n        }\n\n        return $line;\n    }\n}\n"
  },
  {
    "path": "src/Support/Facades/TimeWarden.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Tomloprod\\TimeWarden\\Support\\Facades;\n\nuse Tomloprod\\TimeWarden\\Group;\nuse Tomloprod\\TimeWarden\\Services\\TimeWardenManager;\nuse Tomloprod\\TimeWarden\\Task;\n\n/**\n * @method static TimeWardenManager reset()\n * @method static TimeWardenManager group(string $groupName)\n * @method static TimeWardenManager task(string $taskName)\n * @method static TimeWardenManager start()\n * @method static Task|null stop()\n * @method static float measure(callable $fn, ?string $taskName = null)\n * @method static array<Group> getGroups()\n * @method static string output()\n *\n * Taskable methods:\n * @method static Task createTask(string $taskName)\n * @method static array<Task> getTasks(string $taskName)\n * @method static Task|null getLastTask()\n * @method static float getDuration()\n */\nfinal class TimeWarden\n{\n    /**\n     * @param  array<mixed>  $args\n     */\n    public static function __callStatic(string $method, array $args): mixed\n    {\n        $instance = TimeWardenManager::instance();\n\n        return $instance->$method(...$args);\n    }\n}\n"
  },
  {
    "path": "src/Support/TimeWardenAlias.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse Tomloprod\\TimeWarden\\Services\\TimeWardenManager;\n\nif (! function_exists('timeWarden')) {\n    function timeWarden(): TimeWardenManager\n    {\n        return TimeWardenManager::instance();\n    }\n}\n\nif (! function_exists('timewarden')) {\n    function timewarden(): TimeWardenManager\n    {\n        return TimeWardenManager::instance();\n    }\n}\n"
  },
  {
    "path": "src/Task.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Tomloprod\\TimeWarden;\n\nuse DateTime;\nuse DateTimeImmutable;\nuse Tomloprod\\TimeWarden\\Contracts\\Taskable;\n\nfinal class Task\n{\n    /**\n     * Start time in nanoseconds\n     */\n    private int $startTimestamp = 0;\n\n    /**\n     * End time in nanoseconds\n     */\n    private int $endTimestamp = 0;\n\n    public function __construct(public string $name, private readonly ?Taskable $taskable = null) {}\n\n    public function start(): void\n    {\n        if (! $this->hasStarted()) {\n            /**\n             * Force garbage collection before timing starts to ensure accurate\n             * measurements. This prevents random GC cycles from affecting\n             * benchmark results.\n             */\n            gc_collect_cycles();\n\n            $this->startTimestamp = hrtime(true);\n        }\n    }\n\n    public function stop(?callable $fn = null): void\n    {\n        if (! $this->hasEnded()) {\n            $this->endTimestamp = hrtime(true);\n        }\n\n        if ($fn !== null) {\n            $fn($this);\n        }\n    }\n\n    public function onExceedsMilliseconds(float $milliseconds, callable $fn): ?self\n    {\n        $this->stop();\n\n        if ($this->getDuration() > $milliseconds) {\n            $fn($this);\n        }\n\n        return $this;\n    }\n\n    public function onExceedsSeconds(float $seconds, callable $fn): ?self\n    {\n        $this->stop();\n\n        $durationSeconds = $this->getDuration() / 1000;\n        if ($durationSeconds > $seconds) {\n            $fn($this);\n        }\n\n        return $this;\n    }\n\n    public function onExceedsMinutes(float $minutes, callable $fn): ?self\n    {\n        $this->stop();\n\n        $durationMinutes = $this->getDuration() / 1000 / 60;\n        if ($durationMinutes > $minutes) {\n            $fn($this);\n        }\n\n        return $this;\n    }\n\n    public function onExceedsHours(float $hours, callable $fn): ?self\n    {\n        $this->stop();\n\n        $durationHours = $this->getDuration() / 3600000;\n        if ($durationHours > $hours) {\n            $fn($this);\n        }\n\n        return $this;\n    }\n\n    public function getFriendlyDuration(): string\n    {\n        $durationInMs = $this->getDuration();\n\n        $units = [\n            'day' => 24 * 60 * 60 * 1000,\n            'h' => 60 * 60 * 1000,\n            'min' => 60 * 1000,\n            'sec' => 1000,\n            'ms' => 1,\n        ];\n\n        $timeStrings = [];\n\n        foreach ($units as $name => $divisor) {\n            if ($durationInMs >= $divisor) {\n                $value = floor($durationInMs / $divisor);\n                $durationInMs %= $divisor;\n                $timeStrings[] = $value.$name;\n            }\n        }\n\n        return $timeStrings !== [] ? implode(' ', $timeStrings) : '0ms';\n    }\n\n    /**\n     * @return float The duration time in milliseconds\n     */\n    public function getDuration(): float\n    {\n        // Convert nanoseconds to milliseconds\n        return ($this->endTimestamp - $this->startTimestamp) / 1_000_000;\n    }\n\n    public function getTaskable(): ?Taskable\n    {\n        return $this->taskable;\n    }\n\n    public function hasStarted(): bool\n    {\n        return $this->startTimestamp !== 0;\n    }\n\n    public function hasEnded(): bool\n    {\n        return $this->endTimestamp !== 0;\n    }\n\n    /**\n     * Get the start timestamp in nanoseconds\n     */\n    public function getStartTimestamp(): int\n    {\n        return $this->startTimestamp;\n    }\n\n    /**\n     * Get the end timestamp in nanoseconds\n     */\n    public function getEndTimestamp(): int\n    {\n        return $this->endTimestamp;\n    }\n\n    public function getStartDateTime(): ?DateTimeImmutable\n    {\n        if ($this->hasStarted()) {\n            // Convert nanoseconds to seconds for DateTime\n            $seconds = $this->startTimestamp / 1_000_000_000;\n\n            return new DateTimeImmutable('@'.number_format($seconds, 6, '.', ''));\n        }\n\n        return null;\n    }\n\n    public function getEndDateTime(): ?DateTimeImmutable\n    {\n        if ($this->hasEnded()) {\n            // Convert nanoseconds to seconds for DateTime\n            $seconds = $this->endTimestamp / 1_000_000_000;\n\n            return new DateTimeImmutable('@'.number_format($seconds, 6, '.', ''));\n        }\n\n        return null;\n    }\n\n    /**\n     * Set start timestamp for testing purposes\n     *\n     * @param  int  $nanoseconds  Nanoseconds\n     */\n    public function setTestStartTimestamp(int $nanoseconds): void\n    {\n        $this->startTimestamp = $nanoseconds;\n    }\n\n    /**\n     * Set end timestamp for testing purposes\n     *\n     * @param  int  $nanoseconds  Nanoseconds\n     */\n    public function setTestEndTimestamp(int $nanoseconds): void\n    {\n        $this->endTimestamp = $nanoseconds;\n    }\n\n    /** @return array<string, mixed> */\n    public function toArray(): array\n    {\n        /** @var ?DateTimeImmutable $startDateTime */\n        $startDateTime = $this->getStartDateTime();\n\n        /** @var ?DateTimeImmutable $endDateTime */\n        $endDateTime = $this->getEndDateTime();\n\n        return [\n            'name' => $this->name,\n            'duration' => $this->getDuration(),\n            'friendly_duration' => $this->getFriendlyDuration(),\n            'start_timestamp' => $this->startTimestamp, // nanoseconds\n            'end_timestamp' => $this->endTimestamp,     // nanoseconds\n            'start_datetime' => ($startDateTime instanceof DateTimeImmutable) ? $startDateTime->format(DateTime::ATOM) : null,\n            'end_datetime' => ($endDateTime instanceof DateTimeImmutable) ? $endDateTime->format(DateTime::ATOM) : null,\n        ];\n    }\n}\n"
  },
  {
    "path": "src/TimeWardenSummary.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Tomloprod\\TimeWarden;\n\nfinal class TimeWardenSummary\n{\n    /** @return array<string, mixed> */\n    public function toArray(): array\n    {\n        /** @var array<string, mixed> $tasksInfo */\n        $tasksInfo = [];\n\n        if (timeWarden()->getTasks() !== []) {\n            $tasksInfo[] = timeWarden()->toArray();\n        }\n\n        /** @var Group $group */\n        foreach (timeWarden()->getGroups() as $group) {\n            $tasksInfo[] = $group->toArray();\n        }\n\n        return $tasksInfo;\n    }\n\n    public function toJson(): string\n    {\n        $json = json_encode($this->toArray());\n\n        return ($json === false) ? '[]' : $json;\n    }\n}\n"
  },
  {
    "path": "tests/ArchTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\narch('globals')\n    ->expect(['dd', 'dump', 'ray', 'die', 'var_dump', 'sleep', 'dispatch', 'dispatch_sync'])\n    ->not->toBeUsed();\n\narch('contracts')\n    ->expect('Tomloprod\\TimeWarden\\Contracts')\n    ->toBeInterfaces();\n\narch('concerns')\n    ->expect('Tomloprod\\TimeWarden\\Concerns')\n    ->toBeTraits();\n"
  },
  {
    "path": "tests/Contracts/TaskableTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse Tomloprod\\TimeWarden\\Concerns\\HasTasks;\nuse Tomloprod\\TimeWarden\\Contracts\\Taskable;\n\nbeforeEach(function (): void {\n    $this->tasksClass = new class implements Taskable\n    {\n        use HasTasks;\n\n        public string $name = 'default';\n    };\n});\n\nit('can add a task', function (): void {\n    $task = $this->tasksClass->createTask('TaskName');\n\n    expect($this->tasksClass->getTasks())\n        ->toContain($task);\n});\n\nit('can retrieve the last task', function (): void {\n    $task1 = $this->tasksClass->createTask('TaskName1');\n    $task2 = $this->tasksClass->createTask('TaskName2');\n\n    expect($this->tasksClass->getLastTask())\n        ->toBe($task2);\n});\n\nit('returns null when retrieving the last task if there are no tasks', function (): void {\n    expect($this->tasksClass->getLastTask())\n        ->toBeNull();\n});\n\nit('can obtain an array/json', function (): void {\n    $task1 = $this->tasksClass->createTask('TaskName1');\n    $task1->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));\n    $task1->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0190000')));\n\n    $task2 = $this->tasksClass->createTask('TaskName2');\n    $task2->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));\n    $task2->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0230000')));\n\n    $summaryArray = [\n        'name' => 'default',\n        'duration' => 42.0,\n        'tasks' => [\n            [\n                'name' => 'TaskName1',\n                'duration' => 19.0,\n                'friendly_duration' => '19ms',\n                'start_timestamp' => 1496664000000000000,\n                'end_timestamp' => 1496664000019000000,\n                'start_datetime' => '2017-06-05T12:00:00+00:00',\n                'end_datetime' => '2017-06-05T12:00:00+00:00',\n            ],\n            [\n                'name' => 'TaskName2',\n                'duration' => 23.0,\n                'friendly_duration' => '23ms',\n                'start_timestamp' => 1496664000000000000,\n                'end_timestamp' => 1496664000023000000,\n                'start_datetime' => '2017-06-05T12:00:00+00:00',\n                'end_datetime' => '2017-06-05T12:00:00+00:00',\n            ],\n        ],\n    ];\n\n    expect($this->tasksClass->toArray())->toBe($summaryArray);\n    expect($this->tasksClass->toJson())->toBe(json_encode($summaryArray));\n});\n"
  },
  {
    "path": "tests/GroupTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse Tomloprod\\TimeWarden\\Group;\n\nit('can be created with a name', function (): void {\n    $group = new Group('GroupName');\n\n    expect($group->name)->toBe('GroupName');\n});\n\nit('can add a task', function (): void {\n    $group = new Group('GroupName');\n    $task = $group->createTask('TaskName');\n\n    expect($group->getTasks())->toContain($task);\n\n    expect($task->getTaskable())->toBe($group);\n});\n\nit('can start the last task if it exists', function (): void {\n    $group = new Group('GroupName');\n    $task = $group->createTask('TaskName');\n\n    $group->start();\n\n    expect($task->hasStarted())->toBeTrue();\n});\n\nit('does not start any task if no tasks exist', function (): void {\n    $group = new Group('GroupName');\n\n    $group->start();\n\n    expect($group->getLastTask())->toBeNull();\n});\n"
  },
  {
    "path": "tests/Services/TimeWardenManagerTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse Tomloprod\\TimeWarden\\Group;\nuse Tomloprod\\TimeWarden\\Services\\TimeWardenManager;\nuse Tomloprod\\TimeWarden\\Task;\nuse Tomloprod\\TimeWarden\\TimeWardenSummary;\n\nbeforeEach(function (): void {\n    TimeWardenManager::instance()->reset();\n});\n\nit('throws exception on clone', function (): void {\n    $instance = TimeWardenManager::instance();\n\n    $closure = fn (): mixed => clone $instance;\n\n    expect($closure)->toThrow(Exception::class, 'Cannot clone singleton');\n});\n\nit('throws exception on unserialize', function (): void {\n    $instance = TimeWardenManager::instance();\n\n    $closure = fn (): mixed => unserialize(serialize($instance));\n\n    expect($closure)->toThrow(Exception::class, 'Cannot unserialize singleton');\n});\n\nit('returns the same instance', function (): void {\n    $instance1 = TimeWardenManager::instance();\n    $instance2 = TimeWardenManager::instance();\n\n    expect($instance1)->toBe($instance2);\n});\n\nit('resets the singleton instance', function (): void {\n    $instance1 = TimeWardenManager::instance();\n    $instance1->group('Group1');\n\n    $instance1->reset();\n\n    $instance2 = TimeWardenManager::instance();\n    $groups = $instance2->getGroups();\n\n    expect($groups)\n        ->toBeEmpty()\n        ->not->toBe($instance1);\n});\n\nit('can create and retrieve groups', function (): void {\n    $instance = TimeWardenManager::instance();\n\n    $instance->group('Group1')->task('foo');\n    $instance->group('Group2')->task('bar');\n\n    $groups = $instance->getGroups();\n\n    expect($groups)->toHaveCount(2);\n\n    expect($groups[0])->toBeInstanceOf(Group::class);\n    expect($groups[1])->toBeInstanceOf(Group::class);\n\n    expect($groups[0]->name)->toBe('Group1');\n    expect($groups[1]->name)->toBe('Group2');\n});\n\nit('overwrite last group if doesn\\'t have tasks when a new group is created', function (): void {\n    $instance = TimeWardenManager::instance();\n\n    $instance->group('Group1');\n    $instance->group('Group2');\n    $instance->group('Group3');\n\n    $groups = $instance->getGroups();\n\n    expect($groups)->toHaveCount(1);\n    expect($groups[0]->name)->toBe('Group3');\n    expect($groups[0])->toBeInstanceOf(Group::class);\n});\n\nit('can create tasks of timewarden instance', function (): void {\n    $instance = TimeWardenManager::instance();\n\n    $instance->task('Task1');\n\n    $tasks = $instance->getTasks();\n\n    expect($tasks)->toHaveCount(1);\n\n    expect($tasks[0])->toBeInstanceOf(Task::class);\n});\n\nit('can create tasks inside group', function (): void {\n    $instance = TimeWardenManager::instance();\n\n    $instance->group('Group1')->task('Task1');\n\n    $tasks = $instance->getGroups()[0]->getTasks();\n\n    $timewardenTasks = $instance->getTasks();\n\n    expect($tasks)->toHaveCount(1);\n\n    expect($tasks[0])->toBeInstanceOf(Task::class);\n\n    expect($timewardenTasks)->toHaveCount(0);\n});\n\nit('overwrite last task if was never started when a new task is created', function (): void {\n    $instance = TimeWardenManager::instance();\n\n    $instance->task('Task1')->task('Task2');\n\n    $tasks = $instance->getTasks();\n\n    expect($tasks)->toHaveCount(1);\n\n    expect($tasks[0]->name)->toBe('Task2');\n\n    expect($tasks[0])->toBeInstanceOf(Task::class);\n});\n\nit('stop last task if was never ended when a new task is created', function (): void {\n    $instance = TimeWardenManager::instance();\n\n    $instance->task('Task1')->start()->task('Task2');\n\n    $tasks = $instance->getTasks();\n\n    expect($tasks)->toHaveCount(2);\n\n    // Task 1\n    expect($tasks[0]->name)->toBe('Task1');\n\n    expect($tasks[0]->hasStarted())->toBeTrue();\n\n    expect($tasks[0]->hasEnded())->toBeTrue();\n\n    expect($tasks[0])->toBeInstanceOf(Task::class);\n\n    // Task 2\n    expect($tasks[1]->name)->toBe('Task2');\n\n    expect($tasks[1]->hasStarted())->toBeFalse();\n\n    expect($tasks[1]->hasEnded())->toBeFalse();\n\n    expect($tasks[1])->toBeInstanceOf(Task::class);\n\n    $instance->start();\n\n    expect($tasks[1]->hasStarted())->toBeTrue();\n\n    expect($tasks[1]->hasEnded())->toBeFalse();\n\n    $instance->stop();\n\n    expect($tasks[1]->hasStarted())->toBeTrue();\n\n    expect($tasks[1]->hasEnded())->toBeTrue();\n});\n\ntest('output returns tasks and groups', function (): void {\n    timeWarden()->task('Task 1')->start();\n\n    timeWarden()->task('Task 2')->start();\n\n    timeWarden()->stop();\n\n    timeWarden()->group('Group 1')->task('G1 - Task 1')->start();\n\n    timeWarden()->task('G1 - Task 2')->start();\n\n    timeWarden()->task('G1 - Task 3')->start();\n\n    timeWarden()->group('Group 2')->task('G2 - Task 1')->start();\n\n    $output = timeWarden()->output();\n\n    expect($output)\n        ->toBeString()\n        ->toContain('default')\n        ->toContain('Task 1')\n        ->toContain('Task 2')\n        ->toContain('G1 - Task 1')\n        ->toContain('G1 - Task 2')\n        ->toContain('G1 - Task 3')\n        ->toContain('Group 1')\n        ->toContain('Group 2')\n        ->toContain('G2 - Task 1');\n});\n\nit('can obtain a TimeWardenSummary', function (): void {\n    $instance = TimeWardenManager::instance();\n\n    $instance->task('Task1')->task('Task2');\n\n    expect($instance->getSummary())->toBeInstanceOf(TimeWardenSummary::class);\n});\n\nit('can measure execution time of a callable with default task name', function (): void {\n    $instance = TimeWardenManager::instance();\n\n    $executed = false;\n\n    $duration = $instance->measure(function () use (&$executed): void {\n        $executed = true;\n    });\n\n    expect($duration)->toBeFloat();\n    expect($executed)->toBeTrue();\n\n    $tasks = $instance->getTasks();\n    expect($tasks)->toHaveCount(1);\n    expect($tasks[0]->name)->toBe('callable');\n    expect($tasks[0])->toBeInstanceOf(Task::class);\n});\n\nit('can measure execution time of a callable with custom task name', function (): void {\n    $instance = TimeWardenManager::instance();\n\n    $executed = false;\n\n    $duration = $instance->measure(function () use (&$executed): void {\n        $executed = true;\n    }, 'custom-task');\n\n    expect($duration)->toBeFloat();\n    expect($executed)->toBeTrue();\n\n    $tasks = $instance->getTasks();\n    expect($tasks)->toHaveCount(1);\n    expect($tasks[0]->name)->toBe('custom-task');\n    expect($tasks[0])->toBeInstanceOf(Task::class);\n});\n\nit('can measure execution time inside a group', function (): void {\n    $instance = TimeWardenManager::instance();\n\n    $instance->group('Test Group');\n\n    $executed = false;\n\n    $duration = $instance->measure(function () use (&$executed): void {\n        $executed = true;\n    }, 'group-task');\n\n    expect($duration)->toBeFloat();\n    expect($executed)->toBeTrue();\n\n    $groups = $instance->getGroups();\n    expect($groups)->toHaveCount(1);\n\n    $tasks = $groups[0]->getTasks();\n    expect($tasks)->toHaveCount(1);\n    expect($tasks[0]->name)->toBe('group-task');\n    expect($tasks[0])->toBeInstanceOf(Task::class);\n\n    // TimeWarden instance should have no tasks (they're in the group)\n    expect($instance->getTasks())->toHaveCount(0);\n});\n\nit('ensures task is stopped even if callable throws exception', function (): void {\n    $instance = TimeWardenManager::instance();\n\n    $exceptionThrown = false;\n\n    try {\n        $instance->measure(function (): void {\n            throw new Exception('Test exception');\n        }, 'exception-task');\n    } catch (Exception $e) {\n        $exceptionThrown = true;\n        expect($e->getMessage())->toBe('Test exception');\n    }\n\n    expect($exceptionThrown)->toBeTrue();\n\n    $tasks = $instance->getTasks();\n    expect($tasks)->toHaveCount(1);\n    expect($tasks[0]->name)->toBe('exception-task');\n    expect($tasks[0])->toBeInstanceOf(Task::class);\n});\n"
  },
  {
    "path": "tests/Support/Facades/TimeWardenTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse Tomloprod\\TimeWarden\\Services\\TimeWardenManager;\nuse Tomloprod\\TimeWarden\\Support\\Facades\\TimeWarden;\nuse Tomloprod\\TimeWarden\\Task;\n\nbeforeEach(function (): void {\n    TimeWarden::reset();\n});\n\ntest('facade returns the same instance', function (): void {\n    $instance1 = TimeWardenManager::instance();\n    $instance2 = TimeWarden::instance();\n\n    expect($instance1)->toBe($instance2);\n});\n\nit('can create tasks using TimeWarden facade', function (): void {\n    TimeWarden::task('Task1');\n\n    $tasks = TimeWarden::instance()->getTasks();\n\n    expect($tasks)->toHaveCount(1);\n\n    expect($tasks[0])->toBeInstanceOf(Task::class);\n});\n"
  },
  {
    "path": "tests/Support/TimeWardenAliasTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse Tomloprod\\TimeWarden\\Services\\TimeWardenManager;\n\ntest('timeWarden (w uppercase) alias return instance of TimeWarden', function (): void {\n    expect(timeWarden())\n        ->toBeInstanceOf(TimeWardenManager::class);\n});\n\ntest('timewarden (w lowercase) alias return instance of TimeWarden', function (): void {\n    expect(timewarden())\n        ->toBeInstanceOf(TimeWardenManager::class);\n});\n"
  },
  {
    "path": "tests/TaskTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse Tomloprod\\TimeWarden\\Group;\nuse Tomloprod\\TimeWarden\\Task;\n\nit('can be created with a name', function (): void {\n    $task = new Task('TaskName');\n\n    expect($task->name)->toBe('TaskName');\n});\n\nit('starts and stops task', function (): void {\n    $task = new Task('TaskName');\n\n    expect($task->hasStarted())->toBeFalse();\n    expect($task->hasEnded())->toBeFalse();\n\n    $task->start();\n\n    expect($task->hasStarted())->toBeTrue();\n    expect($task->hasEnded())->toBeFalse();\n\n    $task->stop();\n\n    expect($task->hasStarted())->toBeTrue();\n    expect($task->hasEnded())->toBeTrue();\n});\n\nit('stop task with callable when does not exceed execution time', function (): void {\n    $task = new Task('TaskName');\n    $task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));\n    $task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0190000')));\n\n    /** @var bool $callableIsExecuted */\n    $callableIsExecuted = false;\n\n    $task->stop(static function (Task $task) use (&$callableIsExecuted): void {\n        $task->onExceedsMilliseconds(20, static function () use (&$callableIsExecuted): void {\n            $callableIsExecuted = true;\n        });\n    });\n\n    expect($task->getDuration())->toBeLessThan(20);\n    expect($callableIsExecuted)->toBeFalse();\n});\n\nit('stop task with callable when exceeds execution time', function (): void {\n    $task = new Task('TaskName');\n    $task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));\n    $task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0210000')));\n\n    /** @var bool $callableIsExecuted */\n    $callableIsExecuted = false;\n\n    $task->stop(static function (Task $task) use (&$callableIsExecuted): void {\n        $task->onExceedsMilliseconds(20, static function () use (&$callableIsExecuted): void {\n            $callableIsExecuted = true;\n        });\n    });\n\n    expect($task->getDuration())->toBe((float) 21);\n    expect($callableIsExecuted)->toBeTrue();\n});\n\nit('calculates duration correctly (without usleep)', function (): void {\n    $task = new Task('TaskName');\n    $task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.000000')));\n    $task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.020000')));\n\n    expect($task->getDuration())\n        ->toBeGreaterThanOrEqual(20)\n        ->toBeLessThanOrEqual(21);\n});\n\nit('calculates duration correctly (with usleep)', function (): void {\n    $task = new Task('TaskName');\n    $task->start();\n\n    // Sleep 7ms.\n    usleep(7 * 1000);\n\n    $task->stop();\n\n    expect($task->getDuration())\n        ->toBeGreaterThanOrEqual(7)\n        ->toBeLessThanOrEqual(8);\n})->onlyOnLinux();\n\nit('returns duration as 0 if not started or not stopped', function (): void {\n    $task = new Task('TaskName');\n    $duration = $task->getDuration();\n\n    expect($duration)->toBe(0.0);\n});\n\ntest('getFriendlyDuration', function (): void {\n    $task = new Task('Task');\n\n    $task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.000000')));\n    $task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 13:10:20.030000')));\n\n    expect($task->getFriendlyDuration())->toContain('1h 10min 20sec 30ms');\n});\n\n/**\n * Convert DateTime to nanoseconds for testing\n *\n * @return int Nanoseconds\n */\nfunction dateTimeToTimestamp(DateTimeImmutable $datetime): int\n{\n    // Convert DateTime to nanoseconds for testing\n    $seconds = $datetime->getTimestamp();\n    $microseconds = (int) $datetime->format('u');\n\n    return ($seconds * 1_000_000_000) + ($microseconds * 1000);\n}\n\ntest('onExceedsMilliseconds (exceeds test)', function (): void {\n    $task = new Task('Task');\n    $task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.1000000')));\n    $task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.102000')));\n\n    /** @var bool $timeExceeds */\n    $timeExceeds = false;\n\n    $task->onExceedsMilliseconds(1, static function () use (&$timeExceeds): void {\n        $timeExceeds = true;\n    });\n\n    expect($timeExceeds)->toBeTrue();\n});\n\ntest('onExceedsMilliseconds (does not exceeds test)', function (): void {\n    $task2 = new Task('Task');\n    $task2->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.1000000')));\n    $task2->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.101000')));\n\n    /** @var bool $timeExceeds */\n    $timeExceeds = false;\n\n    $task2->onExceedsMilliseconds(1, static function () use (&$timeExceeds): void {\n        $timeExceeds = true;\n    });\n\n    expect($timeExceeds)->toBeFalse();\n});\n\ntest('onExceedsSeconds (exceeds test)', function (): void {\n    $task = new Task('Task');\n    $task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));\n    $task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:10.0000000')));\n\n    /** @var bool $timeExceeds */\n    $timeExceeds = false;\n\n    $task->onExceedsSeconds(9, static function () use (&$timeExceeds): void {\n        $timeExceeds = true;\n    });\n\n    expect($timeExceeds)->toBeTrue();\n});\n\ntest('onExceedsSeconds (does not exceeds test)', function (): void {\n    $task2 = new Task('Task');\n    $task2->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));\n    $task2->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:10.0000000')));\n\n    /** @var bool $timeExceeds */\n    $timeExceeds = false;\n\n    $task2->onExceedsSeconds(10, static function () use (&$timeExceeds): void {\n        $timeExceeds = true;\n    });\n\n    expect($timeExceeds)->toBeFalse();\n});\n\ntest('onExceedsMinutes (exceeds test)', function (): void {\n    $task = new Task('Task 1 exceeds execution time');\n    $task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));\n    $task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:10:00.0000000')));\n\n    /** @var bool $timeExceeds */\n    $timeExceeds = false;\n\n    $task->onExceedsMinutes(9, static function () use (&$timeExceeds): void {\n        $timeExceeds = true;\n    });\n\n    expect($timeExceeds)->toBeTrue();\n});\n\ntest('onExceedsMinutes (does not exceeds test)', function (): void {\n    $task2 = new Task('Task');\n    $task2->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));\n    $task2->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:10:00.0000000')));\n\n    /** @var bool $timeExceeds */\n    $timeExceeds = false;\n\n    $task2->onExceedsMinutes(10, static function () use (&$timeExceeds): void {\n        $timeExceeds = true;\n    });\n\n    expect($timeExceeds)->toBeFalse();\n});\n\ntest('onExceedsHours (exceeds test)', function (): void {\n    $task = new Task('Task');\n    $task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.000000')));\n    $task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 14:01:15.000000')));\n\n    /** @var bool $timeExceeds */\n    $timeExceeds = false;\n\n    $task->onExceedsHours(2, static function () use (&$timeExceeds): void {\n        $timeExceeds = true;\n    });\n\n    expect($timeExceeds)->toBeTrue();\n});\n\ntest('onExceedsHours (does not exceeds test)', function (): void {\n    $task2 = new Task('Task');\n    $task2->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.000000')));\n    $task2->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:01:15.000000')));\n\n    /** @var bool $timeExceeds */\n    $timeExceeds = false;\n\n    $task2->onExceedsHours(2, static function () use (&$timeExceeds): void {\n        $timeExceeds = true;\n    });\n\n    expect($timeExceeds)->toBeFalse();\n});\n\ntest('getTaskable', function (): void {\n    $group = new Group('GroupName');\n    $task = new Task('TaskName', $group);\n\n    expect($task->getTaskable())->toBe($group);\n});\n\ntest('getters start and end timestamp', function (): void {\n    $task = new Task('Task getStartTimestamp');\n\n    $startTimestamp = dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.000000'));\n    $endTimestamp = dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.000000'));\n\n    $task->setTestStartTimestamp($startTimestamp);\n    $task->setTestEndTimestamp($endTimestamp);\n\n    expect($task->getStartTimestamp())->toBe($startTimestamp);\n\n    expect($task->getEndTimestamp())->toBe($endTimestamp);\n});\n\ntest('getStartDateTime return DateTime on started tasks', function (): void {\n    $task = new Task('Task getStartDateTime -> DateTime');\n\n    $date = '2017-06-05 12:00:00.000000';\n\n    $startTimestamp = dateTimeToTimestamp(new DateTimeImmutable($date));\n\n    $task->setTestStartTimestamp($startTimestamp);\n\n    $startDateTime = $task->getStartDateTime();\n\n    expect($startDateTime)->toBeInstanceOf(DateTimeImmutable::class);\n\n    expect($startDateTime->format('Y-m-d H:i:s.u'))->toBe($date);\n});\n\ntest('getStartDateTime return null on non started tasks', function (): void {\n    $task = new Task('Task getStartDateTime -> null');\n\n    $endDateTime = $task->getStartDateTime();\n\n    expect($endDateTime)->toBeNull();\n});\n\ntest('getEndDateTime returns DateTime on ended tasks', function (): void {\n    $task = new Task('Task getEndDateTime -> DateTime');\n\n    $date = '2017-06-05 12:00:00.000000';\n\n    $timestamp = dateTimeToTimestamp(new DateTimeImmutable($date));\n\n    $task->setTestStartTimestamp($timestamp);\n    $task->setTestEndTimestamp($timestamp);\n\n    $endDateTime = $task->getEndDateTime();\n\n    expect($endDateTime)->toBeInstanceOf(DateTimeImmutable::class);\n\n    expect($endDateTime->format('Y-m-d H:i:s.u'))->toBe($date);\n});\n\ntest('getEndDateTime returns null on non started tasks', function (): void {\n    $task = new Task('Task getEndDateTime -> null');\n\n    $endDateTime = $task->getEndDateTime();\n\n    expect($endDateTime)->toBeNull();\n});\n"
  },
  {
    "path": "tests/TimeWardenSummaryTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse Tomloprod\\TimeWarden\\TimeWardenSummary;\n\nit('can obtain an array/json', function (): void {\n    timeWarden()->reset();\n\n    timeWarden()->task('Generic Task')->start()->stop();\n\n    $task = timeWarden()->getTasks()[0];\n    $task->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));\n    $task->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));\n\n    timeWarden()->group('Group1')->task('TaskName1')->start()->stop();\n\n    $groupTask = timeWarden()->getGroups()[0]->getTasks()[0];\n    $groupTask->setTestStartTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0000000')));\n    $groupTask->setTestEndTimestamp(dateTimeToTimestamp(new DateTimeImmutable('2017-06-05 12:00:00.0320000')));\n\n    /** @var TimeWardenSummary $summary */\n    $summary = timeWarden()->getSummary();\n\n    $summaryArray = [\n        [\n            'name' => 'default',\n            'duration' => 0.0,\n            'tasks' => [\n                [\n                    'name' => 'Generic Task',\n                    'duration' => 0.0,\n                    'friendly_duration' => '0ms',\n                    'start_timestamp' => 1496664000000000000,\n                    'end_timestamp' => 1496664000000000000,\n                    'start_datetime' => '2017-06-05T12:00:00+00:00',\n                    'end_datetime' => '2017-06-05T12:00:00+00:00',\n                ],\n            ],\n        ],\n        [\n            'name' => 'Group1',\n            'duration' => 32.0,\n            'tasks' => [\n                [\n                    'name' => 'TaskName1',\n                    'duration' => 32.0,\n                    'friendly_duration' => '32ms',\n                    'start_timestamp' => 1496664000000000000,\n                    'end_timestamp' => 1496664000032000000,\n                    'start_datetime' => '2017-06-05T12:00:00+00:00',\n                    'end_datetime' => '2017-06-05T12:00:00+00:00',\n                ],\n            ],\n        ],\n    ];\n\n    expect($summary->toArray())->toBe($summaryArray);\n\n    expect($summary->toJson())->toBe(json_encode($summaryArray));\n});\n"
  }
]