├── leo.png ├── message.png ├── apigen.neon ├── .gitignore ├── src ├── Interfaces │ ├── _interface.bdd.php │ ├── interface.bdd.php │ ├── Assert │ │ ├── CollectionAssertTrait.php │ │ ├── ObjectAssertTrait.php │ │ └── TypeAssertTrait.php │ └── Assert.php ├── Responder │ ├── ResponderInterface.php │ ├── ExceptionResponder.php │ └── Exception │ │ └── AssertionException.php ├── ObjectPath │ ├── ObjectPathValue.php │ └── ObjectPath.php ├── Matcher │ ├── NullMatcher.php │ ├── EmptyMatcher.php │ ├── TrueMatcher.php │ ├── TruthyMatcher.php │ ├── EqualMatcher.php │ ├── SameMatcher.php │ ├── InstanceofMatcher.php │ ├── PredicateMatcher.php │ ├── PatternMatcher.php │ ├── SubStringMatcher.php │ ├── GreaterThanMatcher.php │ ├── TypeMatcher.php │ ├── LessThanMatcher.php │ ├── LessThanOrEqualMatcher.php │ ├── AbstractMatcher.php │ ├── GreaterThanOrEqualMatcher.php │ ├── InclusionMatcher.php │ ├── Template │ │ ├── TemplateInterface.php │ │ └── ArrayTemplate.php │ ├── MatcherTrait.php │ ├── MatcherInterface.php │ ├── LengthMatcher.php │ ├── Match.php │ ├── CountableMatcher.php │ ├── RangeMatcher.php │ ├── KeysMatcher.php │ ├── ExceptionMatcher.php │ └── PropertyMatcher.php ├── Formatter │ ├── FormatterInterface.php │ └── Formatter.php ├── DynamicObjectTrait.php ├── Leo.php ├── Core │ └── Definitions.php └── Assertion.php ├── specs ├── fixtures │ └── extend.php ├── matcher │ ├── equal-matcher.spec.php │ ├── same-matcher.spec.php │ ├── type-matcher.spec.php │ ├── null-matcher.spec.php │ ├── true-matcher.spec.php │ ├── empty-matcher.spec.php │ ├── truthy-matcher.spec.php │ ├── less-than-matcher.spec.php │ ├── less-than-or-equal-matcher.spec.php │ ├── greater-than-or-equal-matcher.spec.php │ ├── instanceof-matcher.spec.php │ ├── predicate-matcher.spec.php │ ├── template │ │ └── array-template.spec.php │ ├── match.spec.php │ ├── pattern-matcher.spec.php │ ├── sub-string-matcher.spec.php │ ├── matcher.spec.php │ ├── inclusion-matcher.spec.php │ ├── length-matcher.spec.php │ ├── greater-than-matcher.spec.php │ ├── range-matcher.spec.php │ ├── keys-matcher.spec.php │ ├── property-matcher.spec.php │ └── exception-matcher.spec.php ├── leo.spec.php ├── object-path │ └── object-path.spec.php ├── formatter │ └── formatter.spec.php ├── responder │ ├── responder.spec.php │ └── exception │ │ └── assertion-exception.spec.php └── assertion.spec.php ├── scripts ├── hhvm ├── travis └── travis-after ├── appveyor.yml ├── .scrutinizer.yml ├── Makefile ├── composer.json ├── CHANGELOG.md ├── LICENSE ├── peridot.php ├── .travis.yml ├── CONTRIBUTING.md ├── .php_cs └── README.md /leo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peridot-php/leo/HEAD/leo.png -------------------------------------------------------------------------------- /message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peridot-php/leo/HEAD/message.png -------------------------------------------------------------------------------- /apigen.neon: -------------------------------------------------------------------------------- 1 | destination: docs 2 | source: 3 | - src 4 | 5 | title: Leo 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage/ 2 | /css/ 3 | /docs/ 4 | /img/ 5 | /tmp/ 6 | /vendor/ 7 | -------------------------------------------------------------------------------- /src/Interfaces/_interface.bdd.php: -------------------------------------------------------------------------------- 1 | addMethod('fixture', function () { 7 | return 5; 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /scripts/hhvm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | docker run -it --rm \ 3 | -v "${PWD}:/usr/src/peridot" \ 4 | -w /usr/src/peridot \ 5 | -e "PATH=vendor/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \ 6 | hhvm/hhvm "$@" 7 | -------------------------------------------------------------------------------- /src/Interfaces/interface.bdd.php: -------------------------------------------------------------------------------- 1 | getAssertion(); 18 | 19 | return $assertion->setActual($actual); 20 | } 21 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{branch}-{build}" 2 | build: false 3 | clone_folder: C:\projects\leo 4 | 5 | install: 6 | - cinst OpenSSL.Light -y 7 | - SET PATH=C:\Program Files\OpenSSL;%PATH% 8 | - cinst php -version 7.0.7 -y 9 | - cd c:\tools\php 10 | - copy php.ini-production php.ini 11 | - echo date.timezone="UTC" >> php.ini 12 | - echo extension_dir=ext >> php.ini 13 | - echo extension=php_openssl.dll >> php.ini 14 | - SET PATH=C:\tools\php;%PATH% 15 | - cd C:\projects\leo 16 | - php -r "readfile('http://getcomposer.org/installer');" | php 17 | - php composer.phar install --prefer-source 18 | 19 | test_script: 20 | - cd C:\projects\leo 21 | - vendor/bin/peridot specs 22 | -------------------------------------------------------------------------------- /scripts/travis: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | if [[ -z "$TRAVIS_PHP_VERSION" ]]; then 6 | echo 'TRAVIS_PHP_VERSION not defined.' 7 | 8 | exit 1 9 | fi 10 | 11 | if [[ -z "$TRAVIS_BRANCH" ]]; then 12 | echo 'TRAVIS_BRANCH not defined.' 13 | 14 | exit 1 15 | fi 16 | 17 | if [[ -z "$TRAVIS_PULL_REQUEST" ]]; then 18 | echo 'TRAVIS_PULL_REQUEST not defined.' 19 | 20 | exit 1 21 | fi 22 | 23 | if [[ -z "$PERIDOT_PUBLISH_VERSION" ]]; then 24 | echo 'PERIDOT_PUBLISH_VERSION not defined.' 25 | 26 | exit 1 27 | fi 28 | 29 | if [[ "$TRAVIS_PHP_VERSION" == "$PERIDOT_PUBLISH_VERSION" ]]; then 30 | make ci-coverage 31 | else 32 | make test 33 | fi 34 | -------------------------------------------------------------------------------- /src/Responder/ResponderInterface.php: -------------------------------------------------------------------------------- 1 | expected = 4; 8 | $this->matcher = new EqualMatcher($this->expected); 9 | }); 10 | 11 | describe('->match()', function () { 12 | it('should return true result if actual value is the loosely equal to expected', function () { 13 | expect($this->matcher->match('4')->isMatch())->to->equal(true); 14 | }); 15 | 16 | context('when inverted', function () { 17 | it('should return false result if actual value is loosely equal to expected', function () { 18 | expect($this->matcher->invert()->match($this->expected)->isMatch())->to->equal(false); 19 | }); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /specs/matcher/same-matcher.spec.php: -------------------------------------------------------------------------------- 1 | expected = new stdClass(); 8 | $this->matcher = new SameMatcher($this->expected); 9 | }); 10 | 11 | describe('->match()', function () { 12 | it('should return true result if actual value is the same as expected', function () { 13 | expect($this->matcher->match($this->expected)->isMatch())->to->equal(true); 14 | }); 15 | 16 | context('when inverted', function () { 17 | it('should return false result if actual value is the same as expected', function () { 18 | expect($this->matcher->invert()->match($this->expected)->isMatch())->to->equal(false); 19 | }); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /specs/matcher/type-matcher.spec.php: -------------------------------------------------------------------------------- 1 | expected = 'object'; 8 | $this->matcher = new TypeMatcher($this->expected); 9 | }); 10 | 11 | describe('->match()', function () { 12 | it('should return true result if actual value the same type as the expected', function () { 13 | expect($this->matcher->match(new stdClass())->isMatch())->to->equal(true); 14 | }); 15 | 16 | context('when inverted', function () { 17 | it('should return false result if actual type and expected are the same', function () { 18 | expect($this->matcher->invert()->match(new stdClass())->isMatch())->to->equal(false); 19 | }); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /specs/matcher/null-matcher.spec.php: -------------------------------------------------------------------------------- 1 | matcher = new NullMatcher(); 8 | }); 9 | 10 | it('should return true for null', function () { 11 | $match = $this->matcher->match(null); 12 | expect($match->isMatch())->to->equal(true); 13 | }); 14 | 15 | it('should return false for not null', function () { 16 | $match = $this->matcher->match('true'); 17 | expect($match->isMatch())->to->equal(false); 18 | }); 19 | 20 | context('when negated', function () { 21 | it('should return false when value is null', function () { 22 | $match = $this->matcher->invert()->match(null); 23 | expect($match->isMatch())->to->equal(false); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /specs/matcher/true-matcher.spec.php: -------------------------------------------------------------------------------- 1 | matcher = new TrueMatcher(); 8 | }); 9 | 10 | it('should return true for a true', function () { 11 | $match = $this->matcher->match(true); 12 | expect($match->isMatch())->to->equal(true); 13 | }); 14 | 15 | it('should return false for not true', function () { 16 | $match = $this->matcher->match('true'); 17 | expect($match->isMatch())->to->equal(false); 18 | }); 19 | 20 | context('when negated', function () { 21 | it('should return false when value is true', function () { 22 | $match = $this->matcher->invert()->match(true); 23 | expect($match->isMatch())->to->equal(false); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: install 2 | php --version 3 | vendor/bin/peridot --version 4 | vendor/bin/peridot 5 | 6 | coverage: install 7 | php --version 8 | vendor/bin/peridot --version 9 | phpdbg -qrr vendor/bin/peridot -r html-code-coverage --code-coverage-path "coverage" 10 | 11 | ci-coverage: install 12 | php --version 13 | vendor/bin/peridot --version 14 | phpdbg -qrr vendor/bin/peridot -r clover-code-coverage --code-coverage-path "coverage/clover.xml" 15 | vendor/bin/peridot 16 | 17 | hhvm-test: install 18 | scripts/hhvm vendor/bin/peridot 19 | 20 | lint: install 21 | vendor/bin/php-cs-fixer fix 22 | 23 | install: vendor/autoload.php 24 | 25 | docs: install 26 | vendor/bin/apigen generate 27 | 28 | .PHONY: test coverage ci-coverage hhvm-test lint install 29 | 30 | vendor/autoload.php: composer.lock 31 | composer install 32 | 33 | composer.lock: composer.json 34 | composer update 35 | -------------------------------------------------------------------------------- /scripts/travis-after: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | if [[ -z "$TRAVIS_PHP_VERSION" ]]; then 6 | echo 'TRAVIS_PHP_VERSION not defined.' 7 | 8 | exit 1 9 | fi 10 | 11 | if [[ -z "$TRAVIS_BRANCH" ]]; then 12 | echo 'TRAVIS_BRANCH not defined.' 13 | 14 | exit 1 15 | fi 16 | 17 | if [[ -z "$TRAVIS_PULL_REQUEST" ]]; then 18 | echo 'TRAVIS_PULL_REQUEST not defined.' 19 | 20 | exit 1 21 | fi 22 | 23 | if [[ -z "$PERIDOT_PUBLISH_VERSION" ]]; then 24 | echo 'PERIDOT_PUBLISH_VERSION not defined.' 25 | 26 | exit 1 27 | fi 28 | 29 | echo "TRAVIS_PHP_VERSION is '$TRAVIS_PHP_VERSION'" 30 | echo "TRAVIS_BRANCH is '$TRAVIS_BRANCH'" 31 | echo "TRAVIS_TAG is '$TRAVIS_TAG'" 32 | echo "TRAVIS_PULL_REQUEST is '$TRAVIS_PULL_REQUEST'" 33 | 34 | if [[ "$TRAVIS_PHP_VERSION" == "$PERIDOT_PUBLISH_VERSION" ]]; then 35 | bash <(curl -s https://codecov.io/bash) 36 | fi 37 | -------------------------------------------------------------------------------- /specs/matcher/empty-matcher.spec.php: -------------------------------------------------------------------------------- 1 | matcher = new EmptyMatcher(); 8 | }); 9 | 10 | it('should return true for empty value', function () { 11 | $match = $this->matcher->match([]); 12 | expect($match->isMatch())->to->equal(true); 13 | }); 14 | 15 | it('should return false for non empty value', function () { 16 | $match = $this->matcher->match([1, 2, 3]); 17 | expect($match->isMatch())->to->equal(false); 18 | }); 19 | 20 | context('when negated', function () { 21 | it('should return false when value is empty', function () { 22 | $match = $this->matcher->invert()->match([]); 23 | expect($match->isMatch())->to->equal(false); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /specs/matcher/truthy-matcher.spec.php: -------------------------------------------------------------------------------- 1 | matcher = new TruthyMatcher(); 8 | }); 9 | 10 | it('should return true for a truthy value', function () { 11 | $match = $this->matcher->match([1, 2, 3]); 12 | expect($match->isMatch())->to->equal(true); 13 | }); 14 | 15 | it('should return false for a falsy value', function () { 16 | $match = $this->matcher->match(null); 17 | expect($match->isMatch())->to->equal(false); 18 | }); 19 | 20 | context('when negated', function () { 21 | it('should return false when value is truthy', function () { 22 | $match = $this->matcher->invert()->match([1, 2, 3]); 23 | expect($match->isMatch())->to->equal(false); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "peridot-php/leo", 3 | "description": "Next level assertion and matcher library for PHP", 4 | "keywords": ["assert", "matcher", "expect", "expectation", "testing"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Brian Scaturro", 9 | "email": "scaturrob@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=5.4" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "Peridot\\Leo\\": "src" 18 | }, 19 | "files": ["src/Interfaces/_interface.bdd.php"] 20 | }, 21 | "require-dev": { 22 | "apigen/apigen": "^4", 23 | "friendsofphp/php-cs-fixer": "^1", 24 | "peridot-php/peridot-jumpstart": "^1", 25 | "peridot-php/peridot-prophecy-plugin": "^1" 26 | }, 27 | "config": { 28 | "platform": { 29 | "php": "5.4" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /specs/matcher/less-than-matcher.spec.php: -------------------------------------------------------------------------------- 1 | matcher = new LessThanMatcher(5); 8 | }); 9 | 10 | it('should return true if actual value is less than expected', function () { 11 | $match = $this->matcher->match(4); 12 | expect($match->isMatch())->to->be->true; 13 | }); 14 | 15 | it('should return false if actual value is greater than expected', function () { 16 | $match = $this->matcher->match(6); 17 | expect($match->isMatch())->to->be->false; 18 | }); 19 | 20 | context('when negated', function () { 21 | it('should return false if actual value is below expected', function () { 22 | $match = $this->matcher->invert()->match(4); 23 | expect($match->isMatch())->to->be->false; 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Leo changelog 2 | 3 | ## 1.6.1 (2017-08-17) 4 | 5 | - **[FIXED]** Support for PHP 7.2 ([#28] - thanks [@tomdxw]). 6 | 7 | [#28]: https://github.com/peridot-php/leo/pull/28 8 | [@tomdxw]: https://github.com/tomdxw 9 | 10 | ## 1.6.0 (2016-09-21) 11 | 12 | - **[NEW]** Support for PHP 7 engine exceptions in `ExceptionMatcher` 13 | ([#19] - thanks [@jmalloc]). 14 | - **[NEW]** Support for traversables in `InclusionMatcher` ([#23]). 15 | - **[FIXED]** Using both arguments of `throw()` no longer ignores the exception 16 | type ([#20], [#24]). 17 | - **[MAINTENANCE]** Simplified exception stack trace trimming ([#22]). 18 | 19 | [#19]: https://github.com/peridot-php/leo/pull/19 20 | [#20]: https://github.com/peridot-php/leo/issues/20 21 | [#22]: https://github.com/peridot-php/leo/pull/22 22 | [#23]: https://github.com/peridot-php/leo/pull/23 23 | [#24]: https://github.com/peridot-php/leo/pull/24 24 | [@jmalloc]: https://github.com/jmalloc 25 | -------------------------------------------------------------------------------- /specs/matcher/less-than-or-equal-matcher.spec.php: -------------------------------------------------------------------------------- 1 | matcher = new LessThanOrEqualMatcher(3); 8 | }); 9 | 10 | it('should return true if actual value is at most expected', function () { 11 | $match = $this->matcher->match(3); 12 | expect($match->isMatch())->to->be->true; 13 | }); 14 | 15 | it('should return false if actual value is more than expected', function () { 16 | $match = $this->matcher->match(5); 17 | expect($match->isMatch())->to->be->false; 18 | }); 19 | 20 | context('when negated', function () { 21 | it('should return false if actual value is at most expected', function () { 22 | $match = $this->matcher->invert()->match(3); 23 | expect($match->isMatch())->to->be->false; 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /specs/matcher/greater-than-or-equal-matcher.spec.php: -------------------------------------------------------------------------------- 1 | matcher = new GreaterThanOrEqualMatcher(3); 8 | }); 9 | 10 | it('should return true if actual value is at least expected', function () { 11 | $match = $this->matcher->match(3); 12 | expect($match->isMatch())->to->be->true; 13 | }); 14 | 15 | it('should return false if actual value is less than expected', function () { 16 | $match = $this->matcher->match(2); 17 | expect($match->isMatch())->to->be->false; 18 | }); 19 | 20 | context('when negated', function () { 21 | it('should return false if actual value is at least expected', function () { 22 | $match = $this->matcher->invert()->match(3); 23 | expect($match->isMatch())->to->be->false; 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /specs/matcher/instanceof-matcher.spec.php: -------------------------------------------------------------------------------- 1 | matcher = new InstanceofMatcher('stdClass'); 8 | }); 9 | 10 | it('should return true if object is instance of expected', function () { 11 | $result = $this->matcher->match(new stdClass()); 12 | expect($result->isMatch())->to->be->true; 13 | }); 14 | 15 | it('should return false if object is not instance of expected', function () { 16 | $result = $this->matcher->match([]); 17 | expect($result->isMatch())->to->be->false; 18 | }); 19 | 20 | context('when negated', function () { 21 | it('should return false if object is instanceof expected', function () { 22 | $result = $this->matcher->invert()->match(new stdClass()); 23 | expect($result->isMatch())->to->be->false; 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/ObjectPath/ObjectPathValue.php: -------------------------------------------------------------------------------- 1 | propertyName = $name; 29 | $this->propertyValue = $value; 30 | } 31 | 32 | /** 33 | * @return string 34 | */ 35 | public function getPropertyName() 36 | { 37 | return $this->propertyName; 38 | } 39 | 40 | /** 41 | * @return mixed 42 | */ 43 | public function getPropertyValue() 44 | { 45 | return $this->propertyValue; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /specs/matcher/predicate-matcher.spec.php: -------------------------------------------------------------------------------- 1 | matcher = new PredicateMatcher(function ($num) { 8 | return $num > 1; 9 | }); 10 | }); 11 | 12 | it('should return true if actual value satisfies callable', function () { 13 | $result = $this->matcher->match(2); 14 | expect($result->isMatch())->to->be->true; 15 | }); 16 | 17 | it('should return false if actual value does not satisfy callable', function () { 18 | $result = $this->matcher->match(1); 19 | expect($result->isMatch())->to->be->false; 20 | }); 21 | 22 | context('when negated', function () { 23 | it('return false if actual value satisfies callable', function () { 24 | $result = $this->matcher->invert()->match(2); 25 | expect($result->isMatch())->to->be->false; 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/Matcher/NullMatcher.php: -------------------------------------------------------------------------------- 1 | 'Expected {{actual}} to be null', 39 | 'negated' => 'Expected {{actual}} not to be null', 40 | ]); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /specs/matcher/template/array-template.spec.php: -------------------------------------------------------------------------------- 1 | template = new ArrayTemplate([ 8 | 'default' => 'default', 9 | 'negated' => 'negated', 10 | ]); 11 | }); 12 | 13 | describe('default template accessors', function () { 14 | it('should allow access to default template', function () { 15 | $tpl = 'newdefault'; 16 | $this->template->setDefaultTemplate($tpl); 17 | expect($this->template->getDefaultTemplate())->to->equal($tpl); 18 | }); 19 | }); 20 | 21 | describe('negated template accessors', function () { 22 | it('should allow access to negated template', function () { 23 | $tpl = 'newnegated'; 24 | $this->template->setNegatedTemplate($tpl); 25 | expect($this->template->getNegatedTemplate())->to->equal($tpl); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/Matcher/EmptyMatcher.php: -------------------------------------------------------------------------------- 1 | 'Expected {{actual}} to be empty', 38 | 'negated' => 'Expected {{actual}} not to be empty', 39 | ]); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Matcher/TrueMatcher.php: -------------------------------------------------------------------------------- 1 | 'Expected {{actual}} to be true', 39 | 'negated' => 'Expected {{actual}} to be false', 40 | ]); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Matcher/TruthyMatcher.php: -------------------------------------------------------------------------------- 1 | 'Expected {{actual}} to be truthy', 39 | 'negated' => 'Expected {{actual}} to be falsy', 40 | ]); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Matcher/EqualMatcher.php: -------------------------------------------------------------------------------- 1 | expected == $actual; 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | * 31 | * @return TemplateInterface 32 | */ 33 | public function getDefaultTemplate() 34 | { 35 | return new ArrayTemplate([ 36 | 'default' => 'Expected {{expected}}, got {{actual}}', 37 | 'negated' => 'Expected {{actual}} not to equal {{expected}}', 38 | ]); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Matcher/SameMatcher.php: -------------------------------------------------------------------------------- 1 | expected === $actual; 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | * 29 | * @return TemplateInterface 30 | */ 31 | public function getDefaultTemplate() 32 | { 33 | return new ArrayTemplate([ 34 | 'default' => 'Expected {{actual}} to be identical to {{expected}}', 35 | 'negated' => 'Expected {{actual}} not to be identical to {{expected}}', 36 | ]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Matcher/InstanceofMatcher.php: -------------------------------------------------------------------------------- 1 | expected; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | * 30 | * @return TemplateInterface 31 | */ 32 | public function getDefaultTemplate() 33 | { 34 | return new ArrayTemplate([ 35 | 'default' => 'Expected {{actual}} to be instance of {{expected}}', 36 | 'negated' => 'Expected {{actual}} to not be an instance of {{expected}}', 37 | ]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 - 2016 Brian Scaturro 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/Matcher/PredicateMatcher.php: -------------------------------------------------------------------------------- 1 | 'Expected {{actual}} to satisfy {{expected}}', 25 | 'negated' => 'Expected {{actual}} to not satisfy {{expected}}', 26 | ]); 27 | } 28 | 29 | /** 30 | * Match actual value against the expected predicate. 31 | * 32 | * @param $actual 33 | * @return mixed 34 | */ 35 | protected function doMatch($actual) 36 | { 37 | return (bool) call_user_func_array($this->expected, [$actual]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Formatter/FormatterInterface.php: -------------------------------------------------------------------------------- 1 | expected, $actual); 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | * 33 | * @return TemplateInterface 34 | */ 35 | public function getDefaultTemplate() 36 | { 37 | return new ArrayTemplate([ 38 | 'default' => 'Expected {{actual}} to match {{expected}}', 39 | 'negated' => 'Expected {{actual}} not to match {{expected}}', 40 | ]); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Matcher/SubStringMatcher.php: -------------------------------------------------------------------------------- 1 | expected) !== false; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | * 33 | * @return TemplateInterface 34 | */ 35 | public function getDefaultTemplate() 36 | { 37 | return new ArrayTemplate([ 38 | 'default' => 'Expected {{actual}} to contain {{expected}}', 39 | 'negated' => 'Expected {{actual}} to not contain {{expected}}', 40 | ]); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /peridot.php: -------------------------------------------------------------------------------- 1 | track(__DIR__ . '/src'); 17 | 18 | $dot = new DotReporterPlugin($emitter); 19 | $list = new ListReporterPlugin($emitter); 20 | 21 | $coverage = new CodeCoverageReporters($emitter); 22 | $coverage->register(); 23 | 24 | $prophecy = new ProphecyPlugin($emitter); 25 | 26 | // set the default path 27 | $emitter->on('peridot.start', function (Environment $environment) { 28 | $environment->getDefinition()->getArgument('path')->setDefault('specs'); 29 | }); 30 | 31 | $emitter->on('code-coverage.start', function (AbstractCodeCoverageReporter $reporter) { 32 | $reporter->addDirectoryToWhitelist(__DIR__ . '/src'); 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /specs/matcher/match.spec.php: -------------------------------------------------------------------------------- 1 | isMatch = false; 8 | $this->expected = 'expected'; 9 | $this->actual = 'actual'; 10 | $this->isNegated = false; 11 | $this->subject = new Match($this->isMatch, $this->expected, $this->actual, $this->isNegated); 12 | }); 13 | 14 | it('should retain the data passed to the constructor', function () { 15 | expect($this->subject->isMatch())->to->equal($this->isMatch); 16 | expect($this->subject->getExpected())->to->equal($this->expected); 17 | expect($this->subject->getActual())->to->equal($this->actual); 18 | expect($this->subject->isNegated())->to->equal($this->isNegated); 19 | }); 20 | 21 | it('should allow setting of the actual value', function () { 22 | $this->subject->setActual('other'); 23 | 24 | expect($this->subject->getActual())->to->equal('other'); 25 | }); 26 | 27 | it('should allow setting of the expected value', function () { 28 | $this->subject->setExpected('other'); 29 | 30 | expect($this->subject->getExpected())->to->equal('other'); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /specs/matcher/pattern-matcher.spec.php: -------------------------------------------------------------------------------- 1 | matcher = new PatternMatcher('/^hi/'); 8 | }); 9 | 10 | describe('->match()', function () { 11 | it('should return true if actual value matches expected pattern', function () { 12 | $result = $this->matcher->match('hihowareyou'); 13 | expect($result->isMatch())->to->be->true; 14 | }); 15 | 16 | it('should return false if actual value does not match expected pattern', function () { 17 | $result = $this->matcher->match('nope'); 18 | expect($result->isMatch())->to->be->false; 19 | }); 20 | 21 | it('should throw an exception if the actual value is not a string', function () { 22 | expect([$this->matcher, 'match'])->with(1)->to->throw('InvalidArgumentException'); 23 | }); 24 | 25 | context('when negated', function () { 26 | it('should return false if value does match pattern', function () { 27 | $result = $this->matcher->invert()->match('hithere'); 28 | expect($result->isMatch())->to->be->false; 29 | }); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /specs/matcher/sub-string-matcher.spec.php: -------------------------------------------------------------------------------- 1 | matcher = new SubStringMatcher('foo'); 8 | }); 9 | 10 | describe('->match()', function () { 11 | it('should return true if substring is contained in actual', function () { 12 | $result = $this->matcher->match('foobar'); 13 | expect($result->isMatch())->to->be->true; 14 | }); 15 | 16 | it('should return false if substring is not contained in actual', function () { 17 | $result = $this->matcher->match('hello'); 18 | expect($result->isMatch())->to->be->false; 19 | }); 20 | 21 | it('should throw an exception if actual is not a string', function () { 22 | expect(function () { 23 | $this->matcher->match(1); 24 | })->to->throw('InvalidArgumentException'); 25 | }); 26 | 27 | context('when negated', function () { 28 | it('should return false if substring is contained in actual', function () { 29 | $result = $this->matcher->invert()->match('foobar'); 30 | expect($result->isMatch())->to->be->false; 31 | }); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | matrix: 4 | include: 5 | - php: 5.4 6 | - php: 5.5 7 | - php: 5.6 8 | - php: 7.0 9 | - php: 7.1 10 | - php: nightly 11 | - php: hhvm-3.6 12 | sudo: required 13 | dist: trusty 14 | group: edge 15 | - php: hhvm-3.9 16 | sudo: required 17 | dist: trusty 18 | group: edge 19 | - php: hhvm-3.12 20 | sudo: required 21 | dist: trusty 22 | group: edge 23 | - php: hhvm-3.15 24 | sudo: required 25 | dist: trusty 26 | group: edge 27 | - php: hhvm-nightly 28 | sudo: required 29 | dist: trusty 30 | group: edge 31 | fast_finish: true 32 | allow_failures: 33 | - php: nightly 34 | - php: hhvm-3.6 35 | - php: hhvm-3.9 36 | - php: hhvm-3.12 37 | - php: hhvm-3.15 38 | - php: hhvm-nightly 39 | 40 | before_install: phpenv config-rm xdebug.ini || true 41 | install: composer install --prefer-dist --no-progress --no-interaction --optimize-autoloader --ignore-platform-reqs 42 | script: scripts/travis 43 | after_success: scripts/travis-after 44 | 45 | env: 46 | global: 47 | - PERIDOT_PUBLISH_VERSION=7.0 48 | 49 | cache: 50 | directories: 51 | - $HOME/.composer 52 | 53 | sudo: false 54 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We dig open source. If you want to help out, we would certainly welcome 4 | contributions large or small. 5 | 6 | ## Reporting Bugs 7 | 8 | Issues can be reported on the [issue tracker]. When reporting, examples that 9 | allow contributors to reproduce the issue are always appreciated. Even better, 10 | include a failing test. 11 | 12 | ## Requesting Features 13 | 14 | We would love to hear ideas for making Leo better. Feel free to create a 15 | "feature" issue on the [issue tracker]. 16 | 17 | If you see an existing feature request that you like, please chime in to the 18 | conversation so other contributors can see how many people are interested in a 19 | particular feature. 20 | 21 | ## Contributing code 22 | 23 | We stick to [PSR-2] coding standards. To check for code style issues, first make 24 | sure your changes are staged, then run `make lint`. 25 | 26 | [psr-2]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md 27 | 28 | ### Steps for contributing 29 | 30 | 1. Fork the [repository] on GitHub 31 | 2. Add the code for your feature or bug 32 | 3. Ensure the dependencies can be installed with `make install` 33 | 4. Send a [pull request] 34 | 35 | [repository]: https://github.com/peridot-php/leo 36 | [pull request]: https://help.github.com/articles/creating-a-pull-request 37 | 38 | 39 | 40 | [issue tracker]: https://github.com/peridot-php/leo/issues 41 | -------------------------------------------------------------------------------- /src/Matcher/GreaterThanMatcher.php: -------------------------------------------------------------------------------- 1 | 'Expected {{actual}} to be above {{expected}}', 24 | 'negated' => 'Expected {{actual}} to be at most {{expected}}', 25 | ]); 26 | } 27 | 28 | /** 29 | * @return ArrayTemplate 30 | */ 31 | public function getDefaultCountableTemplate() 32 | { 33 | $count = $this->getCount(); 34 | 35 | return new ArrayTemplate([ 36 | 'default' => "Expected {{actual}} to have a length above {{expected}} but got $count", 37 | 'negated' => 'Expected {{actual}} to not have a length above {{expected}}', 38 | ]); 39 | } 40 | 41 | /** 42 | * Match that actual number is greater than the expected value. 43 | * 44 | * @param $number 45 | * @return bool 46 | */ 47 | protected function matchNumeric($number) 48 | { 49 | return $number > $this->expected; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Matcher/TypeMatcher.php: -------------------------------------------------------------------------------- 1 | setActual($this->type); 31 | } 32 | 33 | /** 34 | * Determine if the actual value has the same type as the expected value. Uses the native gettype() 35 | * function to compare. 36 | * 37 | * @param $actual 38 | * @return bool 39 | */ 40 | public function doMatch($actual) 41 | { 42 | $this->type = gettype($actual); 43 | 44 | return $this->expected === $this->type; 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | * 50 | * @return TemplateInterface 51 | */ 52 | public function getDefaultTemplate() 53 | { 54 | return new ArrayTemplate([ 55 | 'default' => 'Expected {{expected}}, got {{actual}}', 56 | 'negated' => 'Expected a type other than {{expected}}', 57 | ]); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Matcher/LessThanMatcher.php: -------------------------------------------------------------------------------- 1 | 'Expected {{actual}} to be below {{expected}}', 24 | 'negated' => 'Expected {{actual}} to be at least {{expected}}', 25 | ]); 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | * 31 | * @return ArrayTemplate 32 | */ 33 | public function getDefaultCountableTemplate() 34 | { 35 | $count = $this->getCount(); 36 | 37 | return new ArrayTemplate([ 38 | 'default' => "Expected {{actual}} to have a length below {{expected}} but got $count", 39 | 'negated' => 'Expected {{actual}} to not have a length below {{expected}}', 40 | ]); 41 | } 42 | 43 | /** 44 | * Match that actual number is less than the expected value. 45 | * 46 | * @param $number 47 | * @return bool 48 | */ 49 | protected function matchNumeric($number) 50 | { 51 | return $number < $this->expected; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Responder/ExceptionResponder.php: -------------------------------------------------------------------------------- 1 | formatter = $formatter; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | * 35 | * Throws an exception containing the formatted message. 36 | * 37 | * @param Match $match 38 | * @param TemplateInterface $template 39 | * @param string $message 40 | * @return void 41 | * @throws Exception 42 | */ 43 | public function respond(Match $match, TemplateInterface $template, $message = '') 44 | { 45 | if ($match->isMatch()) { 46 | return; 47 | } 48 | 49 | $this->formatter->setMatch($match); 50 | $message = ($message) ? $message : $this->formatter->getMessage($template); 51 | throw new AssertionException($message); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Matcher/LessThanOrEqualMatcher.php: -------------------------------------------------------------------------------- 1 | expected; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | * 30 | * @return TemplateInterface 31 | */ 32 | public function getDefaultTemplate() 33 | { 34 | return new ArrayTemplate([ 35 | 'default' => 'Expected {{actual}} to be at most {{expected}}', 36 | 'negated' => 'Expected {{actual}} to be above {{expected}}', 37 | ]); 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | * 43 | * @return TemplateInterface 44 | */ 45 | public function getDefaultCountableTemplate() 46 | { 47 | $count = $this->getCount(); 48 | 49 | return new ArrayTemplate([ 50 | 'default' => "Expected {{actual}} to have a length at most {{expected}} but got $count", 51 | 'negated' => 'Expected {{actual}} to have a length above {{expected}}', 52 | ]); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Matcher/AbstractMatcher.php: -------------------------------------------------------------------------------- 1 | expected = $expected; 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | * 27 | * @param mixed $actual 28 | * @return Match 29 | */ 30 | public function match($actual = '') 31 | { 32 | $isMatch = $this->doMatch($actual); 33 | $isNegated = $this->isNegated(); 34 | 35 | return new Match($isMatch xor $isNegated, $this->expected, $actual, $isNegated); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | * 41 | * @return TemplateInterface 42 | */ 43 | public function getTemplate() 44 | { 45 | if (!isset($this->template)) { 46 | return $this->getDefaultTemplate(); 47 | } 48 | 49 | return $this->template; 50 | } 51 | 52 | /** 53 | * The actual matching algorithm for the matcher. This is called by ->match() 54 | * to create a Match result. 55 | * 56 | * @param mixed $actual 57 | * @return bool 58 | */ 59 | abstract protected function doMatch($actual); 60 | 61 | /** 62 | * @var mixed 63 | */ 64 | protected $expected; 65 | } 66 | -------------------------------------------------------------------------------- /src/Interfaces/Assert/CollectionAssertTrait.php: -------------------------------------------------------------------------------- 1 | assertion->setActual($countable); 25 | 26 | return $this->assertion->to->have->length($length, $message); 27 | } 28 | 29 | /** 30 | * Perform an inclusion assertion. 31 | * 32 | * @param array|string $haystack 33 | * @param mixed $needle 34 | * @param string $message 35 | */ 36 | public function isIncluded($haystack, $needle, $message = '') 37 | { 38 | $this->assertion->setActual($haystack); 39 | 40 | return $this->assertion->to->include($needle, $message); 41 | } 42 | 43 | /** 44 | * Perform a negated inclusion assertion. 45 | * 46 | * @param array|string $haystack 47 | * @param mixed $needle 48 | * @param string $message 49 | */ 50 | public function notInclude($haystack, $needle, $message = '') 51 | { 52 | $this->assertion->setActual($haystack); 53 | 54 | return $this->assertion->to->not->include($needle, $message); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /specs/matcher/matcher.spec.php: -------------------------------------------------------------------------------- 1 | matcher = new TestMatcher(true); 9 | }); 10 | 11 | describe('->getTemplate()', function () { 12 | it('should return the default template if not set', function () { 13 | $template = new ArrayTemplate([]); 14 | $this->matcher->setDefaultTemplate($template); 15 | expect($this->matcher->getTemplate())->to->equal($template); 16 | }); 17 | 18 | it('should return the template that was set if set', function () { 19 | $template = new ArrayTemplate([]); 20 | $this->matcher->setTemplate($template); 21 | expect($this->matcher->getTemplate())->to->equal($template); 22 | }); 23 | }); 24 | 25 | describe('->invert()', function () { 26 | it('should toggle negated status', function () { 27 | expect($this->matcher->isNegated())->to->equal(false); 28 | $this->matcher->invert(); 29 | expect($this->matcher->isNegated())->to->equal(true); 30 | }); 31 | }); 32 | }); 33 | 34 | class TestMatcher extends AbstractMatcher 35 | { 36 | protected $testTemplate; 37 | 38 | public function setDefaultTemplate($template) 39 | { 40 | $this->testTemplate = $template; 41 | } 42 | 43 | public function getDefaultTemplate() 44 | { 45 | return $this->testTemplate; 46 | } 47 | 48 | protected function doMatch($actual) 49 | { 50 | return true; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Matcher/GreaterThanOrEqualMatcher.php: -------------------------------------------------------------------------------- 1 | = $this->expected; 26 | } 27 | 28 | /** 29 | * Return a default template if none was set. 30 | * 31 | * @return TemplateInterface 32 | */ 33 | public function getDefaultTemplate() 34 | { 35 | return new ArrayTemplate([ 36 | 'default' => 'Expected {{actual}} to be at least {{expected}}', 37 | 'negated' => 'Expected {{actual}} to be below {{expected}}', 38 | ]); 39 | } 40 | 41 | /** 42 | * Return a default template for when a countable has been set. 43 | * 44 | * @return TemplateInterface 45 | */ 46 | public function getDefaultCountableTemplate() 47 | { 48 | $count = $this->getCount(); 49 | 50 | return new ArrayTemplate([ 51 | 'default' => "Expected {{actual}} to have a length at least {{expected}} but got $count", 52 | 'negated' => 'Expected {{actual}} to have a length below {{expected}}', 53 | ]); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /specs/leo.spec.php: -------------------------------------------------------------------------------- 1 | reflection = new ReflectionClass('Peridot\Leo\Leo'); 10 | $leo = $this->reflection->newInstanceWithoutConstructor(); 11 | $construct = $this->reflection->getConstructor(); 12 | $construct->setAccessible(true); 13 | $construct->invoke($leo); 14 | $this->leo = $leo; 15 | }); 16 | 17 | describe('formatter accessors', function () { 18 | it('should allow access to the formatter', function () { 19 | $formatter = new Formatter(); 20 | $this->leo->setFormatter($formatter); 21 | expect($this->leo->getFormatter())->to->equal($formatter); 22 | }); 23 | }); 24 | 25 | describe('responder accessors', function () { 26 | it('should allow access to the responder', function () { 27 | $formatter = new Formatter(); 28 | $responder = new ExceptionResponder($formatter); 29 | $this->leo->setResponder($responder); 30 | expect($this->leo->getResponder())->to->equal($responder); 31 | }); 32 | }); 33 | 34 | describe('assertion accessors', function () { 35 | it('should allow access to the assertion', function () { 36 | $formatter = new Formatter(); 37 | $responder = new ExceptionResponder($formatter); 38 | $assertion = new Assertion($responder); 39 | $this->leo->setAssertion($assertion); 40 | expect($this->leo->getAssertion())->to->equal($assertion); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/Matcher/InclusionMatcher.php: -------------------------------------------------------------------------------- 1 | matchTraversable($actual); 28 | } 29 | 30 | if (is_array($actual)) { 31 | return array_search($this->expected, $actual, true) !== false; 32 | } 33 | 34 | if (is_string($actual)) { 35 | return strpos($actual, $this->expected) !== false; 36 | } 37 | 38 | throw new InvalidArgumentException('Inclusion matcher requires a string or array'); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | * 44 | * @return TemplateInterface 45 | */ 46 | public function getDefaultTemplate() 47 | { 48 | return new ArrayTemplate([ 49 | 'default' => 'Expected {{actual}} to include {{expected}}', 50 | 'negated' => 'Expected {{actual}} to not include {{expected}}', 51 | ]); 52 | } 53 | 54 | private function matchTraversable(Traversable $actual) 55 | { 56 | foreach ($actual as $value) { 57 | if ($value === $this->expected) { 58 | return true; 59 | } 60 | } 61 | 62 | return false; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Matcher/Template/TemplateInterface.php: -------------------------------------------------------------------------------- 1 | setDefaultTemplate("Expected {{expected}}, got {{actual}}"); 19 | * @endcode 20 | * 21 | * @package Peridot\Leo\Matcher\Template 22 | */ 23 | interface TemplateInterface 24 | { 25 | /** 26 | * Return the template variables assigned to the template. 27 | * 28 | * @return array 29 | */ 30 | public function getTemplateVars(); 31 | 32 | /** 33 | * Set the template vars assigned to the template. 34 | * 35 | * @param array $vars 36 | * @return mixed 37 | */ 38 | public function setTemplateVars(array $vars); 39 | 40 | /** 41 | * Set the default template, that is the template for a failed match without 42 | * negation. 43 | * 44 | * @return string 45 | */ 46 | public function getDefaultTemplate(); 47 | 48 | /** 49 | * Set the default template that is used when negation is not specified. 50 | * 51 | * @param string $template 52 | * @return mixed 53 | */ 54 | public function setDefaultTemplate($template); 55 | 56 | /** 57 | * Return the template used for a failed negated match. 58 | * 59 | * @return string 60 | */ 61 | public function getNegatedTemplate(); 62 | 63 | /** 64 | * Set the template used for a failed negated match. 65 | * 66 | * @param string $template 67 | * @return mixed 68 | */ 69 | public function setNegatedTemplate($template); 70 | } 71 | -------------------------------------------------------------------------------- /src/Matcher/MatcherTrait.php: -------------------------------------------------------------------------------- 1 | negated; 24 | } 25 | 26 | /** 27 | * Inverts a matcher. If a matcher is not negated, it will become negated. If a matcher 28 | * is negated, it will no longer be negated. 29 | * 30 | * @return $this 31 | */ 32 | public function invert() 33 | { 34 | $this->negated = !$this->negated; 35 | 36 | return $this; 37 | } 38 | 39 | /** 40 | * Set the TemplateInterface to use for formatting match results. 41 | * 42 | * @param TemplateInterface $template 43 | * @return $this 44 | */ 45 | public function setTemplate(TemplateInterface $template) 46 | { 47 | $this->template = $template; 48 | 49 | return $this; 50 | } 51 | 52 | /** 53 | * Set the Assertion bound to the matcher. Useful for checking 54 | * flags from within a matcher. 55 | * 56 | * @param Assertion $assertion 57 | * @return mixed 58 | */ 59 | public function setAssertion(Assertion $assertion) 60 | { 61 | $this->assertion = $assertion; 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * @var bool 68 | */ 69 | protected $negated = false; 70 | 71 | /** 72 | * @var TemplateInterface 73 | */ 74 | protected $template; 75 | 76 | /** 77 | * @var Assertion 78 | */ 79 | protected $assertion; 80 | } 81 | -------------------------------------------------------------------------------- /src/Matcher/MatcherInterface.php: -------------------------------------------------------------------------------- 1 | 'Expected {{actual}} to have a length of {{expected}} but got {{count}}', 32 | 'negated' => 'Expected {{actual}} to not have a length of {{expected}}', 33 | ]); 34 | 35 | return $template->setTemplateVars(['count' => $this->count]); 36 | } 37 | 38 | /** 39 | * Match the length of the countable interface or string against 40 | * the expected value. 41 | * 42 | * @param string|array|Countable $actual 43 | * @return mixed 44 | */ 45 | protected function doMatch($actual) 46 | { 47 | if ($this->isCountable($actual)) { 48 | $this->count = count($actual); 49 | } 50 | 51 | if (is_string($actual)) { 52 | $this->count = strlen($actual); 53 | } 54 | 55 | if (isset($this->count)) { 56 | return $this->expected === $this->count; 57 | } 58 | 59 | throw new InvalidArgumentException('Length matcher requires a string, array, or Countable'); 60 | } 61 | 62 | /** 63 | * Determine if the native count() function can return a valid result 64 | * on the actual value. 65 | * 66 | * @param mixed $actual 67 | * @return bool 68 | */ 69 | protected function isCountable($actual) 70 | { 71 | return is_array($actual) || $actual instanceof Countable; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | in(__DIR__) 5 | ->exclude( 6 | array( 7 | 'vendor', 8 | ) 9 | ); 10 | 11 | return Symfony\CS\Config\Config::create() 12 | ->level(Symfony\CS\FixerInterface::PSR2_LEVEL) 13 | ->fixers( 14 | array( 15 | // symfony 16 | 'array_element_no_space_before_comma', 17 | 'array_element_white_space_after_comma', 18 | 'blankline_after_open_tag', 19 | 'duplicate_semicolon', 20 | 'extra_empty_lines', 21 | 'function_typehint_space', 22 | 'include', 23 | 'join_function', 24 | 'list_commas', 25 | 'multiline_array_trailing_comma', 26 | 'namespace_no_leading_whitespace', 27 | 'new_with_braces', 28 | 'no_blank_lines_after_class_opening', 29 | 'no_empty_lines_after_phpdocs', 30 | 'object_operator', 31 | 'operators_spaces', 32 | 'phpdoc_indent', 33 | 'phpdoc_params', 34 | 'phpdoc_scalar', 35 | 'phpdoc_short_description', 36 | 'phpdoc_to_comment', 37 | 'phpdoc_trim', 38 | 'phpdoc_type_to_var', 39 | 'phpdoc_types', 40 | 'phpdoc_var_without_name', 41 | 'pre_increment', 42 | 'print_to_echo', 43 | 'remove_leading_slash_use', 44 | 'remove_lines_between_uses', 45 | 'return', 46 | 'self_accessor', 47 | 'short_bool_cast', 48 | 'single_array_no_trailing_comma', 49 | 'single_blank_line_before_namespace', 50 | 'single_quote', 51 | 'spaces_before_semicolon', 52 | 'spaces_cast', 53 | 'standardize_not_equal', 54 | 'ternary_spaces', 55 | 'trim_array_spaces', 56 | 'unary_operators_spaces', 57 | 'unneeded_control_parentheses', 58 | 'unused_use', 59 | 'whitespacy_lines', 60 | 61 | // contrib 62 | 'concat_with_spaces', 63 | 'multiline_spaces_before_semicolon', 64 | 'ordered_use', 65 | ) 66 | ) 67 | ->finder($finder); 68 | -------------------------------------------------------------------------------- /specs/matcher/inclusion-matcher.spec.php: -------------------------------------------------------------------------------- 1 | matcher = new InclusionMatcher('A'); 8 | }); 9 | 10 | it('should throw an exception if actual value is not array or string', function () { 11 | expect([$this->matcher, 'match'])->with(5)->to->throw('InvalidArgumentException'); 12 | }); 13 | 14 | it('should return true if value is in array', function () { 15 | $match = $this->matcher->match(['A', 'B', 'C']); 16 | expect($match->isMatch())->to->equal(true); 17 | }); 18 | 19 | it('should return true if value is in an instance of Traversable', function () { 20 | $match = $this->matcher->match(new ArrayObject(['A', 'B', 'C'])); 21 | expect($match->isMatch())->to->equal(true); 22 | }); 23 | 24 | it('should return true if value is in string', function () { 25 | $match = $this->matcher->match('A pleasure to meet you'); 26 | expect($match->isMatch())->to->equal(true); 27 | }); 28 | 29 | it('should return false if value is not in array', function () { 30 | $match = $this->matcher->match(['B', 'C', 'D']); 31 | expect($match->isMatch())->to->equal(false); 32 | }); 33 | 34 | it('should return false if types are different', function () { 35 | $matcher = new InclusionMatcher('1'); 36 | $match = $matcher->match([1]); 37 | expect($match->isMatch())->to->equal(false); 38 | }); 39 | 40 | it('should return false if value is not in an instance of Traversable', function () { 41 | $match = $this->matcher->match(new ArrayObject(['B', 'C', 'D'])); 42 | expect($match->isMatch())->to->equal(false); 43 | }); 44 | 45 | it('should return false if value is not in string', function () { 46 | $match = $this->matcher->match('The pleasure is all mine'); 47 | expect($match->isMatch())->to->equal(false); 48 | }); 49 | 50 | context('when negated', function () { 51 | it('should return false if value is included', function () { 52 | $match = $this->matcher->invert()->match(['A']); 53 | expect($match->isMatch())->to->equal(false); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /specs/matcher/length-matcher.spec.php: -------------------------------------------------------------------------------- 1 | matcher = new LengthMatcher(2); 8 | }); 9 | 10 | describe('->match()', function () { 11 | context('when matching an array', function () { 12 | it('should return true if the array has the expected length', function () { 13 | $result = $this->matcher->match([1, 2]); 14 | expect($result->isMatch())->to->be->true; 15 | }); 16 | 17 | it('should return false if the array does not have the expected length', function () { 18 | $result = $this->matcher->match([1]); 19 | expect($result->isMatch())->to->be->false; 20 | }); 21 | 22 | context('and is negated', function () { 23 | it('should return false if the array has the expected length', function () { 24 | $result = $this->matcher->invert()->match([1, 2]); 25 | expect($result->isMatch())->to->be->false; 26 | }); 27 | }); 28 | }); 29 | 30 | context('when matching a string', function () { 31 | it('should return true if the string has the expected length', function () { 32 | $result = $this->matcher->match('hi'); 33 | expect($result->isMatch())->to->be->true; 34 | }); 35 | 36 | it('should return false if the string does not have the expected length', function () { 37 | $result = $this->matcher->match('h'); 38 | expect($result->isMatch())->to->be->false; 39 | }); 40 | 41 | context('and is negated', function () { 42 | it('should return false if the string has the expected length', function () { 43 | $result = $this->matcher->invert()->match('hi'); 44 | expect($result->isMatch())->to->be->false; 45 | }); 46 | }); 47 | }); 48 | 49 | it('should throw an exception if actual is not a countable or string', function () { 50 | expect([$this->matcher, 'match'])->with(123)->to->throw('InvalidArgumentException'); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/DynamicObjectTrait.php: -------------------------------------------------------------------------------- 1 | methods[$name] = \Closure::bind($method, $this, $this); 36 | 37 | return $this; 38 | } 39 | 40 | /** 41 | * Adds a lazy property identified by the given name. The property 42 | * is lazy because it is not evaluated until asked for via __get(). 43 | * 44 | * @param string $name 45 | * @param callable $factory 46 | * @param bool $memoize 47 | * @return $this 48 | */ 49 | public function addProperty($name, callable $factory, $memoize = false) 50 | { 51 | $this->properties[$name] = ['factory' => \Closure::bind($factory, $this, $this), 'memoize' => $memoize]; 52 | 53 | return $this; 54 | } 55 | 56 | /** 57 | * A simple mechanism for storing arbitrary flags. Flags are useful 58 | * for tweaking behavior based on their presence. 59 | * 60 | * @return $this|mixed 61 | */ 62 | public function flag() 63 | { 64 | $args = func_get_args(); 65 | $num = count($args); 66 | 67 | if ($num > 1) { 68 | $this->flags[$args[0]] = $args[1]; 69 | 70 | return $this; 71 | } 72 | 73 | if (array_key_exists($args[0], $this->flags)) { 74 | return $this->flags[$args[0]]; 75 | } 76 | } 77 | 78 | /** 79 | * Reset flags. Flags are generally cleared after an Assertion is made. 80 | * 81 | * @return $this 82 | */ 83 | public function clearFlags() 84 | { 85 | $this->flags = []; 86 | 87 | return $this; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /specs/matcher/greater-than-matcher.spec.php: -------------------------------------------------------------------------------- 1 | matcher = new GreaterThanMatcher(5); 8 | }); 9 | 10 | it('should throw an exception if actual value is not numeric', function () { 11 | expect([$this->matcher, 'match'])->with('string')->to->throw('InvalidArgumentException'); 12 | }); 13 | 14 | it('should return true if actual value is greater than expected', function () { 15 | $match = $this->matcher->match(6); 16 | expect($match->isMatch())->to->be->true; 17 | }); 18 | 19 | it('should return false if actual value is less than expected', function () { 20 | $match = $this->matcher->match(4); 21 | expect($match->isMatch())->to->be->false; 22 | }); 23 | 24 | context('when negated', function () { 25 | it('should return false if actual value is above expected', function () { 26 | $match = $this->matcher->invert()->match(6); 27 | expect($match->isMatch())->to->be->false; 28 | }); 29 | }); 30 | 31 | context('when countable is set', function () { 32 | beforeEach(function () { 33 | $this->matcher->setCountable([1, 2, 3, 4, 5, 6]); 34 | }); 35 | 36 | it('should match true when countable length is above expected', function () { 37 | $match = $this->matcher->match(); 38 | expect($match->isMatch())->to->be->true; 39 | }); 40 | 41 | it('should match false when countable length is below expected', function () { 42 | $match = $this->matcher->setCountable([1, 2])->match(); 43 | expect($match->isMatch())->to->be->false; 44 | }); 45 | 46 | context('and matcher is negated', function () { 47 | it('should return false if actual value is above expected', function () { 48 | $match = $this->matcher->invert()->match(); 49 | expect($match->isMatch())->to->be->false; 50 | }); 51 | }); 52 | }); 53 | 54 | describe('->getCountable()', function () { 55 | it('should fetch the countable', function () { 56 | $countable = [1, 2, 3]; 57 | $this->matcher->setCountable($countable); 58 | expect($this->matcher->getCountable())->to->equal($countable); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/Matcher/Match.php: -------------------------------------------------------------------------------- 1 | match = $isMatch; 41 | $this->expected = $expected; 42 | $this->actual = $actual; 43 | $this->isNegated = $isNegated; 44 | } 45 | 46 | /** 47 | * Return whether or not a match succeeded. 48 | * 49 | * @return bool 50 | */ 51 | public function isMatch() 52 | { 53 | return $this->match; 54 | } 55 | 56 | /** 57 | * Get the actual value used in the match. 58 | * 59 | * @return mixed 60 | */ 61 | public function getActual() 62 | { 63 | return $this->actual; 64 | } 65 | 66 | /** 67 | * Get the expected value used in the match. 68 | * 69 | * @return mixed 70 | */ 71 | public function getExpected() 72 | { 73 | return $this->expected; 74 | } 75 | 76 | /** 77 | * Returns whether or not the match was negated. 78 | * 79 | * @return bool 80 | */ 81 | public function isNegated() 82 | { 83 | return $this->isNegated; 84 | } 85 | 86 | /** 87 | * Set the actual value used in the match. 88 | * 89 | * @param mixed $actual 90 | * @return $this 91 | */ 92 | public function setActual($actual) 93 | { 94 | $this->actual = $actual; 95 | 96 | return $this; 97 | } 98 | 99 | /** 100 | * Set the expected value used in the match. 101 | * 102 | * @param mixed $expected 103 | * @return $this 104 | */ 105 | public function setExpected($expected) 106 | { 107 | $this->expected = $expected; 108 | 109 | return $this; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Matcher/Template/ArrayTemplate.php: -------------------------------------------------------------------------------- 1 | "Expected {{actual}} to be {{expected}}", 12 | * 'negated' => "Expected {{actual}} not to be {{expected}} 13 | * ]); 14 | * @endcode 15 | * 16 | * @package Peridot\Leo\Matcher\Template 17 | */ 18 | class ArrayTemplate implements TemplateInterface 19 | { 20 | /** 21 | * @var string 22 | */ 23 | protected $default = ''; 24 | 25 | /** 26 | * @var string 27 | */ 28 | protected $negated = ''; 29 | 30 | /** 31 | * @var array 32 | */ 33 | protected $vars = []; 34 | 35 | /** 36 | * @param array $templates 37 | */ 38 | public function __construct(array $templates) 39 | { 40 | if (array_key_exists('default', $templates)) { 41 | $this->default = $templates['default']; 42 | } 43 | 44 | if (array_key_exists('negated', $templates)) { 45 | $this->negated = $templates['negated']; 46 | } 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | * 52 | * @return string 53 | */ 54 | public function getDefaultTemplate() 55 | { 56 | return $this->default; 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | * 62 | * @param string $default 63 | */ 64 | public function setDefaultTemplate($default) 65 | { 66 | $this->default = $default; 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * {@inheritdoc} 73 | * 74 | * @return string 75 | */ 76 | public function getNegatedTemplate() 77 | { 78 | return $this->negated; 79 | } 80 | 81 | /** 82 | * {@inheritdoc} 83 | * 84 | * @param string $negated 85 | */ 86 | public function setNegatedTemplate($negated) 87 | { 88 | $this->negated = $negated; 89 | 90 | return $this; 91 | } 92 | 93 | /** 94 | * {@inheritdoc} 95 | * 96 | * @return array 97 | */ 98 | public function getTemplateVars() 99 | { 100 | return $this->vars; 101 | } 102 | 103 | /** 104 | * {@inheritdoc} 105 | * 106 | * @param array $vars 107 | * @return $this 108 | */ 109 | public function setTemplateVars(array $vars) 110 | { 111 | $this->vars = $vars; 112 | 113 | return $this; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /specs/matcher/range-matcher.spec.php: -------------------------------------------------------------------------------- 1 | matcher = new RangeMatcher(1, 2); 8 | }); 9 | 10 | describe('->setUpperBound()', function () { 11 | it('should throw an exception if non numeric value given', function () { 12 | expect([$this->matcher, 'setUpperBound']) 13 | ->with('string')->to->throw('InvalidArgumentException'); 14 | }); 15 | }); 16 | 17 | describe('->setLowerBound()', function () { 18 | it('should throw an exception if non numeric value given', function () { 19 | expect([$this->matcher, 'setLowerBound']) 20 | ->with('string')->to->throw('InvalidArgumentException'); 21 | }); 22 | }); 23 | 24 | describe('->getUpperBound()', function () { 25 | it('should fetch the upper bound', function () { 26 | $this->matcher->setUpperBound(5); 27 | expect($this->matcher->getUpperBound())->to->equal(5); 28 | }); 29 | }); 30 | 31 | describe('->getLowerBound()', function () { 32 | it('should fetch the lower bound', function () { 33 | $this->matcher->setLowerBound(5); 34 | expect($this->matcher->getLowerBound())->to->equal(5); 35 | }); 36 | }); 37 | 38 | describe('->match()', function () { 39 | it('should return true if value is within upper and lower bounds', function () { 40 | $result = $this->matcher 41 | ->setLowerBound(1) 42 | ->setUpperBound(3) 43 | ->match(2); 44 | expect($result->isMatch())->to->be->true; 45 | }); 46 | 47 | context('when negated', function () { 48 | it('should return false if value is within upper and lower bounds', function () { 49 | $result = $this->matcher 50 | ->invert() 51 | ->setLowerBound(1) 52 | ->setUpperBound(3) 53 | ->match(2); 54 | expect($result->isMatch())->to->be->false; 55 | }); 56 | }); 57 | 58 | context('when matching the length of a countable', function () { 59 | it('should return true if value count is within upper and lower bounds', function () { 60 | $result = $this->matcher 61 | ->setLowerBound(4) 62 | ->setUpperBound(10) 63 | ->setCountable('hello') 64 | ->match(); 65 | expect($result->isMatch())->to->be->true; 66 | }); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /specs/object-path/object-path.spec.php: -------------------------------------------------------------------------------- 1 | name = new stdClass(); 10 | $object->name->first = 'brian'; 11 | $object->name->last = 'scaturro'; 12 | $object->projects = [ 13 | 'php' => ['peridot', 'leo'], 14 | 'coffeescript' => ['alerts', 'pressbox'], 15 | ]; 16 | $this->object = $object; 17 | 18 | $this->path = new ObjectPath($this->object); 19 | }); 20 | 21 | describe('->get()', function () { 22 | it('should be able to get a nested value', function () { 23 | $first = $this->path->get('name->first'); 24 | expect($first->getPropertyValue())->to->equal('brian'); 25 | }); 26 | 27 | it('should return last value if it is an object', function () { 28 | $this->object->name->origin = new stdClass(); 29 | $this->object->name->origin->country = 'Ireland'; 30 | $origin = $this->path->get('name->origin'); 31 | expect($origin->getPropertyValue())->to->equal($this->object->name->origin); 32 | }); 33 | 34 | it('should return array properties', function () { 35 | $peridot = $this->path->get('projects[php][0]'); 36 | expect($peridot->getPropertyValue())->to->equal('peridot'); 37 | }); 38 | 39 | it('should return null if property does not exist', function () { 40 | expect($this->path->get('nickname'))->to->be->null; 41 | }); 42 | }); 43 | }); 44 | 45 | context('when using an array', function () { 46 | beforeEach(function () { 47 | $this->array = [ 48 | 'name' => [ 49 | 'first' => 'brian', 50 | 'last' => 'scaturro', 51 | ], 52 | 'string', 53 | 1, 54 | ]; 55 | $this->path = new ObjectPath($this->array); 56 | }); 57 | 58 | it('should be able to get an array value', function () { 59 | $one = $this->path->get('[1]'); 60 | expect($one->getPropertyValue())->to->equal(1); 61 | }); 62 | 63 | it('should be able to get nested values', function () { 64 | $name = $this->path->get('[name][first]'); 65 | expect($name->getPropertyValue())->to->equal('brian'); 66 | expect($name->getPropertyName())->to->equal('first'); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/Formatter/Formatter.php: -------------------------------------------------------------------------------- 1 | match; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | * 37 | * @param Match $match 38 | * @return $this 39 | */ 40 | public function setMatch(Match $match) 41 | { 42 | $this->match = $match; 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | * 50 | * @param TemplateInterface $template 51 | * @return mixed|string 52 | */ 53 | public function getMessage(TemplateInterface $template) 54 | { 55 | $vars = $this->getTemplateVars($template); 56 | 57 | $tpl = $this->match->isNegated() 58 | ? $template->getNegatedTemplate() 59 | : $template->getDefaultTemplate(); 60 | 61 | foreach ($vars as $name => $value) { 62 | $tpl = str_replace('{{' . $name . '}}', $this->objectToString($value), $tpl); 63 | } 64 | 65 | return $tpl; 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | * 71 | * @param mixed $obj 72 | * @return string 73 | */ 74 | public function objectToString($obj) 75 | { 76 | switch (gettype($obj)) { 77 | case 'boolean': 78 | return var_export($obj, true); 79 | 80 | case 'NULL': 81 | return 'null'; 82 | 83 | case 'string': 84 | return '"' . $obj . '"'; 85 | } 86 | 87 | return rtrim(print_r($obj, true)); 88 | } 89 | 90 | /** 91 | * Applies match results to other template variables. 92 | * 93 | * @param TemplateInterface $template 94 | * @return array 95 | */ 96 | protected function getTemplateVars(TemplateInterface $template) 97 | { 98 | $vars = [ 99 | 'expected' => $this->match->getExpected(), 100 | 'actual' => $this->match->getActual(), 101 | ]; 102 | 103 | if ($tplVars = $template->getTemplateVars()) { 104 | $vars = array_merge($vars, $tplVars); 105 | } 106 | 107 | return $vars; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /specs/formatter/formatter.spec.php: -------------------------------------------------------------------------------- 1 | formatter = new Formatter(); 10 | }); 11 | 12 | describe('match accessors', function () { 13 | it('should allow access to match', function () { 14 | $match = new Match(false, 4, 3, false); 15 | $this->formatter->setMatch($match); 16 | expect($this->formatter->getMatch())->to->equal($match); 17 | }); 18 | }); 19 | 20 | describe('->objectToString()', function () { 21 | it('should return "false" for false', function () { 22 | $string = $this->formatter->objectToString(false); 23 | expect($string)->to->equal('false'); 24 | }); 25 | 26 | it('should return "true" for true', function () { 27 | $string = $this->formatter->objectToString(true); 28 | expect($string)->to->equal('true'); 29 | }); 30 | 31 | it('should return "null" for null', function () { 32 | $string = $this->formatter->objectToString(null); 33 | expect($string)->to->equal('null'); 34 | }); 35 | 36 | it('should return quoted string for string', function () { 37 | $string = $this->formatter->objectToString('hello'); 38 | expect($string)->to->equal('"hello"'); 39 | }); 40 | 41 | it('should format other objects using print_r', function () { 42 | $obj = new stdClass(); 43 | $obj->first = 'brian'; 44 | $string = $this->formatter->objectToString($obj); 45 | expect($string)->to->equal("stdClass Object\n(\n [first] => brian\n)"); 46 | }); 47 | }); 48 | 49 | describe('->getMessage()', function () { 50 | beforeEach(function () { 51 | $this->template = new ArrayTemplate([ 52 | 'default' => 'Expected {{expected}}, got {{actual}}', 53 | 'negated' => 'Expected {{expected}} not to be {{actual}}', 54 | ]); 55 | }); 56 | 57 | it('should return a default message based on a template', function () { 58 | $match = new Match(false, 4, 3, false); 59 | $message = $this->formatter->setMatch($match)->getMessage($this->template); 60 | expect($message)->to->equal('Expected 4, got 3'); 61 | }); 62 | 63 | it('should return a negated message based on a template', function () { 64 | $match = new Match(false, 4, 4, true); 65 | $message = $this->formatter->setMatch($match)->getMessage($this->template); 66 | expect($message)->to->equal('Expected 4 not to be 4'); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/Matcher/CountableMatcher.php: -------------------------------------------------------------------------------- 1 | countable = $countable; 30 | 31 | return $this; 32 | } 33 | 34 | /** 35 | * Return the countable used by the CountableMatcher. 36 | * 37 | * @return mixed 38 | */ 39 | public function getCountable() 40 | { 41 | return $this->countable; 42 | } 43 | 44 | /** 45 | * Get the count of the countable value. 46 | * @return int 47 | */ 48 | public function getCount() 49 | { 50 | if (is_string($this->countable)) { 51 | return strlen($this->countable); 52 | } 53 | 54 | return count($this->countable); 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | * 60 | * Returns a default countable interface if the countable is set. 61 | * 62 | * @return TemplateInterface 63 | */ 64 | public function getTemplate() 65 | { 66 | if (isset($this->countable)) { 67 | return $this->getDefaultCountableTemplate(); 68 | } 69 | 70 | return parent::getTemplate(); 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | * 76 | * @param $actual 77 | * @return mixed 78 | */ 79 | protected function doMatch($actual = null) 80 | { 81 | if (isset($this->countable)) { 82 | $actual = $this->getCount(); 83 | } 84 | 85 | if (!is_numeric($actual)) { 86 | throw new \InvalidArgumentException(get_class($this) . ' requires a numeric value'); 87 | } 88 | 89 | return $this->matchNumeric($actual); 90 | } 91 | 92 | /** 93 | * Return a default template for when a countable has been set. 94 | * 95 | * @return TemplateInterface 96 | */ 97 | abstract public function getDefaultCountableTemplate(); 98 | 99 | /** 100 | * Determine if a number matches a specified condition. 101 | * 102 | * @param $number 103 | * @return bool 104 | */ 105 | abstract protected function matchNumeric($number); 106 | } 107 | -------------------------------------------------------------------------------- /specs/matcher/keys-matcher.spec.php: -------------------------------------------------------------------------------- 1 | matcher = new KeysMatcher(['foo', 'bar']); 9 | $responder = $this->getProphet()->prophesize('Peridot\Leo\Responder\ResponderInterface'); 10 | $this->assertion = new Assertion($responder->reveal()); 11 | $this->matcher->setAssertion($this->assertion); 12 | }); 13 | 14 | it('should return true if the array has all keys', function () { 15 | $result = $this->matcher->match(['foo' => 1, 'bar' => 2]); 16 | expect($result->isMatch())->to->be->true; 17 | }); 18 | 19 | it('should return false if the array does not have has all keys', function () { 20 | $result = $this->matcher->match(['foo' => 1]); 21 | expect($result->isMatch())->to->be->false; 22 | }); 23 | 24 | it('should return true if the object has all keys', function () { 25 | $obj = new stdClass(); 26 | $obj->foo = 1; 27 | $obj->bar = 2; 28 | $result = $this->matcher->match($obj); 29 | expect($result->isMatch())->to->be->true; 30 | }); 31 | 32 | it('should return false if the object does not have all keys', function () { 33 | $obj = new stdClass(); 34 | $obj->foo = 1; 35 | $result = $this->matcher->match($obj); 36 | expect($result->isMatch())->to->be->false; 37 | }); 38 | 39 | it('should return false if the actual has more keys than expected', function () { 40 | $result = $this->matcher->match(['foo' => 1, 'bar' => 2, 'baz' => 3]); 41 | expect($result->isMatch())->to->be->false; 42 | }); 43 | 44 | it('should throw an exception if something other than an array or object is given', function () { 45 | expect([$this->matcher, 'match'])->with(1)->to->throw('InvalidArgumentException'); 46 | }); 47 | 48 | context('when inverted', function () { 49 | it('should return false if array does have all keys', function () { 50 | $result = $this->matcher->invert()->match(['foo' => 1, 'bar' => 2]); 51 | expect($result->isMatch())->to->be->false; 52 | }); 53 | }); 54 | 55 | context('when contain flag present on assertion', function () { 56 | beforeEach(function () { 57 | $this->assertion->flag('contain', true); 58 | }); 59 | 60 | it('should return true if actual contains expected values', function () { 61 | $result = $this->matcher->match(['foo' => 1, 'bar' => 2, 'baz' => 3]); 62 | expect($result->isMatch())->to->be->true; 63 | }); 64 | 65 | it('should return true if keys exist, but corresponding values are null', function () { 66 | $result = $this->matcher->match(['foo' => 1, 'bar' => null]); 67 | expect($result->isMatch())->to->be->true; 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /specs/responder/responder.spec.php: -------------------------------------------------------------------------------- 1 | formatter = $this->getProphet()->prophesize('Peridot\Leo\Formatter\FormatterInterface'); 12 | $this->responder = new ExceptionResponder($this->formatter->reveal()); 13 | }); 14 | 15 | describe('->respond()', function () { 16 | beforeEach(function () { 17 | $this->formatter->getMessage(Argument::any())->willReturn('FAIL'); 18 | $this->match = new Match(false, 4, 3, false); 19 | $this->template = new ArrayTemplate([ 20 | 'default' => 'Default', 21 | 'negated' => 'Negated', 22 | ]); 23 | }); 24 | 25 | afterEach(function () { 26 | $this->getProphet()->checkPredictions(); 27 | }); 28 | 29 | it('should respond to a false match by throwing an exception', function () { 30 | $this->formatter->setMatch($this->match)->shouldBeCalled(); 31 | expect([$this->responder, 'respond'])->with($this->match, $this->template) 32 | ->to->throw('Peridot\Leo\Responder\Exception\AssertionException', 'FAIL'); 33 | }); 34 | 35 | it('should allow a user exception message', function () { 36 | $this->formatter->setMatch($this->match)->shouldBeCalled(); 37 | expect([$this->responder, 'respond'])->with($this->match, $this->template, 'user') 38 | ->to->throw('Peridot\Leo\Responder\Exception\AssertionException', 'user'); 39 | }); 40 | 41 | it('should trim the exception stack trace', function () { 42 | $this->formatter->setMatch($this->match)->shouldBeCalled(); 43 | $line = null; 44 | $exception = null; 45 | $trace = []; 46 | try { 47 | ($line = __LINE__) && $this->responder->respond($this->match, $this->template); 48 | } catch (AssertionException $exception) { 49 | $trace = $exception->getTrace(); 50 | } 51 | expect($exception)->to->be->an->instanceof('Peridot\Leo\Responder\Exception\AssertionException'); 52 | expect(count($trace))->to->equal(1); 53 | expect($trace[0]['file'])->to->equal(__FILE__); 54 | expect($trace[0]['line'])->to->equal($line); 55 | }); 56 | 57 | it('should do nothing for a true match', function () { 58 | $match = new Match(true, 3, 3, false); 59 | $template = new ArrayTemplate(['default' => '', 'negated' => '']); 60 | $this->formatter->getMessage(Argument::any())->shouldNotBeCalled(); 61 | $this->formatter->setMatch($match)->shouldNotBeCalled(); 62 | $this->responder->respond($match, $template); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/Responder/Exception/AssertionException.php: -------------------------------------------------------------------------------- 1 | getProperty('trace'); 27 | $traceProperty->setAccessible(true); 28 | $call = static::traceLeoCall($traceProperty->getValue($exception)); 29 | 30 | if ($call) { 31 | $trace = array($call); 32 | list($file, $line) = self::traceCallPosition($call); 33 | } else { 34 | $trace = array(); 35 | $file = null; 36 | $line = null; 37 | } 38 | 39 | $traceProperty->setValue($exception, $trace); 40 | self::updateExceptionPosition($reflector, $exception, $file, $line); 41 | } 42 | 43 | /** 44 | * Find the Leo entry point call in a stack trace. 45 | * 46 | * @param array $trace The stack trace. 47 | * 48 | * @return array|null The call, or null if unable to determine the entry point. 49 | */ 50 | public static function traceLeoCall(array $trace) 51 | { 52 | for ($i = count($trace) - 1; $i >= 0; --$i) { 53 | if (self::isLeoTraceEntry($trace[$i])) { 54 | return $trace[$i]; 55 | } 56 | } 57 | 58 | return null; 59 | } 60 | 61 | /** 62 | * Construct a new assertion exception. 63 | * 64 | * @param string $message The message. 65 | */ 66 | public function __construct($message) 67 | { 68 | parent::__construct($message); 69 | 70 | static::trim($this); 71 | } 72 | 73 | private static function isLeoTraceEntry($entry) 74 | { 75 | $prefix = 'Peridot\\Leo\\'; 76 | 77 | if (isset($entry['class'])) { 78 | return 0 === strpos($entry['class'], $prefix); 79 | } 80 | 81 | return 0 === strpos($entry['function'], $prefix); 82 | } 83 | 84 | private static function traceCallPosition($call) 85 | { 86 | return array( 87 | isset($call['file']) ? $call['file'] : null, 88 | isset($call['line']) ? $call['line'] : null, 89 | ); 90 | } 91 | 92 | private static function updateExceptionPosition($reflector, $exception, $file, $line) 93 | { 94 | $fileProperty = $reflector->getProperty('file'); 95 | $fileProperty->setAccessible(true); 96 | $fileProperty->setValue($exception, $file); 97 | 98 | $lineProperty = $reflector->getProperty('line'); 99 | $lineProperty->setAccessible(true); 100 | $lineProperty->setValue($exception, $line); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Leo logo][logo-image] 2 | 3 | [logo-image]: https://raw.github.com/peridot-php/leo/master/leo.png "Leo logo" 4 | 5 | # Leo 6 | 7 | Next level assertion library for PHP 8 | 9 | [![Current version image][version-image]][current version] 10 | [![Current build status image][build-image]][current build status] 11 | [![Current Scrutinizer code quality image][scrutinizer-image]][current scrutinizer code quality] 12 | [![Current coverage status image][coverage-image]][current coverage status] 13 | 14 | [build-image]: https://img.shields.io/travis/peridot-php/leo/master.svg?style=flat-square "Current build status for the master branch" 15 | [coverage-image]: https://img.shields.io/codecov/c/github/peridot-php/leo/master.svg?style=flat-square "Current test coverage for the master branch" 16 | [current build status]: https://travis-ci.org/peridot-php/leo 17 | [current coverage status]: https://codecov.io/github/peridot-php/leo 18 | [current scrutinizer code quality]: https://scrutinizer-ci.com/g/peridot-php/leo/?branch=master 19 | [current version]: https://packagist.org/packages/peridot-php/leo 20 | [scrutinizer-image]: https://img.shields.io/scrutinizer/g/peridot-php/leo/master.svg?style=flat-square "Current Scrutinizer code quality for the master branch" 21 | [version-image]: https://img.shields.io/packagist/v/peridot-php/leo.svg?style=flat-square "This project uses semantic versioning" 22 | 23 | Visit the main site and documentation at [peridot-php.github.io/leo/](http://peridot-php.github.io/leo/). 24 | 25 | ## Expect Interface 26 | 27 | Leo supports a chainable interface for writing assertions via the `expect` 28 | function: 29 | 30 | ```php 31 | expect($obj)->to->have->property('name'); 32 | expect($value)->to->be->ok 33 | expect($fn)->to->throw('InvalidArgumentException', 'Expected message'); 34 | expect($array)->to->be->an('array'); 35 | expect($result)->to->not->be->empty; 36 | ``` 37 | 38 | ## Assert Interface 39 | 40 | Leo supports a more object oriented, non-chainable interface via `Assert`: 41 | 42 | ```php 43 | use Peridot\Leo\Interfaces\Assert; 44 | 45 | $assert = new Assert(); 46 | $assert->ok(true); 47 | $assert->doesNotThrow($fn, 'Exception'); 48 | $assert->isResource(tmpfile()); 49 | $assert->notEqual($actual, $expected); 50 | ``` 51 | 52 | ## Detailed error messages 53 | 54 | Leo matchers generate detailed error messages for failed assertions: 55 | 56 | ![Leo messages][error-message-image] 57 | 58 | [error-message-image]: https://raw.github.com/peridot-php/leo/master/message.png "Leo messages" 59 | 60 | ## Plugins 61 | 62 | Leo can be easily customized. For an example see [LeoHttpFoundation]. Read more 63 | on the [plugin guide]. 64 | 65 | [leohttpfoundation]: https://github.com/peridot-php/leo-http-foundation 66 | [plugin guide]: http://peridot-php.github.io/leo/plugins.html 67 | 68 | ## Running Tests 69 | 70 | make test 71 | 72 | ## Generating Docs 73 | 74 | Documentation is generated via [ApiGen]. Simply run: 75 | 76 | make docs 77 | 78 | [apigen]: http://apigen.org/ 79 | 80 | ## Thanks 81 | 82 | Leo was inspired by several great projects: 83 | 84 | - [Chai] for JS 85 | - [Jasmine] for JS 86 | - [Espérance] for PHP 87 | - [Pho] for PHP 88 | 89 | And of course our work on [Peridot] gave incentive to make a useful complement. 90 | 91 | [chai]: http://chaijs.com/ 92 | [espérance]: https://github.com/esperance/esperance 93 | [jasmine]: http://jasmine.github.io/ 94 | [peridot]: http://peridot-php.github.io/ 95 | [pho]: https://github.com/danielstjules/pho 96 | -------------------------------------------------------------------------------- /src/Matcher/RangeMatcher.php: -------------------------------------------------------------------------------- 1 | setLowerBound($lower); 33 | $this->setUpperBound($upper); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | * 39 | * @return TemplateInterface 40 | */ 41 | public function getDefaultCountableTemplate() 42 | { 43 | return new ArrayTemplate([ 44 | 'default' => "Expected {{actual}} to be within {$this->lowerBound}..{$this->upperBound}", 45 | 'negated' => "Expected {{actual}} to not be within {$this->lowerBound}..{$this->upperBound}", 46 | ]); 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | * 52 | * @return TemplateInterface 53 | */ 54 | public function getDefaultTemplate() 55 | { 56 | return new ArrayTemplate([ 57 | 'default' => "Expected {{actual}} to be within {$this->lowerBound}..{$this->upperBound}", 58 | 'negated' => "Expected {{actual}} to not be within {$this->lowerBound}..{$this->upperBound}", 59 | ]); 60 | } 61 | 62 | /** 63 | * Set the lower bound of the range matcher. 64 | * 65 | * @param mixed $lowerBound 66 | * @return $this 67 | */ 68 | public function setLowerBound($lowerBound) 69 | { 70 | if (!is_numeric($lowerBound)) { 71 | throw new \InvalidArgumentException('Lower bound must be a numeric value'); 72 | } 73 | 74 | $this->lowerBound = $lowerBound; 75 | 76 | return $this; 77 | } 78 | 79 | /** 80 | * Set the upper bound of the range matcher. 81 | * 82 | * @param mixed $upperBound 83 | * @return $this 84 | */ 85 | public function setUpperBound($upperBound) 86 | { 87 | if (!is_numeric($upperBound)) { 88 | throw new \InvalidArgumentException('Upper bound must be a numeric value'); 89 | } 90 | 91 | $this->upperBound = $upperBound; 92 | 93 | return $this; 94 | } 95 | 96 | /** 97 | * Return the lower bound of the range matcher. 98 | * 99 | * @return mixed 100 | */ 101 | public function getLowerBound() 102 | { 103 | return $this->lowerBound; 104 | } 105 | 106 | /** 107 | * Return the upper bound of the range matcher. 108 | * 109 | * @return mixed 110 | */ 111 | public function getUpperBound() 112 | { 113 | return $this->upperBound; 114 | } 115 | 116 | /** 117 | * Determine if the number is between an upper and lower bound (inclusive). 118 | * 119 | * @param $number 120 | * @return bool 121 | */ 122 | protected function matchNumeric($number) 123 | { 124 | return $number <= $this->upperBound && $number >= $this->lowerBound; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Matcher/KeysMatcher.php: -------------------------------------------------------------------------------- 1 | expected) > 1) { 34 | $subject = 'keys'; 35 | } 36 | 37 | $template = new ArrayTemplate([ 38 | 'default' => "Expected {{actual}} to {$this->verb} $subject {{keys}}", 39 | 'negated' => "Expected {{actual}} to not {$this->verb} $subject {{keys}}", 40 | ]); 41 | 42 | return $template->setTemplateVars(['keys' => $this->getKeyString()]); 43 | } 44 | 45 | /** 46 | * Assert that the actual value is an array or object with the expected keys. 47 | * 48 | * @param $actual 49 | * @return mixed 50 | */ 51 | protected function doMatch($actual) 52 | { 53 | $actual = $this->getArrayValue($actual); 54 | if ($this->assertion->flag('contain')) { 55 | $this->verb = 'contain'; 56 | 57 | return $this->matchInclusion($actual); 58 | } 59 | $keys = array_keys($actual); 60 | 61 | return $keys == $this->expected; 62 | } 63 | 64 | /** 65 | * Normalize the actual value into an array, whether it is an object 66 | * or an array. 67 | * 68 | * @param object|array $actual 69 | */ 70 | protected function getArrayValue($actual) 71 | { 72 | if (is_object($actual)) { 73 | return get_object_vars($actual); 74 | } 75 | 76 | if (is_array($actual)) { 77 | return $actual; 78 | } 79 | 80 | throw new \InvalidArgumentException('KeysMatcher expects object or array'); 81 | } 82 | 83 | /** 84 | * Returns a formatted string of expected keys. 85 | * 86 | * @return string keys 87 | */ 88 | protected function getKeyString() 89 | { 90 | $expected = $this->expected; 91 | $keys = ''; 92 | $tail = array_pop($expected); 93 | 94 | if (!empty($expected)) { 95 | $keys = implode('","', $expected) . '", and "'; 96 | } 97 | 98 | $keys .= $tail; 99 | 100 | return $keys; 101 | } 102 | 103 | /** 104 | * Used when the 'contain' flag exists on the Assertion. Checks 105 | * if the expected keys are included in the object or array. 106 | * 107 | * @param array $actual 108 | * @return true 109 | */ 110 | protected function matchInclusion($actual) 111 | { 112 | foreach ($this->expected as $key) { 113 | if (!array_key_exists($key, $actual)) { 114 | return false; 115 | } 116 | } 117 | 118 | return true; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /specs/assertion.spec.php: -------------------------------------------------------------------------------- 1 | responder = new ExceptionResponder($formatter); 12 | $this->assertion = new Assertion($this->responder, 'actual'); 13 | $this->assertion->addMethod('dynamicmethod', function ($expected) { 14 | return new SameMatcher($expected); 15 | }); 16 | $this->assertion->addMethod('nonmatcher', function ($expected) { 17 | return $expected; 18 | }); 19 | }); 20 | 21 | describe('->getResponder()', function () { 22 | it('should return the Assertion responder', function () { 23 | expect($this->assertion->getResponder())->to->equal($this->responder); 24 | }); 25 | }); 26 | 27 | context('when calling a dynamic method', function () { 28 | it('should throw an exception if method does not exist', function () { 29 | expect([$this->assertion, 'notamethod'])->to->throw('BadMethodCallException'); 30 | }); 31 | 32 | it('should return the result of a non-matcher method', function () { 33 | $result = $this->assertion->nonmatcher(1); 34 | expect($result)->to->equal(1); 35 | }); 36 | }); 37 | 38 | context('when calling a dynamic property', function () { 39 | it('should throw an exception if property does not exist', function () { 40 | expect(function () { 41 | $nope = $this->assertion->nope; 42 | })->to->throw('DomainException'); 43 | }); 44 | 45 | it('should return a cached version of the property if it is memoized', function () { 46 | $this->assertion->addProperty('thing', function () { 47 | return new stdClass(); 48 | }, true); 49 | 50 | $thing = $this->assertion->thing; 51 | $thingAgain = $this->assertion->thing; 52 | 53 | expect($thing)->to->equal($thingAgain); 54 | }); 55 | }); 56 | 57 | describe('->flag()', function () { 58 | it('should act as getter and setter', function () { 59 | $this->assertion->flag('not', true); 60 | expect($this->assertion->flag('not'))->to->equal(true); 61 | }); 62 | 63 | it('should return null if flag does not exist', function () { 64 | $flag = $this->assertion->flag('nope'); 65 | expect(is_null($flag))->to->equal(true); 66 | }); 67 | }); 68 | 69 | describe('->extend()', function () { 70 | it('should execute callable in a file', function () { 71 | $plugin = __DIR__ . '/fixtures/extend.php'; 72 | $this->assertion->extend($plugin); 73 | expect($this->assertion->fixture())->to->equal(5); 74 | }); 75 | 76 | it('should execute a passed in callable', function () { 77 | $this->assertion->extend(function ($assertion) { 78 | $assertion->addMethod('fixture', function () { 79 | return 4; 80 | }); 81 | }); 82 | expect($this->assertion->fixture())->to->equal(4); 83 | }); 84 | 85 | it('should throw an exception if no callable given', function () { 86 | expect([$this->assertion, 'extend'])->with('string')->to->throw('InvalidArgumentException'); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /specs/matcher/property-matcher.spec.php: -------------------------------------------------------------------------------- 1 | matcher = new PropertyMatcher('name'); 9 | $response = $this->getProphet()->prophesize('Peridot\Leo\Responder\ResponderInterface'); 10 | $this->assertion = new Assertion($response->reveal()); 11 | $this->matcher->setAssertion($this->assertion); 12 | }); 13 | 14 | describe('->match()', function () { 15 | it('should return a true result if actual value has the given property', function () { 16 | $actual = new stdClass(); 17 | $actual->name = 'brian'; 18 | $result = $this->matcher->match($actual); 19 | expect($result->isMatch())->to->be->true; 20 | }); 21 | 22 | it('should return false if actual value does not have the given property', function () { 23 | $actual = new stdClass(); 24 | $result = $this->matcher->match($actual); 25 | expect($result->isMatch())->to->be->false; 26 | }); 27 | 28 | it('should return true if actual value has property with matching value', function () { 29 | $actual = new stdClass(); 30 | $actual->name = 'brian'; 31 | $result = $this->matcher 32 | ->setValue('brian') 33 | ->match($actual); 34 | expect($result->isMatch())->to->be->true; 35 | }); 36 | 37 | it('should return false if actual value has property with incorrect value', function () { 38 | $actual = new stdClass(); 39 | $actual->name = 'brian'; 40 | $result = $this->matcher 41 | ->setValue('ryan') 42 | ->match($actual); 43 | expect($result->isMatch())->to->be->false; 44 | }); 45 | 46 | context('when matching against an array', function () { 47 | beforeEach(function () { 48 | $this->matcher = new PropertyMatcher(1); 49 | $this->matcher->setAssertion($this->assertion); 50 | }); 51 | 52 | it('should return true if array has index', function () { 53 | $actual = [1, 2]; 54 | $result = $this->matcher->match($actual); 55 | expect($result->isMatch())->to->be->true; 56 | }); 57 | 58 | it('should return false if array does not have index', function () { 59 | $actual = [1]; 60 | $result = $this->matcher->match($actual); 61 | expect($result->isMatch())->to->be->false; 62 | }); 63 | 64 | it('should return true if array has index with matching value', function () { 65 | $actual = [1, 3]; 66 | $result = $this->matcher 67 | ->setValue(3) 68 | ->match($actual); 69 | expect($result->isMatch())->to->be->true; 70 | }); 71 | 72 | it('should return false if array has index without matching value', function () { 73 | $actual = [1, 3]; 74 | $result = $this->matcher 75 | ->setValue(4) 76 | ->match($actual); 77 | expect($result->isMatch())->to->be->false; 78 | }); 79 | }); 80 | 81 | context('when actual is not an array or object', function () { 82 | it('should throw an exception', function () { 83 | expect([$this->matcher, 'match']) 84 | ->with('string')->to->throw('InvalidArgumentException'); 85 | }); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /specs/responder/exception/assertion-exception.spec.php: -------------------------------------------------------------------------------- 1 | getTrace()))->to->equal(0); 11 | }); 12 | }); 13 | 14 | describe('::traceLeoCall()', function () { 15 | it('handles method call traces', function () { 16 | $trace = [ 17 | [ 18 | 'file' => '/path/to/file/a', 19 | 'line' => 111, 20 | 'function' => 'methodA', 21 | 'class' => 'Peridot\Leo\ClassA', 22 | ], 23 | [ 24 | 'file' => '/path/to/file/b', 25 | 'line' => 222, 26 | 'function' => 'methodB', 27 | 'class' => 'Peridot\Leo\ClassB', 28 | ], 29 | [ 30 | 'file' => '/path/to/file/c', 31 | 'line' => 333, 32 | 'function' => 'methodC', 33 | 'class' => 'ClassC', 34 | ], 35 | ]; 36 | $expected = [ 37 | 'file' => '/path/to/file/b', 38 | 'line' => 222, 39 | 'function' => 'methodB', 40 | 'class' => 'Peridot\Leo\ClassB', 41 | ]; 42 | 43 | expect(AssertionException::traceLeoCall($trace))->to->equal($expected); 44 | }); 45 | 46 | it('handles function call traces', function () { 47 | $trace = [ 48 | [ 49 | 'file' => '/path/to/file/a', 50 | 'line' => 111, 51 | 'function' => 'methodA', 52 | 'class' => 'Peridot\Leo\ClassA', 53 | ], 54 | [ 55 | 'file' => '/path/to/file/b', 56 | 'line' => 222, 57 | 'function' => 'Peridot\Leo\functionB', 58 | ], 59 | [ 60 | 'file' => '/path/to/file/c', 61 | 'line' => 333, 62 | 'function' => 'functionC', 63 | ], 64 | ]; 65 | $expected = [ 66 | 'file' => '/path/to/file/b', 67 | 'line' => 222, 68 | 'function' => 'Peridot\Leo\functionB', 69 | ]; 70 | 71 | expect(AssertionException::traceLeoCall($trace))->to->equal($expected); 72 | }); 73 | 74 | it('handles traces with no external calls', function () { 75 | $trace = [ 76 | [ 77 | 'file' => '/path/to/file/a', 78 | 'line' => 111, 79 | 'function' => 'methodA', 80 | 'class' => 'Peridot\Leo\ClassA', 81 | ], 82 | [ 83 | 'file' => '/path/to/file/b', 84 | 'line' => 222, 85 | 'function' => 'Peridot\Leo\functionB', 86 | ], 87 | ]; 88 | $expected = [ 89 | 'file' => '/path/to/file/b', 90 | 'line' => 222, 91 | 'function' => 'Peridot\Leo\functionB', 92 | ]; 93 | 94 | expect(AssertionException::traceLeoCall($trace))->to->equal($expected); 95 | }); 96 | 97 | it('handles empty traces', function () { 98 | expect(AssertionException::traceLeoCall([]))->to->equal(null); 99 | }); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /src/ObjectPath/ObjectPath.php: -------------------------------------------------------------------------------- 1 | subject = $subject; 33 | } 34 | 35 | /** 36 | * Returns an ObjectPathValue if the property described by $path 37 | * can be located in the subject. 38 | * 39 | * A path expression uses object and array syntax. 40 | * 41 | * @code 42 | * 43 | * $person = new stdClass(); 44 | * $person->name = new stdClass(); 45 | * $person->name->first = 'brian'; 46 | * $person->name->last = 'scaturro'; 47 | * $person->hobbies = ['programming', 'reading', 'board games']; 48 | * 49 | * $path = new ObjectPath($person); 50 | * $first = $path->get('name->first'); 51 | * $reading = $path->get('hobbies[0]'); 52 | * 53 | * @endcode 54 | * 55 | * @param string $path 56 | * @return ObjectPathValue 57 | */ 58 | public function get($path) 59 | { 60 | $parts = $this->getPathParts($path); 61 | $properties = $this->getPropertyCollection($this->subject); 62 | $pathValue = null; 63 | while (!empty($properties) && !empty($parts)) { 64 | $key = array_shift($parts); 65 | $key = $this->normalizeKey($key); 66 | $pathValue = $this->getPathValue($key, $properties); 67 | 68 | if (!array_key_exists($key, $properties)) { 69 | break; 70 | } 71 | 72 | $properties = $this->getPropertyCollection($properties[$key]); 73 | } 74 | 75 | return $pathValue; 76 | } 77 | 78 | /** 79 | * Breaks a path expression into an array used 80 | * for navigating a path. 81 | * 82 | * @param $path 83 | * @return array 84 | */ 85 | public function getPathParts($path) 86 | { 87 | $path = preg_replace('/\[/', '->[', $path); 88 | if (preg_match('/^->/', $path)) { 89 | $path = substr($path, 2); 90 | } 91 | 92 | return explode('->', $path); 93 | } 94 | 95 | /** 96 | * Returns a property as an array. 97 | * 98 | * @param $subject 99 | * @return array 100 | */ 101 | protected function getPropertyCollection($subject) 102 | { 103 | if (is_object($subject)) { 104 | return get_object_vars($subject); 105 | } 106 | 107 | return $subject; 108 | } 109 | 110 | /** 111 | * Return a key that can be used on the current subject. 112 | * 113 | * @param $key 114 | * @param $matches 115 | * @return mixed 116 | */ 117 | protected function normalizeKey($key) 118 | { 119 | if (preg_match(self::$arrayKey, $key, $matches)) { 120 | $key = $matches[1]; 121 | 122 | return $key; 123 | } 124 | 125 | return $key; 126 | } 127 | 128 | /** 129 | * Given a key and a collection of properties, this method 130 | * will return an ObjectPathValue if possible. 131 | * 132 | * @param $key 133 | * @param $properties 134 | * @return null|ObjectPathValue 135 | */ 136 | protected function getPathValue($key, $properties) 137 | { 138 | if (!array_key_exists($key, $properties)) { 139 | return null; 140 | } 141 | 142 | return new ObjectPathValue($key, $properties[$key]); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Leo.php: -------------------------------------------------------------------------------- 1 | formatter = new Formatter(); 51 | $this->responder = new ExceptionResponder($this->formatter); 52 | $this->assertion = new Assertion($this->responder); 53 | 54 | $this->assertion->extend(__DIR__ . '/Core/Definitions.php'); 55 | } 56 | 57 | /** 58 | * Return the Leo Assertion. 59 | * 60 | * @return Assertion 61 | */ 62 | public function getAssertion() 63 | { 64 | return $this->assertion; 65 | } 66 | 67 | /** 68 | * Set the Assertion used by Leo. 69 | * 70 | * @param $assertion 71 | * @return $this 72 | */ 73 | public function setAssertion($assertion) 74 | { 75 | $this->assertion = $assertion; 76 | 77 | return $this; 78 | } 79 | 80 | /** 81 | * Return the FormatterInterface used by Leo. 82 | * 83 | * @return FormatterInterface 84 | */ 85 | public function getFormatter() 86 | { 87 | return $this->formatter; 88 | } 89 | 90 | /** 91 | * Set the FormatterInterface used by Leo. 92 | * 93 | * @param FormatterInterface $formatter 94 | * @return $this 95 | */ 96 | public function setFormatter(FormatterInterface $formatter) 97 | { 98 | $this->formatter = $formatter; 99 | 100 | return $this; 101 | } 102 | 103 | /** 104 | * Return the ResponderInterface being used by Leo. 105 | * 106 | * @return ResponderInterface 107 | */ 108 | public function getResponder() 109 | { 110 | return $this->responder; 111 | } 112 | 113 | /** 114 | * Set the ResponderInterface used by Leo. 115 | * 116 | * @param ResponderInterface $responder 117 | * @return $this 118 | */ 119 | public function setResponder(ResponderInterface $responder) 120 | { 121 | $this->responder = $responder; 122 | 123 | return $this; 124 | } 125 | 126 | /** 127 | * Singleton access to Leo. A singleton is used instead of a facade as 128 | * PHP has some hangups about binding scope from static methods. This should 129 | * be used to access all Assertion members. 130 | * 131 | * @code 132 | * 133 | * $assertion = Leo::instance()->getAssertion(); 134 | * $assertion->extend(function($assertion)) { 135 | * $assertion->addMethod('coolAssertion', function($expected, $message = "") { 136 | * $this->flag('message', $message); 137 | * return new CoolMatcher($expected); 138 | * }); 139 | * }); 140 | * 141 | * @endcode 142 | * 143 | * @return Leo 144 | */ 145 | public static function instance() 146 | { 147 | if (!self::$instance) { 148 | self::$instance = new self(); 149 | } 150 | 151 | return self::$instance; 152 | } 153 | 154 | /** 155 | * Singleton access to Leo's assertion object. 156 | * 157 | * @return Assertion 158 | */ 159 | public static function assertion() 160 | { 161 | return self::instance()->getAssertion(); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Interfaces/Assert/ObjectAssertTrait.php: -------------------------------------------------------------------------------- 1 | assertion->setActual($object); 23 | 24 | return $this->assertion->is->instanceof($class, $message); 25 | } 26 | 27 | /** 28 | * Perform a negated instanceof assertion. 29 | * 30 | * @param object $object 31 | * @param string $class 32 | * @param string $message 33 | */ 34 | public function notInstanceOf($object, $class, $message = '') 35 | { 36 | $this->assertion->setActual($object); 37 | 38 | return $this->assertion->is->not->instanceof($class, $message); 39 | } 40 | 41 | /** 42 | * Perform a property assertion. 43 | * 44 | * @param array|object $object 45 | * @param string $property 46 | * @param string $message 47 | */ 48 | public function property($object, $property, $message = '') 49 | { 50 | $this->assertion->setActual($object); 51 | 52 | return $this->assertion->to->have->property($property, null, $message); 53 | } 54 | 55 | /** 56 | * Perform a negated property assertion. 57 | * 58 | * @param array|object $object 59 | * @param string $property 60 | * @param string $message 61 | */ 62 | public function notProperty($object, $property, $message = '') 63 | { 64 | $this->assertion->setActual($object); 65 | 66 | return $this->assertion->to->not->have->property($property, null, $message); 67 | } 68 | 69 | /** 70 | * Perform a deep property assertion. 71 | * 72 | * @param array|object $object 73 | * @param string $property 74 | * @param string $message 75 | */ 76 | public function deepProperty($object, $property, $message = '') 77 | { 78 | $this->assertion->setActual($object); 79 | 80 | return $this->assertion->to->have->deep->property($property, null, $message); 81 | } 82 | 83 | /** 84 | * Perform a negated deep property assertion. 85 | * 86 | * @param array|object $object 87 | * @param string $property 88 | * @param string $message 89 | */ 90 | public function notDeepProperty($object, $property, $message = '') 91 | { 92 | $this->assertion->setActual($object); 93 | 94 | return $this->assertion->to->not->have->deep->property($property, null, $message); 95 | } 96 | 97 | /** 98 | * Perform a property value assertion. 99 | * 100 | * @param array|object $object 101 | * @param string $property 102 | * @param mixed $value 103 | * @param string $message 104 | */ 105 | public function propertyVal($object, $property, $value, $message = '') 106 | { 107 | $this->assertion->setActual($object); 108 | 109 | return $this->assertion->to->have->property($property, $value, $message); 110 | } 111 | 112 | /** 113 | * Perform a negated property value assertion. 114 | * 115 | * @param array|object $object 116 | * @param string $property 117 | * @param mixed $value 118 | * @param string $message 119 | */ 120 | public function propertyNotVal($object, $property, $value, $message = '') 121 | { 122 | $this->assertion->setActual($object); 123 | 124 | return $this->assertion->to->not->have->property($property, $value, $message); 125 | } 126 | 127 | /** 128 | * Perform a deep property value assertion. 129 | * 130 | * @param array|object $object 131 | * @param string $property 132 | * @param mixed $value 133 | * @param string $message 134 | */ 135 | public function deepPropertyVal($object, $property, $value, $message = '') 136 | { 137 | $this->assertion->setActual($object); 138 | 139 | return $this->assertion->to->have->deep->property($property, $value, $message); 140 | } 141 | 142 | /** 143 | * Perform a negated deep property value assertion. 144 | * 145 | * @param array|object $object 146 | * @param string $property 147 | * @param mixed $value 148 | * @param string $message 149 | */ 150 | public function deepPropertyNotVal($object, $property, $value, $message = '') 151 | { 152 | $this->assertion->setActual($object); 153 | 154 | return $this->assertion->to->not->have->deep->property($property, $value, $message); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/Interfaces/Assert.php: -------------------------------------------------------------------------------- 1 | 'loosely->equal', 33 | '===' => 'equal', 34 | '>' => 'above', 35 | '>=' => 'least', 36 | '<' => 'below', 37 | '<=' => 'most', 38 | '!=' => 'not->loosely->equal', 39 | '!==' => 'not->equal', 40 | ]; 41 | 42 | /** 43 | * @var Assertion 44 | */ 45 | protected $assertion; 46 | 47 | /** 48 | * @param Assertion $assertion 49 | */ 50 | public function __construct(Assertion $assertion = null) 51 | { 52 | if ($assertion === null) { 53 | $assertion = Leo::assertion(); 54 | } 55 | $this->assertion = $assertion; 56 | } 57 | 58 | /** 59 | * Perform an a loose equality assertion. 60 | * 61 | * @param mixed $actual 62 | * @param mixed $expected 63 | * @param string $message 64 | */ 65 | public function equal($actual, $expected, $message = '') 66 | { 67 | $this->assertion->setActual($actual); 68 | 69 | return $this->assertion->to->loosely->equal($expected, $message); 70 | } 71 | 72 | /** 73 | * Perform a negated loose equality assertion. 74 | * 75 | * @param mixed $actual 76 | * @param mixed $expected 77 | * @param string $message 78 | */ 79 | public function notEqual($actual, $expected, $message = '') 80 | { 81 | $this->assertion->setActual($actual); 82 | 83 | return $this->assertion->to->not->equal($expected, $message); 84 | } 85 | 86 | /** 87 | * Performs a throw assertion. 88 | * 89 | * @param callable $fn 90 | * @param $exceptionType 91 | * @param string $exceptionMessage 92 | * @param string $message 93 | */ 94 | public function throws(callable $fn, $exceptionType, $exceptionMessage = '', $message = '') 95 | { 96 | $this->assertion->setActual($fn); 97 | 98 | return $this->assertion->to->throw($exceptionType, $exceptionMessage, $message); 99 | } 100 | 101 | /** 102 | * Performs a negated throw assertion. 103 | * 104 | * @param callable $fn 105 | * @param $exceptionType 106 | * @param string $exceptionMessage 107 | * @param string $message 108 | */ 109 | public function doesNotThrow(callable $fn, $exceptionType, $exceptionMessage = '', $message = '') 110 | { 111 | $this->assertion->setActual($fn); 112 | 113 | return $this->assertion->not->to->throw($exceptionType, $exceptionMessage, $message); 114 | } 115 | 116 | /** 117 | * Perform an ok assertion. 118 | * 119 | * @param mixed $object 120 | * @param string $message 121 | */ 122 | public function ok($object, $message = '') 123 | { 124 | $this->assertion->setActual($object); 125 | 126 | return $this->assertion->to->be->ok($message); 127 | } 128 | 129 | /** 130 | * Perform a negated assertion. 131 | * 132 | * @param mixed $object 133 | * @param string $message 134 | */ 135 | public function notOk($object, $message = '') 136 | { 137 | $this->assertion->setActual($object); 138 | 139 | return $this->assertion->to->not->be->ok($message); 140 | } 141 | 142 | /** 143 | * Perform a strict equality assertion. 144 | * 145 | * @param mixed $actual 146 | * @param mixed $expected 147 | * @param string $message 148 | */ 149 | public function strictEqual($actual, $expected, $message = '') 150 | { 151 | $this->assertion->setActual($actual); 152 | 153 | return $this->assertion->to->equal($expected, $message); 154 | } 155 | 156 | /** 157 | * Perform a negated strict equality assertion. 158 | * 159 | * @param mixed $actual 160 | * @param mixed $expected 161 | * @param string $message 162 | */ 163 | public function notStrictEqual($actual, $expected, $message = '') 164 | { 165 | $this->assertion->setActual($actual); 166 | 167 | return $this->assertion->to->not->equal($expected, $message); 168 | } 169 | 170 | /** 171 | * Perform a pattern assertion. 172 | * 173 | * @param string $value 174 | * @param string $pattern 175 | * @param string $message 176 | */ 177 | public function match($value, $pattern, $message = '') 178 | { 179 | $this->assertion->setActual($value); 180 | 181 | return $this->assertion->to->match($pattern, $message); 182 | } 183 | 184 | /** 185 | * Perform a negated pattern assertion. 186 | * 187 | * @param string $value 188 | * @param string $pattern 189 | * @param string $message 190 | */ 191 | public function notMatch($value, $pattern, $message = '') 192 | { 193 | $this->assertion->setActual($value); 194 | 195 | return $this->assertion->to->not->match($pattern, $message); 196 | } 197 | 198 | /** 199 | * Compare two values using the given operator. 200 | * 201 | * @param mixed $left 202 | * @param string $operator 203 | * @param mixed $right 204 | * @param string $message 205 | */ 206 | public function operator($left, $operator, $right, $message = '') 207 | { 208 | if (!isset(static::$operators[$operator])) { 209 | throw new \InvalidArgumentException("Invalid operator $operator"); 210 | } 211 | $this->assertion->setActual($left); 212 | 213 | return $this->assertion->{static::$operators[$operator]}($right, $message); 214 | } 215 | 216 | /** 217 | * Defined to allow use of reserved words for methods. 218 | * 219 | * @param $method 220 | * @param $args 221 | */ 222 | public function __call($method, $args) 223 | { 224 | switch ($method) { 225 | case 'instanceOf': 226 | return call_user_func_array([$this, 'isInstanceOf'], $args); 227 | case 'include': 228 | return call_user_func_array([$this, 'isIncluded'], $args); 229 | default: 230 | throw new \BadMethodCallException("Call to undefined method $method"); 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/Matcher/ExceptionMatcher.php: -------------------------------------------------------------------------------- 1 | expected = $exceptionType; 26 | } 27 | 28 | /** 29 | * Set arguments to be passed to the callable. 30 | * 31 | * @param array $arguments 32 | * @return $this 33 | */ 34 | public function setArguments(array $arguments) 35 | { 36 | $this->arguments = $arguments; 37 | 38 | return $this; 39 | } 40 | 41 | /** 42 | * Set the expected message of the exception. 43 | * 44 | * @param string $message 45 | * @return $this 46 | */ 47 | public function setExpectedMessage($message) 48 | { 49 | $this->expectedMessage = $message; 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * Set the message thrown from an exception resulting from the 56 | * callable being invoked. 57 | * 58 | * @param string $message 59 | */ 60 | public function setMessage($message) 61 | { 62 | $this->message = $message; 63 | } 64 | 65 | /** 66 | * Returns the arguments passed to the callable. 67 | * 68 | * @return array 69 | */ 70 | public function getArguments() 71 | { 72 | return $this->arguments; 73 | } 74 | 75 | /** 76 | * Return the expected exception message. 77 | * 78 | * @return string 79 | */ 80 | public function getExpectedMessage() 81 | { 82 | return $this->expectedMessage; 83 | } 84 | 85 | /** 86 | * Return the message thrown by an exception resulting from the callable 87 | * being invoked. 88 | * 89 | * @return string 90 | */ 91 | public function getMessage() 92 | { 93 | return $this->message; 94 | } 95 | 96 | /** 97 | * {@inheritdoc} 98 | * 99 | * If the expected message has been set, the message template will be used. 100 | * 101 | * @return TemplateInterface 102 | */ 103 | public function getTemplate() 104 | { 105 | if ($this->expectedMessage) { 106 | return $this->getMessageTemplate(); 107 | } 108 | 109 | if (!isset($this->template)) { 110 | return $this->getDefaultTemplate(); 111 | } 112 | 113 | return $this->template; 114 | } 115 | 116 | /** 117 | * Set the template to be used when an expected exception message is provided. 118 | * 119 | * @param TemplateInterface $template 120 | * @return $this 121 | */ 122 | public function setMessageTemplate(TemplateInterface $template) 123 | { 124 | $this->messageTemplate = $template; 125 | 126 | return $this; 127 | } 128 | 129 | /** 130 | * Return a template for rendering exception message templates. 131 | * 132 | * return TemplateInterface 133 | */ 134 | public function getMessageTemplate() 135 | { 136 | if ($this->messageTemplate) { 137 | return $this->messageTemplate; 138 | } 139 | 140 | return $this->getDefaultMessageTemplate(); 141 | } 142 | 143 | /** 144 | * {@inheritdoc} 145 | * 146 | * @return TemplateInterface 147 | */ 148 | public function getDefaultTemplate() 149 | { 150 | return new ArrayTemplate([ 151 | 'default' => 'Expected exception of type {{expected}}', 152 | 'negated' => 'Expected type of exception not to be {{expected}}', 153 | ]); 154 | } 155 | 156 | /** 157 | * Return a default template for exception message assertions. 158 | * 159 | * @return ArrayTemplate 160 | */ 161 | public function getDefaultMessageTemplate() 162 | { 163 | return new ArrayTemplate([ 164 | 'default' => 'Expected exception message {{expected}}, got {{actual}}', 165 | 'negated' => 'Expected exception message {{actual}} not to equal {{expected}}', 166 | ]); 167 | } 168 | 169 | /** 170 | * Executes the callable and matches the exception type and exception message. 171 | * 172 | * @param $actual 173 | * @return Match 174 | */ 175 | public function match($actual) 176 | { 177 | $this->validateCallable($actual); 178 | 179 | list($exception, $message) = $this->callableException($actual); 180 | $this->setMessage($message); 181 | 182 | return $this->matchMessage($actual, $exception, $message); 183 | } 184 | 185 | /** 186 | * Validate that expected is indeed a valid callable. 187 | * 188 | * @throws \BadFunctionCallException 189 | */ 190 | protected function validateCallable($callable) 191 | { 192 | if (!is_callable($callable)) { 193 | $callable = rtrim(print_r($callable, true)); 194 | throw new \BadFunctionCallException('Invalid callable ' . $callable . ' given'); 195 | } 196 | } 197 | 198 | private function callableException($callable) 199 | { 200 | $exception = null; 201 | $message = null; 202 | 203 | try { 204 | call_user_func_array($callable, $this->arguments); 205 | } catch (Exception $exception) { 206 | $message = $exception->getMessage(); 207 | // fall-through ... 208 | } catch (Throwable $exception) { 209 | $message = $exception->getMessage(); 210 | // fall-through ... 211 | } 212 | 213 | return array($exception, $message); 214 | } 215 | 216 | private function matchMessage($actual, $exception, $message) 217 | { 218 | if (!$this->expectedMessage || $message === $this->expectedMessage) { 219 | return $this->matchType($actual, $exception); 220 | } 221 | 222 | $isNegated = $this->isNegated(); 223 | 224 | return new Match($isNegated, $this->expectedMessage, $message, $isNegated); 225 | } 226 | 227 | private function matchType($actual, $exception) 228 | { 229 | $isMatch = $exception instanceof $this->expected; 230 | $isNegated = $this->isNegated(); 231 | 232 | return new Match($isMatch xor $isNegated, $this->expected, $actual, $isNegated); 233 | } 234 | 235 | /** 236 | * @var mixed 237 | */ 238 | protected $expected; 239 | 240 | /** 241 | * @var array 242 | */ 243 | protected $arguments = []; 244 | 245 | /** 246 | * @var string 247 | */ 248 | protected $expectedMessage = ''; 249 | 250 | /** 251 | * A captured exception message. 252 | * 253 | * @var string 254 | */ 255 | protected $message; 256 | 257 | /** 258 | * @var TemplateInterface 259 | */ 260 | protected $messageTemplate; 261 | } 262 | -------------------------------------------------------------------------------- /src/Core/Definitions.php: -------------------------------------------------------------------------------- 1 | addProperty($chain, function () { 38 | return $this; 39 | }, true); 40 | } 41 | 42 | $assertion->addProperty('not', function () { 43 | return $this->flag('not', true); 44 | }); 45 | 46 | $assertion->addMethod('with', function () { 47 | return $this->flag('arguments', func_get_args()); 48 | }); 49 | 50 | $assertion->addProperty('loosely', function () { 51 | return $this->flag('loosely', true); 52 | }); 53 | 54 | $assertion->addMethod('equal', function ($expected, $message = '') { 55 | $this->flag('message', $message); 56 | if ($this->flag('loosely')) { 57 | return new EqualMatcher($expected); 58 | } 59 | 60 | return new SameMatcher($expected); 61 | }); 62 | 63 | $assertion->addMethod('throw', function ($exceptionType, $exceptionMessage = '', $message = '') { 64 | $this->flag('message', $message); 65 | $matcher = new ExceptionMatcher($exceptionType); 66 | $matcher->setExpectedMessage($exceptionMessage); 67 | $matcher->setArguments($this->flag('arguments') ?: []); 68 | 69 | return $matcher; 70 | }); 71 | 72 | $type = function ($expected, $message = '') { 73 | $this->flag('message', $message); 74 | 75 | return new TypeMatcher($expected); 76 | }; 77 | 78 | $assertion 79 | ->addMethod('a', $type) 80 | ->addMethod('an', $type); 81 | 82 | $include = function ($expected, $message = '') { 83 | $this->flag('message', $message); 84 | 85 | return new InclusionMatcher($expected); 86 | }; 87 | 88 | $assertion 89 | ->addMethod('include', $include) 90 | ->addMethod('contain', $include); 91 | 92 | $contain = function () { 93 | return $this->flag('contain', true); 94 | }; 95 | 96 | $assertion 97 | ->addProperty('contain', $contain) 98 | ->addProperty('include', $contain); 99 | 100 | $truthy = function ($message = '') { 101 | $this->flag('message', $message); 102 | 103 | return new TruthyMatcher(); 104 | }; 105 | 106 | $assertion 107 | ->addMethod('ok', $truthy) 108 | ->addProperty('ok', $truthy); 109 | 110 | $true = function ($message = '') { 111 | $this->flag('message', $message); 112 | 113 | return new TrueMatcher(); 114 | }; 115 | 116 | $assertion 117 | ->addMethod('true', $true) 118 | ->addProperty('true', $true); 119 | 120 | $false = function ($message = '') { 121 | $this->flag('message', $message); 122 | $matcher = new TrueMatcher(); 123 | 124 | return $matcher->invert(); 125 | }; 126 | 127 | $assertion 128 | ->addMethod('false', $false) 129 | ->addProperty('false', $false); 130 | 131 | $null = function ($message = '') { 132 | $this->flag('message', $message); 133 | 134 | return new NullMatcher(); 135 | }; 136 | 137 | $assertion 138 | ->addMethod('null', $null) 139 | ->addProperty('null', $null); 140 | 141 | $empty = function ($message = '') { 142 | $this->flag('message', $message); 143 | 144 | return new EmptyMatcher(); 145 | }; 146 | 147 | $assertion 148 | ->addMethod('empty', $empty) 149 | ->addProperty('empty', $empty); 150 | 151 | $assertion->addProperty('length', function () { 152 | return $this->flag('length', $this->getActual()); 153 | }); 154 | 155 | /* 156 | * Define a helper for creating countable matchers. A countable 157 | * matcher is a matcher that matches against a single numeric 158 | * value, or a value that can be reduced to a single numeric value 159 | * via the count() function. 160 | * 161 | * @param $className 162 | * @return callable 163 | */ 164 | $countable = function ($className) { 165 | return function ($expected, $message = '') use ($className) { 166 | $this->flag('message', $message); 167 | $class = "Peridot\\Leo\\Matcher\\$className"; 168 | $matcher = new $class($expected); 169 | if ($countable = $this->flag('length')) { 170 | $matcher->setCountable($countable); 171 | } 172 | 173 | return $matcher; 174 | }; 175 | }; 176 | 177 | $assertion->addMethod('above', $countable('GreaterThanMatcher')); 178 | $assertion->addMethod('least', $countable('GreaterThanOrEqualMatcher')); 179 | $assertion->addMethod('below', $countable('LessThanMatcher')); 180 | $assertion->addMethod('most', $countable('LessThanOrEqualMatcher')); 181 | 182 | $assertion->addMethod('within', function ($lower, $upper, $message = '') { 183 | $this->flag('message', $message); 184 | $matcher = new RangeMatcher($lower, $upper); 185 | if ($countable = $this->flag('length')) { 186 | $matcher->setCountable($countable); 187 | } 188 | 189 | return $matcher; 190 | }); 191 | 192 | $assertion->addMethod('instanceof', function ($expected, $message = '') { 193 | $this->flag('message', $message); 194 | 195 | return new InstanceofMatcher($expected); 196 | }); 197 | 198 | $assertion->addProperty('deep', function () { 199 | return $this->flag('deep', true); 200 | }); 201 | 202 | $assertion->addMethod('property', function ($name, $value = '', $message = '') { 203 | $this->flag('message', $message); 204 | $matcher = new PropertyMatcher($name, $value); 205 | $matcher->setAssertion($this); 206 | 207 | return $matcher->setIsDeep($this->flag('deep')); 208 | }); 209 | 210 | $assertion->addMethod('length', function ($expected, $message = '') { 211 | $this->flag('message', $message); 212 | 213 | return new LengthMatcher($expected); 214 | }); 215 | 216 | $assertion->addMethod('match', function ($pattern, $message = '') { 217 | $this->flag('message', $message); 218 | 219 | return new PatternMatcher($pattern); 220 | }); 221 | 222 | $assertion->addMethod('string', function ($expected, $message = '') { 223 | $this->flag('message', $message); 224 | 225 | return new SubStringMatcher($expected); 226 | }); 227 | 228 | $assertion->addMethod('keys', function (array $keys, $message = '') { 229 | $this->flag('message', $message); 230 | 231 | return new KeysMatcher($keys); 232 | }); 233 | 234 | $assertion->addMethod('satisfy', function (callable $predicate, $message = '') { 235 | $this->flag('message', $message); 236 | 237 | return new PredicateMatcher($predicate); 238 | }); 239 | }; 240 | -------------------------------------------------------------------------------- /specs/matcher/exception-matcher.spec.php: -------------------------------------------------------------------------------- 1 | matcher = new ExceptionMatcher('DomainException'); 9 | }); 10 | 11 | describe('->match()', function () { 12 | it('should return true result if callable throws exception', function () { 13 | $result = $this->matcher->match(function () { 14 | throw new DomainException('hello world'); 15 | }); 16 | expect($result->isMatch())->to->equal(true); 17 | }); 18 | 19 | it('should return false result if callable throws different type', function () { 20 | $result = $this->matcher->match(function () { 21 | throw new RuntimeException('hello world'); 22 | }); 23 | expect($result->isMatch())->to->equal(false); 24 | }); 25 | 26 | it('should throw exception if callable is not valid', function () { 27 | $obj = new stdClass(); 28 | $matcher = new ExceptionMatcher('DomainException'); 29 | expect([$matcher, 'match'])->with([$obj, 'nope'])->to->throw('BadFunctionCallException'); 30 | }); 31 | 32 | it('should return false when function throws no exception', function () { 33 | $matcher = new ExceptionMatcher('Exception'); 34 | $result = $matcher->match(function () { 35 | }); 36 | expect($result->isMatch())->to->equal(false); 37 | }); 38 | 39 | context('when inverted', function () { 40 | it('should return true result if exceptions are different', function () { 41 | $result = $this->matcher->invert()->match(function () { 42 | throw new RuntimeException(); 43 | }); 44 | expect($result->isMatch())->to->equal(true); 45 | }); 46 | 47 | it('should return false result if exceptions are same', function () { 48 | $result = $this->matcher->invert()->match(function () { 49 | throw new DomainException(); 50 | }); 51 | expect($result->isMatch())->to->equal(false); 52 | }); 53 | }); 54 | 55 | context('when matching exception message', function () { 56 | it('should return true result if messages are the same', function () { 57 | $this->matcher->setExpectedMessage('hello world'); 58 | $result = $this->matcher->match(function () { 59 | throw new DomainException('hello world'); 60 | }); 61 | expect($result->isMatch())->to->equal(true); 62 | }); 63 | 64 | it('should return false result if messages are different', function () { 65 | $this->matcher->setExpectedMessage('goodbye'); 66 | $result = $this->matcher->match(function () { 67 | throw new DomainException('hello world'); 68 | }); 69 | expect($result->isMatch())->to->equal(false); 70 | }); 71 | 72 | it('should still check the exception type', function () { 73 | $this->matcher->setExpectedMessage('hello world'); 74 | $result = $this->matcher->match(function () { 75 | throw new RuntimeException('hello world'); 76 | }); 77 | expect($result->isMatch())->to->equal(false); 78 | }); 79 | 80 | context('and matcher is inverted', function () { 81 | it('should return true result if exception messages are different', function () { 82 | $this->matcher->setExpectedMessage('goodbye'); 83 | $result = $this->matcher->invert()->match(function () { 84 | throw new DomainException('hello world'); 85 | }); 86 | expect($result->isMatch())->to->equal(true); 87 | }); 88 | 89 | it('should return false result if exception messages are same', function () { 90 | $this->matcher->setExpectedMessage('hello world'); 91 | $result = $this->matcher->invert()->match(function () { 92 | throw new DomainException('hello world'); 93 | }); 94 | expect($result->isMatch())->to->equal(false); 95 | }); 96 | }); 97 | }); 98 | 99 | if (!interface_exists('Throwable')) { 100 | return; 101 | } 102 | 103 | context('with throwable', function () { 104 | beforeEach(function () { 105 | $this->matcher = new ExceptionMatcher('TypeError'); 106 | }); 107 | 108 | it('should return true result if callable throws throwable', function () { 109 | $result = $this->matcher->match(function () { 110 | throw new TypeError('hello world'); 111 | }); 112 | expect($result->isMatch())->to->equal(true); 113 | }); 114 | 115 | it('should return false result if callable throws different type', function () { 116 | $result = $this->matcher->match(function () { 117 | throw new ParseError('hello world'); 118 | }); 119 | expect($result->isMatch())->to->equal(false); 120 | }); 121 | }); 122 | }); 123 | 124 | describe('->getDefaultMessageTemplate()', function () { 125 | it('should it return a template for wrong exception type by default', function () { 126 | $template = $this->matcher->getDefaultMessageTemplate(); 127 | $default = $template->getDefaultTemplate(); 128 | $negated = $template->getNegatedTemplate(); 129 | expect($default)->to->equal('Expected exception message {{expected}}, got {{actual}}'); 130 | expect($negated)->to->equal('Expected exception message {{actual}} not to equal {{expected}}'); 131 | }); 132 | }); 133 | 134 | describe('->getTemplate()', function () { 135 | context('when expected message has been set', function () { 136 | beforeEach(function () { 137 | $this->matcher->setExpectedMessage('message'); 138 | }); 139 | 140 | it('should return the set message template', function () { 141 | $template = new ArrayTemplate([]); 142 | $this->matcher->setMessageTemplate($template); 143 | expect($this->matcher->getTemplate())->to->equal($template); 144 | }); 145 | }); 146 | 147 | context('when a custom template has been set', function () { 148 | beforeEach(function () { 149 | $this->template = new ArrayTemplate([]); 150 | $this->matcher->setTemplate($this->template); 151 | }); 152 | 153 | it('should return the set template', function () { 154 | expect($this->matcher->getTemplate())->to->equal($this->template); 155 | }); 156 | }); 157 | }); 158 | 159 | describe('->getArguments()', function () { 160 | it('should fetch callable arguments', function () { 161 | $args = [1, 2, 3]; 162 | $this->matcher->setArguments($args); 163 | expect($this->matcher->getArguments())->to->equal($args); 164 | }); 165 | }); 166 | 167 | describe('->getExpectedMessage()', function () { 168 | it('should fetch expected message', function () { 169 | $expected = 'expected'; 170 | $this->matcher->setExpectedMessage($expected); 171 | expect($this->matcher->getExpectedMessage())->to->equal($expected); 172 | }); 173 | }); 174 | 175 | describe('->getMessage()', function () { 176 | it('should fetch the actual message', function () { 177 | $expected = 'expected'; 178 | $this->matcher->setMessage($expected); 179 | expect($this->matcher->getMessage())->to->equal($expected); 180 | }); 181 | }); 182 | }); 183 | -------------------------------------------------------------------------------- /src/Matcher/PropertyMatcher.php: -------------------------------------------------------------------------------- 1 | setKey($key) 50 | ->setValue($value); 51 | } 52 | 53 | /** 54 | * Return the expected object or array key. 55 | * 56 | * @return int|string 57 | */ 58 | public function getKey() 59 | { 60 | return $this->key; 61 | } 62 | 63 | /** 64 | * Set the expected object or array key. 65 | * 66 | * @param int|string $key 67 | * @return $this 68 | */ 69 | public function setKey($key) 70 | { 71 | $this->key = $key; 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * Return the expected property value. 78 | * 79 | * @return mixed 80 | */ 81 | public function getValue() 82 | { 83 | return $this->value; 84 | } 85 | 86 | /** 87 | * Set the expected property value. 88 | * 89 | * @param mixed $value 90 | * @return $this 91 | */ 92 | public function setValue($value) 93 | { 94 | $this->value = $value; 95 | 96 | return $this; 97 | } 98 | 99 | /** 100 | * {@inheritdoc} 101 | * 102 | * @return TemplateInterface 103 | */ 104 | public function getDefaultTemplate() 105 | { 106 | list($default, $negated) = $this->getTemplateStrings(); 107 | 108 | $template = new ArrayTemplate([ 109 | 'default' => $default, 110 | 'negated' => $negated, 111 | ]); 112 | 113 | return $template->setTemplateVars([ 114 | 'key' => $this->getKey(), 115 | 'value' => $this->getValue(), 116 | 'actualValue' => $this->getActualValue(), 117 | ]); 118 | } 119 | 120 | /** 121 | * Return the actual value given to the matcher. 122 | * 123 | * @return mixed 124 | */ 125 | public function getActualValue() 126 | { 127 | return $this->actualValue; 128 | } 129 | 130 | /** 131 | * Set the actual value given to the matcher. Used to 132 | * store whether or not the actual value was set. 133 | * 134 | * @param mixed $actualValue 135 | * @return $this 136 | */ 137 | public function setActualValue($actualValue) 138 | { 139 | $this->actualValue = $actualValue; 140 | $this->actualValueSet = true; 141 | 142 | return $this; 143 | } 144 | 145 | /** 146 | * Return if the actual value has been set. 147 | * 148 | * @return bool 149 | */ 150 | public function isActualValueSet() 151 | { 152 | return $this->actualValueSet; 153 | } 154 | 155 | /** 156 | * Tell the property matcher to match deep properties. 157 | * 158 | * return $this 159 | */ 160 | public function setIsDeep($isDeep) 161 | { 162 | $this->isDeep = $isDeep; 163 | 164 | return $this; 165 | } 166 | 167 | /** 168 | * Return whether or not the matcher is matching deep properties. 169 | * 170 | * @return bool 171 | */ 172 | public function isDeep() 173 | { 174 | return $this->isDeep; 175 | } 176 | 177 | /** 178 | * Matches if the actual value has a property, optionally matching 179 | * the expected value of that property. If the deep flag is set, 180 | * the matcher will use the ObjectPath utility to parse deep expressions. 181 | * 182 | * @code 183 | * 184 | * $this->doMatch('child->name->first', 'brian'); 185 | * 186 | * @endcode 187 | * 188 | * @param mixed $actual 189 | * @return mixed 190 | */ 191 | protected function doMatch($actual) 192 | { 193 | $this->validateActual($actual); 194 | 195 | if ($this->isDeep()) { 196 | return $this->matchDeep($actual); 197 | } 198 | 199 | $actual = $this->actualToArray($actual); 200 | 201 | return $this->matchArrayIndex($actual); 202 | } 203 | 204 | /** 205 | * Convert the actual value to an array, whether it is an object or an array. 206 | * 207 | * @param object|array $actual 208 | * @return array|object 209 | */ 210 | protected function actualToArray($actual) 211 | { 212 | if (is_object($actual)) { 213 | return get_object_vars($actual); 214 | } 215 | 216 | return $actual; 217 | } 218 | 219 | /** 220 | * Match that an array index exists, and matches 221 | * the expected value if set. 222 | * 223 | * @param $actual 224 | * @return bool 225 | */ 226 | protected function matchArrayIndex($actual) 227 | { 228 | if (isset($actual[$this->getKey()])) { 229 | $this->assertion->setActual($actual[$this->getKey()]); 230 | 231 | return $this->isExpected($actual[$this->getKey()]); 232 | } 233 | 234 | return false; 235 | } 236 | 237 | /** 238 | * Uses ObjectPath to parse an expression if the deep flag 239 | * is set. 240 | * 241 | * @param $actual 242 | * @return bool 243 | */ 244 | protected function matchDeep($actual) 245 | { 246 | $path = new ObjectPath($actual); 247 | $value = $path->get($this->getKey()); 248 | 249 | if ($value === null) { 250 | return false; 251 | } 252 | 253 | $this->assertion->setActual($value->getPropertyValue()); 254 | 255 | return $this->isExpected($value->getPropertyValue()); 256 | } 257 | 258 | /** 259 | * Check if the given value is expected. 260 | * 261 | * @param $value 262 | * @return bool 263 | */ 264 | protected function isExpected($value) 265 | { 266 | if ($expected = $this->getValue()) { 267 | $this->setActualValue($value); 268 | 269 | return $this->getActualValue() === $expected; 270 | } 271 | 272 | return true; 273 | } 274 | 275 | /** 276 | * Ensure that the actual value is an object or an array. 277 | * 278 | * @param $actual 279 | */ 280 | protected function validateActual($actual) 281 | { 282 | if (!is_object($actual) && !is_array($actual)) { 283 | throw new \InvalidArgumentException('PropertyMatcher expects an object or an array'); 284 | } 285 | } 286 | 287 | /** 288 | * Returns the strings used in creating the template for the matcher. 289 | * 290 | * @return array 291 | */ 292 | protected function getTemplateStrings() 293 | { 294 | $default = 'Expected {{actual}} to have a{{deep}}property {{key}}'; 295 | $negated = 'Expected {{actual}} to not have a{{deep}}property {{key}}'; 296 | 297 | if ($this->getValue() && $this->isActualValueSet()) { 298 | $default = 'Expected {{actual}} to have a{{deep}}property {{key}} of {{value}}, but got {{actualValue}}'; 299 | $negated = 'Expected {{actual}} to not have a{{deep}}property {{key}} of {{value}}'; 300 | } 301 | 302 | $deep = ' '; 303 | if ($this->isDeep()) { 304 | $deep = ' deep '; 305 | } 306 | 307 | return str_replace('{{deep}}', $deep, [$default, $negated]); 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/Interfaces/Assert/TypeAssertTrait.php: -------------------------------------------------------------------------------- 1 | assertion->setActual($actual); 23 | 24 | return $this->assertion->to->be->a($expected, $message); 25 | } 26 | 27 | /** 28 | * Performs a negated type assertion. 29 | * 30 | * @param mixed $actual 31 | * @param string $expected 32 | * @param string $message 33 | */ 34 | public function notTypeOf($actual, $expected, $message = '') 35 | { 36 | $this->assertion->setActual($actual); 37 | 38 | return $this->assertion->to->not->be->a($expected, $message); 39 | } 40 | 41 | /** 42 | * Perform a true assertion. 43 | * 44 | * @param mixed $value 45 | * @param string $message 46 | */ 47 | public function isTrue($value, $message = '') 48 | { 49 | $this->assertion->setActual($value); 50 | 51 | return $this->assertion->to->be->true($message); 52 | } 53 | 54 | /** 55 | * Perform a false assertion. 56 | * 57 | * @param mixed $value 58 | * @param string $message 59 | */ 60 | public function isFalse($value, $message = '') 61 | { 62 | $this->assertion->setActual($value); 63 | 64 | return $this->assertion->to->be->false($message); 65 | } 66 | 67 | /** 68 | * Perform a null assertion. 69 | * 70 | * @param mixed $value 71 | * @param string $message 72 | */ 73 | public function isNull($value, $message = '') 74 | { 75 | $this->assertion->setActual($value); 76 | 77 | return $this->assertion->to->be->null($message); 78 | } 79 | 80 | /** 81 | * Perform a negated null assertion. 82 | * 83 | * @param mixed $value 84 | * @param string $message 85 | */ 86 | public function isNotNull($value, $message = '') 87 | { 88 | $this->assertion->setActual($value); 89 | 90 | return $this->assertion->to->not->be->null($message); 91 | } 92 | 93 | /** 94 | * Performs a predicate assertion to check if actual 95 | * value is callable. 96 | * 97 | * @param mixed $value 98 | * @param string $message 99 | */ 100 | public function isCallable($value, $message = '') 101 | { 102 | $this->assertion->setActual($value); 103 | 104 | return $this->assertion->to->satisfy('is_callable', $message); 105 | } 106 | 107 | /** 108 | * Performs a negated predicate assertion to check if actual 109 | * value is not a callable. 110 | * 111 | * @param mixed $value 112 | * @param string $message 113 | */ 114 | public function isNotCallable($value, $message = '') 115 | { 116 | $this->assertion->setActual($value); 117 | 118 | return $this->assertion->to->not->satisfy('is_callable', $message); 119 | } 120 | 121 | /** 122 | * Perform a type assertion for type "object.". 123 | * 124 | * @param mixed $value 125 | * @param string $message 126 | */ 127 | public function isObject($value, $message = '') 128 | { 129 | return $this->typeOf($value, 'object', $message); 130 | } 131 | 132 | /** 133 | * Perform a negative type assertion for type "object.". 134 | * 135 | * @param mixed $value 136 | * @param string $message 137 | */ 138 | public function isNotObject($value, $message = '') 139 | { 140 | return $this->notTypeOf($value, 'object', $message); 141 | } 142 | 143 | /** 144 | * Perform a type assertion for type "array.". 145 | * 146 | * @param mixed $value 147 | * @param string $message 148 | */ 149 | public function isArray($value, $message = '') 150 | { 151 | return $this->typeOf($value, 'array', $message); 152 | } 153 | 154 | /** 155 | * Performs a negative type assertion for type "array.". 156 | * 157 | * @param mixed $value 158 | * @param string $message 159 | */ 160 | public function isNotArray($value, $message = '') 161 | { 162 | return $this->notTypeOf($value, 'array', $message); 163 | } 164 | 165 | /** 166 | * Perform a type assertion for type "string.". 167 | * 168 | * @param mixed $value 169 | * @param string $message 170 | */ 171 | public function isString($value, $message = '') 172 | { 173 | return $this->typeOf($value, 'string', $message); 174 | } 175 | 176 | /** 177 | * Perform a negated type assertion for type "string.". 178 | * 179 | * @param mixed $value 180 | * @param string $message 181 | */ 182 | public function isNotString($value, $message = '') 183 | { 184 | return $this->notTypeOf($value, 'string', $message); 185 | } 186 | 187 | /** 188 | * Performs a predicate assertion to check if actual 189 | * value is numeric. 190 | * 191 | * @param mixed $value 192 | * @param string $message 193 | */ 194 | public function isNumeric($value, $message = '') 195 | { 196 | $this->assertion->setActual($value); 197 | 198 | return $this->assertion->to->satisfy('is_numeric', $message); 199 | } 200 | 201 | /** 202 | * Performs a negated predicate assertion to check if actual 203 | * value is numeric. 204 | * 205 | * @param mixed $value 206 | * @param string $message 207 | */ 208 | public function isNotNumeric($value, $message = '') 209 | { 210 | $this->assertion->setActual($value); 211 | 212 | return $this->assertion->not->to->satisfy('is_numeric', $message); 213 | } 214 | 215 | /** 216 | * Perform a type assertion for type "integer.". 217 | * 218 | * @param $value 219 | * @param string $message 220 | */ 221 | public function isInteger($value, $message = '') 222 | { 223 | return $this->typeOf($value, 'integer', $message); 224 | } 225 | 226 | /** 227 | * Perform a negated type assertion for type "integer.". 228 | * 229 | * @param mixed $value 230 | * @param string $message 231 | */ 232 | public function isNotInteger($value, $message = '') 233 | { 234 | return $this->notTypeOf($value, 'integer', $message); 235 | } 236 | 237 | /** 238 | * Perform a type assertion for type "double.". 239 | * 240 | * @param mixed $value 241 | * @param string $message 242 | */ 243 | public function isDouble($value, $message = '') 244 | { 245 | return $this->typeOf($value, 'double', $message); 246 | } 247 | 248 | /** 249 | * Perform a negated type assertion for type "double.". 250 | * 251 | * @param mixed $value 252 | * @param string $message 253 | */ 254 | public function isNotDouble($value, $message = '') 255 | { 256 | return $this->notTypeOf($value, 'double', $message); 257 | } 258 | 259 | /** 260 | * Perform a type assertion for type "resource.". 261 | * 262 | * @param mixed $value 263 | * @param string $message 264 | */ 265 | public function isResource($value, $message = '') 266 | { 267 | return $this->typeOf($value, 'resource', $message); 268 | } 269 | 270 | /** 271 | * Perform a negated type assertion for type "resource.". 272 | * 273 | * @param mixed $value 274 | * @param string $message 275 | */ 276 | public function isNotResource($value, $message = '') 277 | { 278 | return $this->notTypeOf($value, 'resource', $message); 279 | } 280 | 281 | /** 282 | * Perform a type assertion for type "boolean.". 283 | * 284 | * @param mixed $value 285 | * @param string $message 286 | */ 287 | public function isBoolean($value, $message = '') 288 | { 289 | return $this->typeOf($value, 'boolean', $message); 290 | } 291 | 292 | /** 293 | * Perform a negated type assertion for type "boolean.". 294 | * 295 | * @param mixed $value 296 | * @param string $message 297 | */ 298 | public function isNotBoolean($value, $message = '') 299 | { 300 | return $this->notTypeOf($value, 'boolean', $message); 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /src/Assertion.php: -------------------------------------------------------------------------------- 1 | equal() assertion 53 | * @property-read Assertion $contain enables the contain flag for use with the ->keys() assertion 54 | * @property-read Assertion $include enables the contain flag for use with the ->keys() assertion 55 | * @property-read Assertion $ok a lazy property that performs an ->ok() assertion 56 | * @property-read Assertion $true a lazy property that performs a ->true() assertion 57 | * @property-read Assertion $false a lazy property that performs a ->false() assertion 58 | * @property-read Assertion $null a lazy property that performs a ->null() assertion 59 | * @property-read Assertion $empty a lazy property that performs an ->empty() assertion 60 | * @property-read Assertion $length enables the length flag for use with countable assertions such as ->above(), ->least(), ->below(), ->most(), and ->within() 61 | * @property-read Assertion $deep enables the deep flag for use with assertions that need to traverse structures like the ->property() assertion 62 | * 63 | * @package Peridot\Leo 64 | */ 65 | final class Assertion 66 | { 67 | use DynamicObjectTrait; 68 | 69 | /** 70 | * A static cache for memoized properties. 71 | * 72 | * @var array 73 | */ 74 | private static $propertyCache = []; 75 | 76 | /** 77 | * @var ResponderInterface 78 | */ 79 | protected $responder; 80 | 81 | /** 82 | * @var mixed 83 | */ 84 | protected $actual; 85 | 86 | /** 87 | * @param ResponderInterface $responder 88 | */ 89 | public function __construct(ResponderInterface $responder, $actual = null) 90 | { 91 | $this->responder = $responder; 92 | $this->actual = $actual; 93 | } 94 | 95 | /** 96 | * Returns the current ResponderInterface assigned to this Assertion. 97 | * 98 | * @return ResponderInterface 99 | */ 100 | public function getResponder() 101 | { 102 | return $this->responder; 103 | } 104 | 105 | /** 106 | * Delegate methods to assertion methods. 107 | * 108 | * @param $method 109 | * @param $args 110 | * @return mixed 111 | */ 112 | public function __call($method, $args) 113 | { 114 | if (!isset($this->methods[$method])) { 115 | throw new \BadMethodCallException("Method $method does not exist"); 116 | } 117 | 118 | return $this->request($this->methods[$method], $args); 119 | } 120 | 121 | /** 122 | * @param $name 123 | * @return mixed 124 | */ 125 | public function __get($name) 126 | { 127 | if (!isset($this->properties[$name])) { 128 | throw new \DomainException("Property $name not found"); 129 | } 130 | 131 | if (array_key_exists($name, self::$propertyCache)) { 132 | return self::$propertyCache[$name]; 133 | } 134 | 135 | $property = $this->properties[$name]; 136 | $result = $this->request($property['factory']); 137 | 138 | if ($property['memoize']) { 139 | self::$propertyCache[$name] = $result; 140 | } 141 | 142 | return $result; 143 | } 144 | 145 | /** 146 | * A request to an Assertion will attempt to resolve 147 | * the result as an assertion before returning the result. 148 | * 149 | * @param callable $fn 150 | * @return mixed 151 | */ 152 | public function request(callable $fn, array $arguments = []) 153 | { 154 | $result = call_user_func_array($fn, $arguments); 155 | 156 | if ($result instanceof MatcherInterface) { 157 | return $this->assert($result); 158 | } 159 | 160 | return $result; 161 | } 162 | 163 | /** 164 | * Extend calls the given callable - or file that returns a callable - and passes the current Assertion instance 165 | * to it. Assertion can be extended via the ->addMethod(), ->flag(), and ->addProperty() 166 | * methods. 167 | * 168 | * @param callable $fn 169 | */ 170 | public function extend($fn) 171 | { 172 | if (is_string($fn) && file_exists($fn)) { 173 | $fn = include $fn; 174 | } 175 | 176 | if (is_callable($fn)) { 177 | return call_user_func($fn, $this); 178 | } 179 | 180 | throw new \InvalidArgumentException('Assertion::extend requires a callable or a file that returns one'); 181 | } 182 | 183 | /** 184 | * Set the actual value used for matching expectations against. 185 | * 186 | * @param $actual 187 | * @return $this 188 | */ 189 | public function setActual($actual) 190 | { 191 | $this->actual = $actual; 192 | 193 | return $this; 194 | } 195 | 196 | /** 197 | * Return the actual value being asserted against. 198 | * 199 | * @return mixed 200 | */ 201 | public function getActual() 202 | { 203 | return $this->actual; 204 | } 205 | 206 | /** 207 | * Assert against the given matcher. 208 | * 209 | * @param $result 210 | * @return $this 211 | */ 212 | public function assert(MatcherInterface $matcher) 213 | { 214 | if ($this->flag('not')) { 215 | $matcher->invert(); 216 | } 217 | 218 | $match = $matcher 219 | ->setAssertion($this) 220 | ->match($this->getActual()); 221 | 222 | $message = $this->flag('message'); 223 | 224 | $this->clearFlags(); 225 | 226 | $this->responder->respond($match, $matcher->getTemplate(), $message); 227 | 228 | return $this; 229 | } 230 | } 231 | --------------------------------------------------------------------------------