diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..654c18b
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,18 @@
+# This file is for unifying the coding style for different editors and IDEs
+# editorconfig.org
+
+# PHP PSR-2 Coding Standards
+# http://www.php-fig.org/psr/psr-2/
+
+root = true
+
+[*.{php,inc,module}]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = space
+indent_size = 4
+
+[*.{json,json.dist,yml,yml.dist}]
+indent_size = 4
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..bc19d00
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,12 @@
+/spec export-ignore
+.editorconfig export-ignore
+.gitattributes export-ignore
+.gitignore export-ignore
+.scrutinizer.yml export-ignore
+.travis.yml export-ignore
+phpunit.xml.dist export-ignore
+infection.json.dist export-ignore
+grumphp.yml.dist export-ignore
+phpspec.yml.dist export-ignore
+/docker export-ignore
+docker-composer.yaml export-ignore
\ No newline at end of file
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..63e6e20
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @loophp
diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..d9159b2
--- /dev/null
+++ b/.github/CODE_OF_CONDUCT.md
@@ -0,0 +1,46 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at am@localheinz.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
+
+[homepage]: http://contributor-covenant.org
+[version]: http://contributor-covenant.org/version/1/4/
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 0000000..5ebc01b
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1,29 @@
+# CONTRIBUTING
+
+We're using [Travis CI](https://travis-ci.com) as a continuous integration system.
+
+For details, see [`.travis.yml`](../.travis.yml).
+
+## Tests
+
+We're using [`grumphp/grumphp`](https://github.com/phpro/grumphp) to drive the development.
+
+Run
+
+```bash
+./vendor/bin/grumphp run
+```
+
+to run all the tests.
+
+## Coding Standards
+
+We are using [`drupol/php-conventions`](https://github.com/drupol/php-conventions) to enforce coding standards.
+
+Run
+
+```bash
+./vendor/bin/grumphp run
+```
+
+to automatically detect/fix coding standard violations.
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..9bacad0
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,2 @@
+github: drupol
+custom: ["https://www.paypal.me/drupol"]
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..3421bc3
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE.md
@@ -0,0 +1,13 @@
+## Steps required to reproduce the problem
+
+1.
+2.
+3.
+
+## Expected Result
+
+*
+
+## Actual Result
+
+*
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..e448c72
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,9 @@
+This PR
+
+* [x]
+* [ ]
+* [ ]
+
+Follows #.
+Related to #.
+Fixes #.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..e75be60
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,12 @@
+version: 2
+updates:
+ - package-ecosystem: composer
+ directory: "/"
+ schedule:
+ interval: daily
+ open-pull-requests-limit: 10
+ - package-ecosystem: github-actions
+ directory: "/"
+ schedule:
+ interval: daily
+ open-pull-requests-limit: 10
\ No newline at end of file
diff --git a/.github/settings.yml b/.github/settings.yml
new file mode 100644
index 0000000..dcb72b2
--- /dev/null
+++ b/.github/settings.yml
@@ -0,0 +1,49 @@
+# https://github.com/probot/settings
+
+branches:
+ - name: master
+ protection:
+ enforce_admins: false
+ required_pull_request_reviews:
+ dismiss_stale_reviews: true
+ require_code_owner_reviews: true
+ required_approving_review_count: 1
+ required_status_checks:
+ contexts:
+ - "Grumphp"
+ strict: false
+ restrictions: null
+
+labels:
+ - name: bug
+ color: ee0701
+
+ - name: dependencies
+ color: 0366d6
+
+ - name: enhancement
+ color: 0e8a16
+
+ - name: question
+ color: cc317c
+
+ - name: security
+ color: ee0701
+
+ - name: stale
+ color: eeeeee
+
+repository:
+ allow_merge_commit: true
+ allow_rebase_merge: false
+ allow_squash_merge: false
+ default_branch: master
+ description: "A PHPSpec extension providing matchers for measuring time in tests."
+ topics: phpspec-extension
+ has_downloads: true
+ has_issues: true
+ has_pages: false
+ has_projects: false
+ has_wiki: false
+ name: phpspec-time
+ private: false
diff --git a/.github/stale.yml b/.github/stale.yml
new file mode 100644
index 0000000..8eb151c
--- /dev/null
+++ b/.github/stale.yml
@@ -0,0 +1,10 @@
+daysUntilStale: 60
+
+daysUntilClose: 7
+
+staleLabel: stale
+
+markComment: >
+ This issue has been automatically marked as stale because it has not had
+ recent activity. It will be closed if no further activity occurs. Thank you
+ for your contributions.
diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
new file mode 100644
index 0000000..70e49aa
--- /dev/null
+++ b/.github/workflows/continuous-integration.yml
@@ -0,0 +1,75 @@
+# https://help.github.com/en/categories/automating-your-workflow-with-github-actions
+
+on:
+ - pull_request
+ - push
+
+name: "Continuous Integration"
+
+jobs:
+ run:
+ name: "Grumphp"
+ runs-on: ${{ matrix.operating-system }}
+ strategy:
+ fail-fast: false
+ matrix:
+ operating-system: [ubuntu-latest, windows-latest, macOS-latest]
+ php-versions: ['7.4']
+
+ steps:
+ - name: Set git to use LF
+ run: |
+ git config --global core.autocrlf false
+ git config --global core.eol lf
+
+ - name: Checkout
+ uses: actions/checkout@v2.3.3
+ with:
+ fetch-depth: 1
+
+ - name: Install PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-versions }}
+ extensions: gd,mbstring,xdebug
+
+ - name: Get Composer Cache Directory
+ id: composer-cache
+ run: echo "::set-output name=dir::$(composer config cache-files-dir)"
+
+ - name: Cache dependencies
+ uses: actions/cache@v2
+ with:
+ path: ${{ steps.composer-cache.outputs.dir }}
+ key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
+ restore-keys: ${{ runner.os }}-composer-
+
+ - name: Install dependencies
+ run: composer install --no-progress --no-suggest --prefer-dist --optimize-autoloader
+
+ - name: Run Grumphp
+ run: vendor/bin/grumphp run --no-ansi -n
+ env:
+ STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}
+
+ - name: Send PSALM data
+ run: vendor/bin/psalm --shepherd --stats
+ continue-on-error: true
+
+ - name: Send Scrutinizer data
+ run: |
+ wget https://scrutinizer-ci.com/ocular.phar
+ php ocular.phar code-coverage:upload --format=php-clover build/logs/clover.xml
+ continue-on-error: true
+
+ - name: Infection score report
+ run: |
+ vendor/bin/infection run -j 2
+ continue-on-error: true
+
+ - name: PHP Insights report
+ run: |
+ rm -rf composer.lock vendor
+ composer require nunomaduro/phpinsights --dev
+ vendor/bin/phpinsights analyse src/ -n
+ continue-on-error: true
diff --git a/.github/workflows/prune.yaml b/.github/workflows/prune.yaml
new file mode 100644
index 0000000..db4abe6
--- /dev/null
+++ b/.github/workflows/prune.yaml
@@ -0,0 +1,33 @@
+# https://docs.github.com/en/actions
+
+name: "Prune"
+
+on: # yamllint disable-line rule:truthy
+ schedule:
+ - cron: "0 12 * * *"
+
+env:
+ DAYS_BEFORE_CLOSE: 5
+ DAYS_BEFORE_STALE: 5
+
+jobs:
+ prune:
+ name: "Issues"
+
+ runs-on: "ubuntu-latest"
+
+ steps:
+ - name: "Prune issues and pull requests"
+ uses: "actions/stale@v3.0.12"
+ with:
+ days-before-close: "${{ env.DAYS_BEFORE_CLOSE }}"
+ days-before-stale: "${{ env.DAYS_BEFORE_STALE }}"
+ repo-token: "${{ secrets.GITHUB_TOKEN }}"
+ stale-issue-label: "stale"
+ stale-issue-message: |
+ Since this issue has not had any activity within the last ${{ env.DAYS_BEFORE_STALE }} days, I have marked it as stale.
+ I will close it if no further activity occurs within the next ${{ env.DAYS_BEFORE_CLOSE }} days.
+ stale-pr-label: "stale"
+ stale-pr-message: |
+ Since this pull request has not had any activity within the last ${{ env.DAYS_BEFORE_STALE }} days, I have marked it as stale.
+ I will close it if no further activity occurs within the next ${{ env.DAYS_BEFORE_CLOSE }} days.
\ No newline at end of file
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
new file mode 100644
index 0000000..c062164
--- /dev/null
+++ b/.github/workflows/release.yaml
@@ -0,0 +1,44 @@
+# https://docs.github.com/en/actions
+
+name: "Release"
+
+on: # yamllint disable-line rule:truthy
+ push:
+ tags:
+ - "**"
+
+jobs:
+ release:
+ name: "Release"
+
+ runs-on: "ubuntu-latest"
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2.3.3
+ with:
+ fetch-depth: 1
+
+ - name: Determine tag
+ id: tag_name
+ run: |
+ echo ::set-output name=current_version::${GITHUB_REF#refs/tags/}
+ shell: bash
+
+ - name: Get Changelog Entry
+ id: changelog_reader
+ uses: mindsers/changelog-reader-action@v2
+ with:
+ version: ${{ steps.tag_name.outputs.current_version }}
+ path: ./CHANGELOG.md
+
+ - name: "Create release"
+ uses: "actions/create-release@v1.1.4"
+ env:
+ GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+ with:
+ release_name: "${{ steps.tag_name.outputs.current_version }}"
+ tag_name: "${{ steps.tag_name.outputs.current_version }}"
+ body: ${{ steps.changelog_reader.outputs.changes }}
+ prerelease: ${{ steps.changelog_reader.outputs.status == 'prereleased' }}
+ draft: ${{ steps.changelog_reader.outputs.status == 'unreleased' }}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..39ea436
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+/composer.lock
+/vendor
+/build
+/.php_cs.cache
+/example/
+/.idea/
+/test.php
+/phpspec.yml
+/examples/
+/node_modules/
+/benchmarks/
\ No newline at end of file
diff --git a/.scrutinizer.yml b/.scrutinizer.yml
new file mode 100644
index 0000000..db99448
--- /dev/null
+++ b/.scrutinizer.yml
@@ -0,0 +1,21 @@
+build:
+ nodes:
+ analysis:
+ environment:
+ php:
+ version: 7.4
+ tests:
+ override:
+ - php-scrutinizer-run
+
+filter:
+ paths:
+ - 'src/*'
+
+tools:
+ external_code_coverage:
+ timeout: 600
+ php_loc: true
+ php_pdepend: true
+ php_sim: true
+ php_changetracking: true
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..c946b00
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Pol Dellaiera
+
+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..24d9331
--- /dev/null
+++ b/README.md
@@ -0,0 +1,98 @@
+[![Latest Stable Version][latest stable version]][packagist]
+ [![GitHub stars][github stars]][packagist]
+ [![Total Downloads][total downloads]][packagist]
+ [![GitHub Workflow Status][github workflow status]][github actions]
+ [![Scrutinizer code quality][code quality]][code quality link]
+ [![Type Coverage][type coverage]][sheperd type coverage]
+ [![Code Coverage][code coverage]][code quality link]
+ [![License][license]][packagist]
+ [![Donate!][donate github]][github sponsor]
+ [![Donate!][donate paypal]][paypal sponsor]
+
+# PHPSpec Time
+
+A [PHPSpec][phpspec] extension providing matchers for measuring time in tests.
+
+New matchers:
+
+* `shouldTakeLessThan(float $seconds)`
+* `shouldTakeMoreThan(float $seconds)`
+* `shouldTakeInBetween(float $from, float $to)`
+
+## Installation
+
+`composer require loophp/phpspec-time`
+
+## Usage
+
+Add the extension to the phpspec configuration file:
+
+```yaml
+extensions:
+ loophp\phpspectime\Extension: ~
+```
+
+## Code quality, tests and benchmarks
+
+Every time changes are introduced into the library, [Github][github actions] run the
+tests.
+
+The library has tests written with [PHPSpec][phpspec].
+Feel free to check them out in the `spec` directory. Run `composer phpspec` to trigger the tests.
+
+Before each commit some inspections are executed with [GrumPHP][grumphp],
+run `composer grumphp` to check manually.
+
+The quality of the tests is tested with [Infection][infection] a PHP Mutation testing
+framework, run `composer infection` to try it.
+
+Static analysers are also controlling the code. [PHPStan][phpstan] and
+[PSalm][psalm] are enabled to their maximum level.
+
+## Contributing
+
+Feel free to contribute by sending Github pull requests. I'm quite reactive :-)
+
+If you can't contribute to the code, you can also sponsor me on [Github][github sponsor] or [Paypal][paypal sponsor].
+
+## Changelog
+
+See [CHANGELOG.md][changelog-md] for a changelog based on [git commits][git-commits].
+
+For more detailed changelogs, please check [the release changelogs][changelog-releases].
+
+[latest stable version]: https://img.shields.io/packagist/v/loophp/phpspec-time.svg?style=flat-square
+[packagist]: https://packagist.org/packages/loophp/phpspec-time
+
+[github stars]: https://img.shields.io/github/stars/loophp/phpspec-time.svg?style=flat-square
+
+[total downloads]: https://img.shields.io/packagist/dt/loophp/phpspec-time.svg?style=flat-square
+
+[github workflow status]: https://img.shields.io/github/workflow/status/loophp/phpspec-time/Continuous%20Integration?style=flat-square
+[github actions]: https://github.com/loophp/phpspec-time/actions
+
+[code quality]: https://img.shields.io/scrutinizer/quality/g/loophp/phpspec-time/master.svg?style=flat-square
+[code quality link]: https://scrutinizer-ci.com/g/loophp/phpspec-time/?branch=master
+
+[type coverage]: https://shepherd.dev/github/loophp/phpspec-time/coverage.svg
+[sheperd type coverage]: https://shepherd.dev/github/loophp/phpspec-time
+
+[code coverage]: https://img.shields.io/scrutinizer/coverage/g/loophp/phpspec-time/master.svg?style=flat-square
+[code quality link]: https://img.shields.io/scrutinizer/quality/g/loophp/phpspec-time/master.svg?style=flat-square
+
+[license]: https://img.shields.io/packagist/l/loophp/phpspec-time.svg?style=flat-square
+
+[donate github]: https://img.shields.io/badge/Sponsor-Github-brightgreen.svg?style=flat-square
+[github sponsor]: https://github.com/sponsors/drupol
+
+[donate paypal]: https://img.shields.io/badge/Sponsor-Paypal-brightgreen.svg?style=flat-square
+[paypal sponsor]: https://www.paypal.me/drupol
+
+[phpspec]: http://www.phpspec.net/
+[grumphp]: https://github.com/phpro/grumphp
+[infection]: https://github.com/infection/infection
+[phpstan]: https://github.com/phpstan/phpstan
+[psalm]: https://github.com/vimeo/psalm
+[changelog-md]: https://github.com/loophp/phpspec-time/blob/master/CHANGELOG.md
+[git-commits]: https://github.com/loophp/phpspec-time/commits/master
+[changelog-releases]: https://github.com/loophp/phpspec-time/releases
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..4e9a244
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,43 @@
+{
+ "name": "loophp/phpspec-time",
+ "type": "library",
+ "description": "A PHPSpec extension providing matchers for measuring time in tests.",
+ "keywords": [
+ "phpspec-extension"
+ ],
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Pol Dellaiera",
+ "email": "pol.dellaiera@protonmail.com"
+ }
+ ],
+ "require": {
+ "php": ">= 7.1.3",
+ "devster/ubench": "^2.1"
+ },
+ "require-dev": {
+ "drupol/php-conventions": "^2",
+ "friends-of-phpspec/phpspec-code-coverage": "^4.3.2",
+ "infection/infection": "^0.18",
+ "infection/phpspec-adapter": "^0.1.1",
+ "phpspec/phpspec": "^5.1.2 || ^6.2.1",
+ "phpstan/phpstan-strict-rules": "^0.12",
+ "symfony/var-dumper": "^5.1",
+ "vimeo/psalm": "^3.18.2 || ^4"
+ },
+ "config": {
+ "sort-packages": true
+ },
+ "autoload": {
+ "psr-4": {
+ "loophp\\phpspectime\\": "./src/"
+ }
+ },
+ "scripts": {
+ "bench": "vendor/bin/phpbench run --report='generator: \"table\", cols: [ \"suite\", \"subject\", \"mean\", \"diff\", \"mem_peak\", \"mem_real\"], break: [\"benchmark\"]'",
+ "grumphp": "vendor/bin/grumphp run",
+ "infection": "vendor/bin/infection run -j 2",
+ "phpspec": "vendor/bin/phpspec run"
+ }
+}
diff --git a/grumphp.yml.dist b/grumphp.yml.dist
new file mode 100644
index 0000000..37684f2
--- /dev/null
+++ b/grumphp.yml.dist
@@ -0,0 +1,15 @@
+imports:
+ - { resource: vendor/drupol/php-conventions/config/php71/grumphp.yml }
+
+parameters:
+ extra_tasks:
+ psalm:
+ show_info: true
+ phpspec:
+ verbose: true
+ infection:
+ threads: 10
+ test_framework: phpspec
+ configuration: infection.json.dist
+ min_msi: 50
+ min_covered_msi: 50
diff --git a/infection.json.dist b/infection.json.dist
new file mode 100644
index 0000000..3fe5fe5
--- /dev/null
+++ b/infection.json.dist
@@ -0,0 +1,18 @@
+{
+ "timeout": 10,
+ "source": {
+ "directories": [
+ "src"
+ ]
+ },
+ "logs": {
+ "text": "build/infection.log",
+ "summary": "build/summary.log",
+ "debug": "build/debug.log",
+ "perMutator": "build/per-mutator.md",
+ "badge": {
+ "branch": "master"
+ }
+ },
+ "testFramework":"phpspec"
+}
diff --git a/phpspec.yml.dist b/phpspec.yml.dist
new file mode 100644
index 0000000..3ca81d3
--- /dev/null
+++ b/phpspec.yml.dist
@@ -0,0 +1,12 @@
+formatter.name: pretty
+extensions:
+ FriendsOfPhpSpec\PhpSpec\CodeCoverage\CodeCoverageExtension:
+ format:
+ - html
+ - clover
+ - php
+ - text
+ output:
+ html: build/coverage
+ clover: build/logs/clover.xml
+ php: build/coverage.php
\ No newline at end of file
diff --git a/psalm.xml b/psalm.xml
new file mode 100644
index 0000000..ac026d1
--- /dev/null
+++ b/psalm.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/spec/loophp/phpspectime/ExtensionSpec.php b/spec/loophp/phpspectime/ExtensionSpec.php
new file mode 100644
index 0000000..69324a1
--- /dev/null
+++ b/spec/loophp/phpspectime/ExtensionSpec.php
@@ -0,0 +1,41 @@
+shouldHaveType(Extension::class);
+ }
+
+ public function it_set_extension_matchers(): void
+ {
+ $container = new IndexedServiceContainer();
+
+ $this
+ ->load($container, []);
+
+ $services = [
+ 'matchers.takeMoreThan' => TakeMoreThanMatcher::class,
+ 'matchers.takeLessThan' => TakeLessThanMatcher::class,
+ 'matchers.takeInBetween' => TakeInBetweenMatcher::class,
+ ];
+
+ foreach ($services as $id => $service) {
+ if (false === $container->has($id)) {
+ throw new Exception(sprintf('Matcher %s not found.', $service));
+ }
+ }
+ }
+}
diff --git a/spec/loophp/phpspectime/Matcher/TakeInBetweenMatcherSpec.php b/spec/loophp/phpspectime/Matcher/TakeInBetweenMatcherSpec.php
new file mode 100644
index 0000000..cfa049b
--- /dev/null
+++ b/spec/loophp/phpspectime/Matcher/TakeInBetweenMatcherSpec.php
@@ -0,0 +1,323 @@
+beConstructedWith($unwrapper, $presenter, $bench);
+
+ $this
+ ->supports('takeInBetween', 'bar', [3])
+ ->shouldReturn(false);
+
+ $this
+ ->supports('takeInBetween', 'bar', [3, 9])
+ ->shouldReturn(true);
+
+ $this
+ ->supports('foo', 'bar', [3])
+ ->shouldReturn(false);
+ }
+
+ public function it_can_get_priority()
+ {
+ $this
+ ->getPriority()
+ ->shouldReturn(1);
+ }
+
+ // Inverted
+
+ public function it_detect_when_a_call_is_not_within_the_defined_timespan(Presenter $presenter, Unwrapper $unwrapper, Ubench $bench)
+ {
+ $expected = [5.0, 15.0];
+ $subject = new class() {
+ public function foo()
+ {
+ return 'bar';
+ }
+ };
+
+ $bench
+ ->start()
+ ->willReturn(0);
+
+ $bench
+ ->end()
+ ->willReturn(10);
+
+ $bench
+ ->getTime(true)
+ ->willReturn(20.0);
+
+ $unwrapper
+ ->unwrapAll(Argument::any())
+ ->willReturn(['foo', [$expected]]);
+
+ $presenter
+ ->presentValue(20.0)
+ ->willReturn(20.0);
+
+ $presenter
+ ->presentValue(5.0)
+ ->willReturn(5.0);
+
+ $presenter
+ ->presentValue(15.0)
+ ->willReturn(15.0);
+
+ $presenter
+ ->presentValue(Argument::cetera())
+ ->willReturn(Argument::cetera());
+
+ $this
+ ->beConstructedWith($unwrapper, $presenter, $bench);
+
+ $this
+ ->negativeMatch('foo', $subject, $expected)()
+ ->shouldReturn(true);
+ }
+
+ public function it_detect_when_a_call_within_the_defined_timespan(Presenter $presenter, Unwrapper $unwrapper, Ubench $bench)
+ {
+ $expected = [5.0, 15.0];
+ $subject = new class() {
+ public function foo()
+ {
+ return 'bar';
+ }
+ };
+
+ $bench
+ ->start()
+ ->willReturn(0);
+
+ $bench
+ ->end()
+ ->willReturn(10);
+
+ $bench
+ ->getTime(true)
+ ->willReturn(10.0);
+
+ $unwrapper
+ ->unwrapAll(Argument::any())
+ ->willReturn(['foo', [$expected]]);
+
+ $presenter
+ ->presentValue(10.0)
+ ->willReturn(10.0);
+
+ $presenter
+ ->presentValue(5.0)
+ ->willReturn(5.0);
+
+ $presenter
+ ->presentValue(15.0)
+ ->willReturn(15.0);
+
+ $presenter
+ ->presentValue(Argument::cetera())
+ ->willReturn(Argument::cetera());
+
+ $this
+ ->beConstructedWith($unwrapper, $presenter, $bench);
+
+ $this
+ ->positiveMatch('foo', $subject, $expected)()
+ ->shouldReturn(true);
+ }
+
+ public function it_is_initializable(Presenter $presenter, Unwrapper $unwrapper, Ubench $bench)
+ {
+ $this
+ ->beConstructedWith($unwrapper, $presenter, $bench);
+
+ $this
+ ->shouldHaveType(TakeInBetweenMatcher::class);
+ }
+
+ public function it_throws_when_a_call_is_not_within_the_defined_timespan(Presenter $presenter, Unwrapper $unwrapper, Ubench $bench)
+ {
+ $expected = [5.0, 15.0];
+ $subject = new class() {
+ public function foo()
+ {
+ return 'bar';
+ }
+ };
+
+ $bench
+ ->start()
+ ->willReturn(0);
+
+ $bench
+ ->end()
+ ->willReturn(10);
+
+ $bench
+ ->getTime(true)
+ ->willReturn(10.0);
+
+ $unwrapper
+ ->unwrapAll(Argument::any())
+ ->willReturn(['foo', [$expected]]);
+
+ $presenter
+ ->presentValue(10.0)
+ ->willReturn(10.0);
+
+ $presenter
+ ->presentValue(5.0)
+ ->willReturn(5.0);
+
+ $presenter
+ ->presentValue(15.0)
+ ->willReturn(15.0);
+
+ $presenter
+ ->presentValue(Argument::cetera())
+ ->willReturn(Argument::cetera());
+
+ $this
+ ->beConstructedWith($unwrapper, $presenter, $bench);
+
+ $this
+ ->negativeMatch('foo', $subject, $expected)
+ ->shouldThrow(MatcherException::class)
+ ->during('__invoke');
+ }
+
+ public function it_throws_when_a_call_within_the_defined_timespan(Presenter $presenter, Unwrapper $unwrapper, Ubench $bench)
+ {
+ $expected = [5.0, 15.0];
+ $subject = new class() {
+ public function foo()
+ {
+ return 'bar';
+ }
+ };
+
+ $bench
+ ->start()
+ ->willReturn(0);
+
+ $bench
+ ->end()
+ ->willReturn(20);
+
+ $bench
+ ->getTime(true)
+ ->willReturn(20.0);
+
+ $unwrapper
+ ->unwrapAll(Argument::any())
+ ->willReturn(['foo', [$expected]]);
+
+ $presenter
+ ->presentValue(10.0)
+ ->willReturn(20.0);
+
+ $presenter
+ ->presentValue(5.0)
+ ->willReturn(5.0);
+
+ $presenter
+ ->presentValue(15.0)
+ ->willReturn(15.0);
+
+ $presenter
+ ->presentValue(Argument::cetera())
+ ->willReturn(Argument::cetera());
+
+ $this
+ ->beConstructedWith($unwrapper, $presenter, $bench);
+
+ $this
+ ->positiveMatch('foo', $subject, $expected)
+ ->shouldThrow(MatcherException::class)
+ ->during('__invoke');
+ }
+
+ public function it_throws_when_parameters_are_wrong(Presenter $presenter, Unwrapper $unwrapper, Ubench $bench)
+ {
+ // Inverted parameters.
+ $expected = [15.0, 5.0];
+ $subject = new class() {
+ public function foo()
+ {
+ return 'bar';
+ }
+ };
+
+ $bench
+ ->start()
+ ->willReturn(0);
+
+ $bench
+ ->end()
+ ->willReturn(20);
+
+ $bench
+ ->getTime(true)
+ ->willReturn(20.0);
+
+ $unwrapper
+ ->unwrapAll(Argument::any())
+ ->willReturn(['foo', [$expected]]);
+
+ $presenter
+ ->presentValue(10.0)
+ ->willReturn(20.0);
+
+ $presenter
+ ->presentValue(5.0)
+ ->willReturn(5.0);
+
+ $presenter
+ ->presentValue(15.0)
+ ->willReturn(15.0);
+
+ $presenter
+ ->presentValue(Argument::cetera())
+ ->willReturn(Argument::cetera());
+
+ $this
+ ->beConstructedWith($unwrapper, $presenter, $bench);
+
+ $this
+ ->shouldThrow(MatcherException::class)
+ ->during('positiveMatch', ['foo', $subject, $expected]);
+
+ $expected = ['foo', 5.0];
+
+ $this
+ ->shouldThrow(MatcherException::class)
+ ->during('positiveMatch', ['foo', $subject, $expected]);
+
+ $expected = [15.0, 'bar'];
+
+ $this
+ ->shouldThrow(MatcherException::class)
+ ->during('positiveMatch', ['foo', $subject, $expected]);
+ }
+
+ public function let(Presenter $presenter, Unwrapper $unwrapper, Ubench $bench)
+ {
+ $this
+ ->beConstructedWith($unwrapper, $presenter, $bench);
+ }
+}
diff --git a/spec/loophp/phpspectime/Matcher/TakeLessThanMatcherSpec.php b/spec/loophp/phpspectime/Matcher/TakeLessThanMatcherSpec.php
new file mode 100644
index 0000000..1460906
--- /dev/null
+++ b/spec/loophp/phpspectime/Matcher/TakeLessThanMatcherSpec.php
@@ -0,0 +1,351 @@
+beConstructedWith($unwrapper, $presenter, $bench);
+
+ $this
+ ->supports('takeLessThan', 'bar', [3])
+ ->shouldReturn(true);
+
+ $this
+ ->supports('lastLessThan', 'bar', [3])
+ ->shouldReturn(true);
+
+ $this
+ ->supports('lastLessThan', 'bar', [3, 5])
+ ->shouldReturn(false);
+
+ $this
+ ->supports('foo', 'bar', [3])
+ ->shouldReturn(false);
+ }
+
+ public function it_can_get_priority()
+ {
+ $this
+ ->getPriority()
+ ->shouldReturn(1);
+ }
+
+ public function it_is_initializable(Presenter $presenter, Unwrapper $unwrapper, Ubench $bench)
+ {
+ $this
+ ->beConstructedWith($unwrapper, $presenter, $bench);
+
+ $this
+ ->shouldHaveType(TakeLessThanMatcher::class);
+ }
+
+ // Inverted now
+
+ public function it_returns_true_when_a_call_is_not_shorter_than_expected(Presenter $presenter, Unwrapper $unwrapper, Ubench $bench)
+ {
+ $expected = 10;
+ $subject = new class() {
+ public function foo()
+ {
+ return 'bar';
+ }
+ };
+
+ $bench
+ ->start()
+ ->willReturn(0);
+
+ $bench
+ ->end()
+ ->willReturn(20);
+
+ $bench
+ ->getTime(true)
+ ->willReturn(20.0);
+
+ $unwrapper
+ ->unwrapAll(Argument::any())
+ ->willReturn(['foo', [$expected]]);
+
+ $presenter
+ ->presentValue($expected)
+ ->willReturn($expected);
+
+ $presenter
+ ->presentValue(20.0)
+ ->willReturn(20.0);
+
+ $presenter
+ ->presentValue(Argument::cetera())
+ ->willReturn(Argument::cetera());
+
+ $this
+ ->beConstructedWith($unwrapper, $presenter, $bench);
+
+ $this
+ ->negativeMatch('foo', $subject, [$expected])()
+ ->shouldReturn(true);
+ }
+
+ public function it_returns_true_when_a_call_is_shorter_than_expected(Presenter $presenter, Unwrapper $unwrapper, Ubench $bench)
+ {
+ $expected = 10;
+ $subject = new class() {
+ public function foo()
+ {
+ return 'bar';
+ }
+ };
+
+ $bench
+ ->start()
+ ->willReturn(0);
+
+ $bench
+ ->end()
+ ->willReturn(5);
+
+ $bench
+ ->getTime(true)
+ ->willReturn(5.0);
+
+ $unwrapper
+ ->unwrapAll(Argument::any())
+ ->willReturn(['foo', [$expected]]);
+
+ $presenter
+ ->presentValue($expected)
+ ->willReturn($expected);
+
+ $presenter
+ ->presentValue(5.0)
+ ->willReturn(5.0);
+
+ $presenter
+ ->presentValue(Argument::cetera())
+ ->willReturn(Argument::cetera());
+
+ $this
+ ->beConstructedWith($unwrapper, $presenter, $bench);
+
+ $this
+ ->positiveMatch('foo', $subject, [$expected])()
+ ->shouldReturn(true);
+ }
+
+ public function it_throws_when_a_call_is_not_shorter_than_expected(Presenter $presenter, Unwrapper $unwrapper, Ubench $bench)
+ {
+ $expected = 20;
+ $subject = new class() {
+ public function foo()
+ {
+ return 'bar';
+ }
+ };
+
+ $bench
+ ->start()
+ ->willReturn(0);
+
+ $bench
+ ->end()
+ ->willReturn(10);
+
+ $bench
+ ->getTime(true)
+ ->willReturn(10.0);
+
+ $unwrapper
+ ->unwrapAll(Argument::any())
+ ->willReturn(['foo', [$expected]]);
+
+ $presenter
+ ->presentValue($expected)
+ ->willReturn($expected);
+
+ $presenter
+ ->presentValue(10.0)
+ ->willReturn(10.0);
+
+ $presenter
+ ->presentValue(Argument::cetera())
+ ->willReturn(Argument::cetera());
+
+ $this
+ ->beConstructedWith($unwrapper, $presenter, $bench);
+
+ $this
+ ->negativeMatch('foo', $subject, [$expected])
+ ->shouldThrow(MatcherException::class)
+ ->during('__invoke');
+ }
+
+ public function it_throws_when_a_call_is_shorter_than_expected(Presenter $presenter, Unwrapper $unwrapper, Ubench $bench)
+ {
+ $expected = 10;
+ $subject = new class() {
+ public function foo()
+ {
+ return 'bar';
+ }
+ };
+
+ $bench
+ ->start()
+ ->willReturn(0);
+
+ $bench
+ ->end()
+ ->willReturn(20);
+
+ $bench
+ ->getTime(true)
+ ->willReturn(20.0);
+
+ $unwrapper
+ ->unwrapAll(Argument::any())
+ ->willReturn(['foo', [$expected]]);
+
+ $presenter
+ ->presentValue($expected)
+ ->willReturn($expected);
+
+ $presenter
+ ->presentValue(20.0)
+ ->willReturn(20.0);
+
+ $presenter
+ ->presentValue(Argument::cetera())
+ ->willReturn(Argument::cetera());
+
+ $this
+ ->beConstructedWith($unwrapper, $presenter, $bench);
+
+ $this
+ ->positiveMatch('foo', $subject, [$expected])
+ ->shouldThrow(MatcherException::class)
+ ->during('__invoke');
+ }
+
+ public function it_throws_when_method_is_not_found(Presenter $presenter, Unwrapper $unwrapper, Ubench $bench)
+ {
+ $expected = 10;
+ $subject = new class() {
+ public function foo()
+ {
+ return 'bar';
+ }
+ };
+
+ $bench
+ ->start()
+ ->willReturn(0);
+
+ $bench
+ ->end()
+ ->willReturn(20);
+
+ $bench
+ ->getTime(true)
+ ->willReturn(20.0);
+
+ $unwrapper
+ ->unwrapAll(Argument::any())
+ ->willReturn(['plop', [$expected]]);
+
+ $presenter
+ ->presentValue(10.0)
+ ->willReturn(20.0);
+
+ $presenter
+ ->presentValue(5.0)
+ ->willReturn(5.0);
+
+ $presenter
+ ->presentValue(15.0)
+ ->willReturn(15.0);
+
+ $presenter
+ ->presentValue(Argument::cetera())
+ ->willReturn(Argument::cetera());
+
+ $this
+ ->beConstructedWith($unwrapper, $presenter, $bench);
+
+ $this
+ ->positiveMatch('plop', $subject, [10])
+ ->shouldThrow(MethodNotFoundException::class)
+ ->during('__invoke');
+ }
+
+ public function it_throws_when_parameters_are_wrong(Presenter $presenter, Unwrapper $unwrapper, Ubench $bench)
+ {
+ // Inverted parameters.
+ $expected = 'foo';
+ $subject = new class() {
+ public function foo()
+ {
+ return 'bar';
+ }
+ };
+
+ $bench
+ ->start()
+ ->willReturn(0);
+
+ $bench
+ ->end()
+ ->willReturn(20);
+
+ $bench
+ ->getTime(true)
+ ->willReturn(20.0);
+
+ $unwrapper
+ ->unwrapAll(Argument::any())
+ ->willReturn(['foo', [$expected]]);
+
+ $presenter
+ ->presentValue(10.0)
+ ->willReturn(20.0);
+
+ $presenter
+ ->presentValue(5.0)
+ ->willReturn(5.0);
+
+ $presenter
+ ->presentValue(15.0)
+ ->willReturn(15.0);
+
+ $presenter
+ ->presentValue(Argument::cetera())
+ ->willReturn(Argument::cetera());
+
+ $this
+ ->beConstructedWith($unwrapper, $presenter, $bench);
+
+ $this
+ ->shouldThrow(MatcherException::class)
+ ->during('positiveMatch', ['foo', $subject, [$expected]]);
+ }
+
+ public function let(Presenter $presenter, Unwrapper $unwrapper, Ubench $bench)
+ {
+ $this
+ ->beConstructedWith($unwrapper, $presenter, $bench);
+ }
+}
diff --git a/spec/loophp/phpspectime/Matcher/TakeMoreThanMatcherSpec.php b/spec/loophp/phpspectime/Matcher/TakeMoreThanMatcherSpec.php
new file mode 100644
index 0000000..137b3e9
--- /dev/null
+++ b/spec/loophp/phpspectime/Matcher/TakeMoreThanMatcherSpec.php
@@ -0,0 +1,248 @@
+beConstructedWith($unwrapper, $presenter, $bench);
+
+ $this
+ ->supports('takeMoreThan', 'bar', [3])
+ ->shouldReturn(true);
+
+ $this
+ ->supports('lastMoreThan', 'bar', [3])
+ ->shouldReturn(true);
+
+ $this
+ ->supports('lastMoreThan', 'bar', [3, 5])
+ ->shouldReturn(false);
+
+ $this
+ ->supports('foo', 'bar', [3])
+ ->shouldReturn(false);
+ }
+
+ public function it_can_get_priority()
+ {
+ $this
+ ->getPriority()
+ ->shouldReturn(1);
+ }
+
+ public function it_detect_when_a_call_is_longer_than_expected(Presenter $presenter, Unwrapper $unwrapper, Ubench $bench)
+ {
+ $expected = 1;
+ $subject = new class() {
+ public function foo()
+ {
+ return 'bar';
+ }
+ };
+
+ $bench
+ ->start()
+ ->willReturn(0);
+
+ $bench
+ ->end()
+ ->willReturn(10);
+
+ $bench
+ ->getTime(true)
+ ->willReturn(10.0);
+
+ $unwrapper
+ ->unwrapAll(Argument::any())
+ ->willReturn(['foo', [$expected]]);
+
+ $presenter
+ ->presentValue($expected)
+ ->willReturn($expected);
+
+ $presenter
+ ->presentValue(10.0)
+ ->willReturn(10.0);
+
+ $presenter
+ ->presentValue(Argument::cetera())
+ ->willReturn(Argument::cetera());
+
+ $this
+ ->beConstructedWith($unwrapper, $presenter, $bench);
+
+ $this
+ ->positiveMatch('foo', $subject, [$expected])()
+ ->shouldReturn(true);
+ }
+
+ // Inverted now
+
+ public function it_detect_when_a_call_is_not_longer_than_expected(Presenter $presenter, Unwrapper $unwrapper, Ubench $bench)
+ {
+ $expected = 1;
+ $subject = new class() {
+ public function foo()
+ {
+ return 'bar';
+ }
+ };
+
+ $bench
+ ->start()
+ ->willReturn(0);
+
+ $bench
+ ->end()
+ ->willReturn(10);
+
+ $bench
+ ->getTime(true)
+ ->willReturn(10.0);
+
+ $unwrapper
+ ->unwrapAll(Argument::any())
+ ->willReturn(['foo', [$expected]]);
+
+ $presenter
+ ->presentValue($expected)
+ ->willReturn($expected);
+
+ $presenter
+ ->presentValue(10.0)
+ ->willReturn(10.0);
+
+ $presenter
+ ->presentValue(Argument::cetera())
+ ->willReturn(Argument::cetera());
+
+ $this
+ ->beConstructedWith($unwrapper, $presenter, $bench);
+
+ $this
+ ->negativeMatch('foo', $subject, [$expected])
+ ->shouldThrow(MatcherException::class)
+ ->during('__invoke');
+ }
+
+ public function it_detect_when_a_call_is_not_shorter_than_expected(Presenter $presenter, Unwrapper $unwrapper, Ubench $bench)
+ {
+ $expected = 20;
+ $subject = new class() {
+ public function foo()
+ {
+ return 'bar';
+ }
+ };
+
+ $bench
+ ->start()
+ ->willReturn(0);
+
+ $bench
+ ->end()
+ ->willReturn(10);
+
+ $bench
+ ->getTime(true)
+ ->willReturn(10.0);
+
+ $unwrapper
+ ->unwrapAll(Argument::any())
+ ->willReturn(['foo', [$expected]]);
+
+ $presenter
+ ->presentValue($expected)
+ ->willReturn($expected);
+
+ $presenter
+ ->presentValue(10.0)
+ ->willReturn(10.0);
+
+ $presenter
+ ->presentValue(Argument::cetera())
+ ->willReturn(Argument::cetera());
+
+ $this
+ ->beConstructedWith($unwrapper, $presenter, $bench);
+
+ $this
+ ->negativeMatch('foo', $subject, [$expected])()
+ ->shouldReturn(true);
+ }
+
+ public function it_detect_when_a_call_is_shorter_than_expected(Presenter $presenter, Unwrapper $unwrapper, Ubench $bench)
+ {
+ $expected = 20;
+ $subject = new class() {
+ public function foo()
+ {
+ return 'bar';
+ }
+ };
+
+ $bench
+ ->start()
+ ->willReturn(0);
+
+ $bench
+ ->end()
+ ->willReturn(10);
+
+ $bench
+ ->getTime(true)
+ ->willReturn(10.0);
+
+ $unwrapper
+ ->unwrapAll(Argument::any())
+ ->willReturn(['foo', [$expected]]);
+
+ $presenter
+ ->presentValue($expected)
+ ->willReturn($expected);
+
+ $presenter
+ ->presentValue(10.0)
+ ->willReturn(10.0);
+
+ $presenter
+ ->presentValue(Argument::cetera())
+ ->willReturn(Argument::cetera());
+
+ $this
+ ->beConstructedWith($unwrapper, $presenter, $bench);
+
+ $this
+ ->positiveMatch('foo', $subject, [$expected])
+ ->shouldThrow(MatcherException::class)
+ ->during('__invoke');
+ }
+
+ public function it_is_initializable(Presenter $presenter, Unwrapper $unwrapper, Ubench $bench)
+ {
+ $this
+ ->beConstructedWith($unwrapper, $presenter, $bench);
+
+ $this
+ ->shouldHaveType(TakeMoreThanMatcher::class);
+ }
+
+ public function let(Presenter $presenter, Unwrapper $unwrapper, Ubench $bench)
+ {
+ $this
+ ->beConstructedWith($unwrapper, $presenter, $bench);
+ }
+}
diff --git a/src/Extension.php b/src/Extension.php
new file mode 100644
index 0000000..ffdbdd8
--- /dev/null
+++ b/src/Extension.php
@@ -0,0 +1,46 @@
+define(
+ 'matchers.takeMoreThan',
+ static function (IndexedServiceContainer $c) {
+ return new TakeMoreThanMatcher($c->get('unwrapper'), $c->get('formatter.presenter'), new Ubench());
+ },
+ ['matchers']
+ );
+
+ $container->define(
+ 'matchers.takeLessThan',
+ static function (IndexedServiceContainer $c) {
+ return new TakeLessThanMatcher($c->get('unwrapper'), $c->get('formatter.presenter'), new Ubench());
+ },
+ ['matchers']
+ );
+
+ $container->define(
+ 'matchers.takeInBetween',
+ static function (IndexedServiceContainer $c) {
+ return new TakeInBetweenMatcher($c->get('unwrapper'), $c->get('formatter.presenter'), new Ubench());
+ },
+ ['matchers']
+ );
+ }
+}
diff --git a/src/Matcher/AbstractTakeThanMatcher.php b/src/Matcher/AbstractTakeThanMatcher.php
new file mode 100644
index 0000000..2598d18
--- /dev/null
+++ b/src/Matcher/AbstractTakeThanMatcher.php
@@ -0,0 +1,144 @@
+
+ * (c) Konstantin Kudryashov
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace loophp\phpspectime\Matcher;
+
+use PhpSpec\Exception\Example\MatcherException;
+use PhpSpec\Exception\Fracture\MethodNotFoundException;
+use PhpSpec\Formatter\Presenter\Presenter;
+use PhpSpec\Matcher\Matcher;
+use PhpSpec\Wrapper\DelayedCall;
+use PhpSpec\Wrapper\Unwrapper;
+use Ubench;
+
+use function call_user_func;
+use function call_user_func_array;
+use function count;
+use function get_class;
+use function in_array;
+
+abstract class AbstractTakeThanMatcher implements Matcher
+{
+ /**
+ * @var Ubench
+ */
+ protected $bench;
+
+ protected $keywords;
+
+ /**
+ * @var \PhpSpec\Formatter\Presenter\Presenter
+ */
+ protected $presenter;
+
+ /**
+ * @var \PhpSpec\Wrapper\Unwrapper
+ */
+ protected $unwrapper;
+
+ public function __construct(Unwrapper $unwrapper, Presenter $presenter, Ubench $bench)
+ {
+ $this->unwrapper = $unwrapper;
+ $this->presenter = $presenter;
+ $this->bench = $bench;
+ }
+
+ public function getPriority(): int
+ {
+ return 1;
+ }
+
+ public function negativeMatch(string $name, $subject, array $arguments): DelayedCall
+ {
+ return $this->getDelayedCall([$this, 'verifyNegative'], $subject, $arguments);
+ }
+
+ public function positiveMatch(string $name, $subject, array $arguments): DelayedCall
+ {
+ return $this->getDelayedCall([$this, 'verifyPositive'], $subject, $arguments);
+ }
+
+ /**
+ * @psalm-suppress UndefinedDocblockClass
+ *
+ * @param mixed $subject
+ */
+ public function supports(string $name, $subject, array $arguments): bool
+ {
+ return in_array($name, $this->keywords, true) && 1 === count($arguments);
+ }
+
+ /**
+ * @return float|string
+ */
+ protected function bench(callable $callable, array $arguments)
+ {
+ $this->bench->start();
+ call_user_func_array($callable, $arguments);
+ $this->bench->end();
+
+ return $this->bench->getTime(true);
+ }
+
+ protected function getDelayedCall(callable $check, $subject, array $arguments): DelayedCall
+ {
+ $timeInSeconds = $this->getParameters($arguments);
+ $unwrapper = $this->unwrapper;
+
+ return new DelayedCall(
+ static function ($method, $arguments) use ($check, $subject, $timeInSeconds, $unwrapper) {
+ $arguments = $unwrapper->unwrapAll($arguments);
+
+ $methodName = $arguments[0];
+ $arguments = $arguments[1] ?? [];
+ $callable = [$subject, $methodName];
+
+ [$class, $methodName] = [$subject, $methodName];
+
+ if (!method_exists($class, $methodName) && !method_exists($class, '__call')) {
+ throw new MethodNotFoundException(
+ sprintf('Method %s::%s not found.', get_class($class), $methodName),
+ $class,
+ $methodName,
+ $arguments
+ );
+ }
+
+ return call_user_func($check, $callable, $arguments, $timeInSeconds);
+ }
+ );
+ }
+
+ /**
+ * @return numeric
+ */
+ protected function getParameters(array $arguments)
+ {
+ $timeInSeconds = current($arguments);
+
+ if (!is_numeric($timeInSeconds)) {
+ throw new MatcherException(
+ sprintf(
+ "Wrong argument provided in throw matcher.\n" .
+ "Fully qualified classname or exception instance expected,\n" .
+ 'Got %s.',
+ $this->presenter->presentValue($arguments[0])
+ )
+ );
+ }
+
+ return $timeInSeconds;
+ }
+}
diff --git a/src/Matcher/TakeInBetweenMatcher.php b/src/Matcher/TakeInBetweenMatcher.php
new file mode 100644
index 0000000..5615014
--- /dev/null
+++ b/src/Matcher/TakeInBetweenMatcher.php
@@ -0,0 +1,104 @@
+keywords, true) && 2 === count($arguments);
+ }
+
+ public function verifyNegative(callable $callable, array $arguments, array $parameters): bool
+ {
+ [$from, $to] = $parameters;
+
+ $elapsedTime = $this->bench($callable, $arguments);
+
+ if ($elapsedTime < $from || $elapsedTime > $to) {
+ return true;
+ }
+
+ throw TimeMatcherException::notInRangeMatcherException(
+ $this->presenter,
+ $elapsedTime,
+ $from,
+ $to
+ );
+ }
+
+ public function verifyPositive(callable $callable, array $arguments, array $parameters): bool
+ {
+ [$from, $to] = $parameters;
+
+ $elapsedTime = $this->bench($callable, $arguments);
+
+ if ($elapsedTime >= $from && $elapsedTime <= $to) {
+ return true;
+ }
+
+ throw TimeMatcherException::notInRangeMatcherException(
+ $this->presenter,
+ $elapsedTime,
+ $from,
+ $to
+ );
+ }
+
+ protected function getParameters(array $arguments): array
+ {
+ $from = current($arguments);
+ $to = end($arguments);
+
+ if (!is_numeric($from)) {
+ throw new MatcherException(
+ sprintf(
+ "Wrong 'from' argument provided in TakeInBetween matcher.\n" .
+ "Numeric value expected,\n" .
+ 'Got %s.',
+ $this->presenter->presentValue($arguments[0])
+ )
+ );
+ }
+
+ if (!is_numeric($to)) {
+ throw new MatcherException(
+ sprintf(
+ "Wrong 'to' argument provided in TakeInBetween matcher.\n" .
+ "Numeric value expected,\n" .
+ 'Got %s.',
+ $this->presenter->presentValue($arguments[0])
+ )
+ );
+ }
+
+ if ($to < $from) {
+ throw new MatcherException(
+ sprintf(
+ "Wrong argument provided in TakeInBetween matcher.\n" .
+ "First argument should be equal or greater to second argument.\n"
+ )
+ );
+ }
+
+ return [$from, $to];
+ }
+}
diff --git a/src/Matcher/TakeLessThanMatcher.php b/src/Matcher/TakeLessThanMatcher.php
new file mode 100644
index 0000000..750a0e8
--- /dev/null
+++ b/src/Matcher/TakeLessThanMatcher.php
@@ -0,0 +1,45 @@
+bench($callable, $arguments);
+
+ if (false === $timeInSeconds > $elapsedTime) {
+ return true;
+ }
+
+ throw TimeMatcherException::tooFastMatcherException(
+ $this->presenter,
+ $elapsedTime,
+ $timeInSeconds
+ );
+ }
+
+ public function verifyPositive(callable $callable, array $arguments, float $timeInSeconds): bool
+ {
+ $elapsedTime = $this->bench($callable, $arguments);
+
+ if (false === $timeInSeconds <= $elapsedTime) {
+ return true;
+ }
+
+ throw TimeMatcherException::tooSlowMatcherException(
+ $this->presenter,
+ $elapsedTime,
+ $timeInSeconds
+ );
+ }
+}
diff --git a/src/Matcher/TakeMoreThanMatcher.php b/src/Matcher/TakeMoreThanMatcher.php
new file mode 100644
index 0000000..96a2cfa
--- /dev/null
+++ b/src/Matcher/TakeMoreThanMatcher.php
@@ -0,0 +1,45 @@
+bench($callable, $arguments);
+
+ if (false === $timeInSeconds <= $elapsedTime) {
+ return true;
+ }
+
+ throw TimeMatcherException::tooSlowMatcherException(
+ $this->presenter,
+ $elapsedTime,
+ $timeInSeconds
+ );
+ }
+
+ public function verifyPositive(callable $callable, array $arguments, float $timeInSeconds): bool
+ {
+ $elapsedTime = $this->bench($callable, $arguments);
+
+ if (false === $timeInSeconds > $elapsedTime) {
+ return true;
+ }
+
+ throw TimeMatcherException::tooFastMatcherException(
+ $this->presenter,
+ $elapsedTime,
+ $timeInSeconds
+ );
+ }
+}
diff --git a/src/TimeMatcherException.php b/src/TimeMatcherException.php
new file mode 100644
index 0000000..bc00d5e
--- /dev/null
+++ b/src/TimeMatcherException.php
@@ -0,0 +1,51 @@
+presentValue($from),
+ $presenter->presentValue($to),
+ $presenter->presentValue($result)
+ )
+ );
+ }
+
+ public static function tooFastMatcherException(Presenter $presenter, $result, float $expected): MatcherException
+ {
+ return new MatcherException(
+ sprintf(
+ "Method call too fast to complete.\n" .
+ "%s second(s) expected,\n" .
+ 'Got %s.',
+ $presenter->presentValue($expected),
+ $presenter->presentValue($result)
+ )
+ );
+ }
+
+ public static function tooSlowMatcherException(Presenter $presenter, $result, float $expected): MatcherException
+ {
+ return new MatcherException(
+ sprintf(
+ "Method call too slow to complete.\n" .
+ "%s second(s) expected,\n" .
+ 'Got %s.',
+ $presenter->presentValue($expected),
+ $presenter->presentValue($result)
+ )
+ );
+ }
+}