diff --git a/.travis.yml b/.travis.yml index 66427c3a..c736202c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,6 @@ -sudo: false +sudo: required +dist: xenial +group: edge language: php php: @@ -6,25 +8,30 @@ php: - 5.5 - 5.6 - 7.0 + - 7.1 - hhvm matrix: fast_finish: true + allow_failures: + - php: hhvm before_script: - composer config -g github-oauth.github.com $GITHUB_COMPOSER_AUTH - composer self-update - composer install --no-interaction - - if [[ $TRAVIS_PHP_VERSION =~ ^hhvm ]]; then echo 'xdebug.enable = On' >> /etc/hhvm/php.ini; fi - #- if [[ $TRAVIS_PHP_VERSION =~ ^7 ]]; then pecl install xdebug; fi + - if [[ $TRAVIS_PHP_VERSION =~ ^hhvm ]]; then echo 'hhvm.php7.all = 1' >> /etc/hhvm/php.ini; fi + - if [[ $TRAVIS_PHP_VERSION =~ ^7 ]]; then pecl install xdebug; fi -script: bin/kahlan --config=kahlan-config.travis.php --clover=clover.xml +script: + - vendor/bin/phpcs + - bin/kahlan --config=kahlan-config.travis.php --clover=clover.xml after_success: - - "if [ $(phpenv version-name) = '5.6' ]; then curl -X POST -d @codeclimate.json -H 'Content-Type:application/json' https://codeclimate.com/test_reports --verbose; fi" - - "if [ $(phpenv version-name) = '5.6' ]; then curl -F 'json_file=@coveralls.json' https://coveralls.io/api/v1/jobs --verbose; fi" - - "if [ $(phpenv version-name) = '5.6' ]; then wget https://scrutinizer-ci.com/ocular.phar; fi" - - "if [ $(phpenv version-name) = '5.6' ]; then php ocular.phar code-coverage:upload --format=php-clover 'clover.xml'; fi" + - "if [ $(phpenv version-name) = '7.0' ]; then curl -X POST -d @codeclimate.json -H 'Content-Type:application/json' https://codeclimate.com/test_reports --verbose; fi" + - "if [ $(phpenv version-name) = '7.0' ]; then curl -F 'json_file=@coveralls.json' https://coveralls.io/api/v1/jobs --verbose; fi" + - "if [ $(phpenv version-name) = '7.0' ]; then wget https://scrutinizer-ci.com/ocular.phar; fi" + - "if [ $(phpenv version-name) = '7.0' ]; then php ocular.phar code-coverage:upload --format=php-clover 'clover.xml'; fi" env: global: diff --git a/CHANGELOG.md b/CHANGELOG.md index fc1779b2..39664b3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ ## Last changes +## 3.0.0 (2016-09-01) + + * **Add:** Add `allow()` DSL. + * **Add:** Add `toBeCalled()` matcher. + * **Add:** `toReceive` now support a chain of messages as definition if correctly stubbed. + * **Add:** Allow to monkey patch a class using a specific instance for all `new` on that class. + * **Change:** Refactor the reporting to provide more meaningful messages on failure. + * **Bugfix:** Fixes an issue with `toReceive()/toBeCalled` and stubs where past called methods were taken into account. + * **BC break:** Cached files are no more compatible, cached files needs to be purged. + * **BC break:** Rename `'params'` option to `'args'` in `Double::instance()`. + * **BC break:** `Stub::on()` is now deprecated use `allow()` instead. + * **BC break:** Rename `Stub::create()` to `Double::instance()`. + * **BC break:** Rename `Stub::classname()` to `Double::classname()`. + * **BC break:** Rename `before()` and `after()` to `beforeAll()` and `afterAll()`. + * **BC break:** Remove `toReceiveNext` matchers in flavor of `->ordered` attribute to be more close to rspec way. + ## 2.5.7 (2016-09-23) * **BC break:** Moving Kahlan to its own organization. diff --git a/README.md b/README.md index 9315b468..56866b18 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,12 @@ Kahlan is a full-featured Unit & BDD test framework a la RSpec/JSpec which uses a `describe-it` syntax and moves testing in PHP one step forward. -Kahlan embraces the [KISS principle](http://en.wikipedia.org/wiki/KISS_principle) and makes Unit & BDD testing fun again! +**Kahlan allows to stub or monkey patch your code directly like in Ruby or JavaScript without any required PECL-extentions.** -**Killer feature:** Kahlan allows to stub or monkey patch your code directly like in Ruby or JavaScript without any required PECL-extentions. - -## Video +## Videos * Warren Seymour presentation at Unified Diff (2015) +* Grafikart presentation in French (2016) ## IRC @@ -26,42 +25,62 @@ Kahlan embraces the [KISS principle](http://en.wikipedia.org/wiki/KISS_principle ## Documentation -See the whole [documentation here](http://kahlan.readthedocs.org/en/latest) (documentation for Kahlan <= 1.3.0 [can still be found here](docs/deprecated)) +See the whole [documentation here](http://kahlan.readthedocs.org/en/latest) ## Requirements * PHP 5.5+ * Composer - * [Xdebug](http://xdebug.org/) (if you want to perform code coverage analysis) + * [phpdbg](http://php.net/manual/en/debugger-about.php) or [Xdebug](http://xdebug.org/) (required for code coverage analysis only) ## Main Features -* Simple API +* RSpec/JSpec syntax * Code Coverage metrics ([xdebug](http://xdebug.org) or [phpdbg](http://phpdbg.com/docs) required) * Handy stubbing system ([mockery](https://github.com/padraic/mockery) or [prophecy](https://github.com/phpspec/prophecy) are no longer needed) * Set stubs on your class methods directly (i.e allows dynamic mocking) * Ability to Monkey Patch your code (i.e. allows replacement of core functions/classes on the fly) -* Check called methods on your class/instances +* Check called methods on your classes/instances * Built-in Reporters (Terminal or HTML reporting through [istanbul](https://gotwarlost.github.io/istanbul/) or [lcov](http://ltp.sourceforge.net/coverage/lcov.php)) * Built-in Exporters (Coveralls, Code Climate, Scrutinizer, Clover) * Extensible, customizable workflow -* Small code base (~10 times smaller than PHPUnit) ## Syntax ```php +toBe(true); + + }); + + it("expects methods to be called", function() { + + expect($user)->toReceive('save')->with(['validates' => false]); + + $user = new User(); + $user->validates(['validates' => false]); + + }); + + it("stubs a function", function() { - expect(true)->toBe(true); + allow('time')->toBeCalled()->andReturn(123); + $user = new User(); + expect($user->save())->toBe(true) + expect($user->created)->toBe(123); }); - it("passes if false !== true", function() { + it("stubs a class", function() { - expect(false)->not->toBe(true); + allow('PDO')->toReceive('prepare', 'fetchAll')->andReturn([['name' => 'bob']]); + $user = new User(); + expect($user->all())->toBe([['name' => 'bob']]); }); diff --git a/composer.json b/composer.json index 97fceead..ca58bc70 100644 --- a/composer.json +++ b/composer.json @@ -11,11 +11,17 @@ "require": { "php": ">=5.4" }, + "require-dev": { + "squizlabs/php_codesniffer": "^2.7" + }, "autoload": { "psr-4": { "Kahlan\\": "src/" }, - "files": ["src/init.php"] + "files": [ + "src/init.php", + "src/functions.php" + ] }, "autoload-dev": { "psr-4": { diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 9e00d75d..00000000 --- a/docs/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# Kahlan -— The Unit/BDD PHP Test Framework for Freedom, Truth, and Justice — - -Kahlan is a full-featured Unit & BDD test framework a la RSpec/JSpec which uses a `describe-it` syntax and moves testing in PHP one step forward. - -Kahlan embraces the [KISS principle](http://en.wikipedia.org/wiki/KISS_principle) and makes Unit & BDD testing fun again! - -**Killer feature:** Kahlan allows to stub or monkey patch your code directly like in Ruby or JavaScript without any required PECL-extentions. - -## Video - -* Warren Seymour presentation at Unified Diff (2015) - -## IRC - -**chat.freenode.net** (server) -**#kahlan** (channel) - -## Download - -[Download Kahlan on Github](https://github.com/kahlan/kahlan) - -## Documentation - -* [Why This One?](why-this-one.md) -* [Getting Started](getting-started.md) -* [Overview](overview.md) -* [Matchers](matchers.md) - * [Classic matchers](matchers.md#classic) - * [Method invocation matchers](matchers.md#method) - * [Argument matchers](matchers.md#argument) - * [Custom matchers](matchers.md#custom) -* [Stubs](stubs.md) - * [Method Stubbing](stubs.md#method-stubbing) - * [Instance Stubbing](stubs.md#instance-stubbing) - * [Class Stubbing](stubs.md#class-stubbing) - * [Custom Stubbing](stubs.md#custom-stubbing) -* [Monkey Patching](monkey-patching.md) - * [Monkey Patch Quit Statements](monkey-patching.md#monkey-patch-quit-statements) -* [Reporters](reporters.md) -* [Pro Tips](pro-tips.md) - including CLI arguments -* [The `kahlan-config.php` file](config-file.md) -* [Integration with popular frameworks](integration.md) -* [FAQ](faq.md) diff --git a/docs/allow.md b/docs/allow.md new file mode 100644 index 00000000..aad30a08 --- /dev/null +++ b/docs/allow.md @@ -0,0 +1,157 @@ +## Stubs & Monkey Patching DSL + +A method stub or simply stub in software development is used to stand in for some other programming functionality. This section explains how to perform such replacement with Kahlan. + +### Method Stubbing + +Use `allow()` to stub an existing method on any class like so: + +```php +it("stubs a method by setting a return value", function() { + $instance = new MyClass(); + allow($instance)->toReceive('myMethod')->andReturn('Good Morning World!'); + + expect($instance->myMethod())->toBe('Good Morning World!'); +}); +``` + +```php +it("stubs a method by setting a return value only when some arguments matches", function() { + $instance = new MyClass(); + allow($instance)->toReceive('myMethod')->with('Hello!')->andReturn('Good Morning World!'); + + expect($instance->myMethod('Hello!'))->toBe('Good Morning World!'); + expect($instance->myMethod())->toBe(null); +}); +``` + +You can specify multiple return values with: + +```php +it("stubs a method with multiple return values", function() { + $instance = new MyClass(); + allow($instance)->toReceive('sequential')->andReturn(1, 3, 2); + + expect($instance->sequential())->toBe(1); + expect($instance->sequential())->toBe(3); + expect($instance->sequential())->toBe(2); +}); +``` + +You can also stub `static` methods using `::`: + +```php +it("stubs a static method", function() { + $instance = new MyClass(); + allow($instance)->toReceive('::myMethod')->andReturn('Good Morning World!'); + + expect($instance::myMethod())->toBe('Good Morning World!'); +}); +``` + +It's also possible to use a closure to replace the whole method logic: + +```php +it("stubs a method using a closure", function() { + allow($foo)->toReceive('myMethod')->andRun(function($param) { return $param; }); + expect($instance->myMethod('Hello World!'))->toBe('Hello World!'); +}); +``` + +Moreover you can stub a chain of methods by using the following syntax. + +```php +it('should patch PDO', function() { + allow('PDO')->toReceive('prepare', 'fetchAll')->andReturn([['name' => 'bob']]); + + $user = new User(); + expect($user->all())->toBe([ + ['name' => 'bob'] + ]); +}); +``` + +Where the `User` class is: + +```php +_db = new PDO('mysql:dbname=testdb;host=localhost', 'root',''); + } + + public function all() + { + $stmt = $this->db->prepare('SELECT * FROM users'); + $stmt->execute(); + return $stmt->fetchAll(); + } +} +``` + +In practice method chaining is considered as code smells because it tends to violate the Law of Demeter. So use it wisely. + +Finally, `where()` can be used to specify some arguments requirement for a chain of methods: + +```php +it('returns the stubbed return value when arguments requirement match', function() { + $query = new MyQuery(); + allow($query) + ->toReceive('find', 'where', 'order', 'limit') + ->where([ + 'find' => ['widgets']], + 'where' => [['name' => 'Bottle Opener']], + 'order' => [['id' => 'desc']], + 'limit' => [10] + ]) + ->andReturn([[id => '123','name' => 'Bottle Opener']]); + + expect($query->find('widgets') + ->where(['name' => 'Bottle Opener']) + ->order(['id' => 'desc']) + ->limit(10))->toBe([[id => '123','name' => 'Bottle Opener']]); +}); +``` + +### Function Stubbing + +Use `allow()` to stub almost all functions like so: + +```php +it("shows some examples of function stubbing", function() { + allow('time')->toBeCalled()->andReturn(123); + allow('time')->toBeCalled()->andReturn(123, 456, 789); + allow('time')->toBeCalled()->andRun(function() { return 123; }); + + allow('rand')->toBeCalled()->with(0, 10)->andReturn(5); +}); +``` + +### Monkey Patching + +Use `allow()` to monkey patch classes like so: + +```php +it("shows some examples of function stubbing", function() { + // Monkey patch `PDO` and stub chained methods under the hood. + allow('PDO')->toReceive('prepare->fetchAll')->andReturn([['name' => 'bob']]); + allow('PDO')->toReceive('prepare->fetchAll')->andRun(function() { + return [['name' => 'bob']]; + }); + + // Monkey patch `PDO` with a specific class. + allow('PDO')->toBe('My\Alternative\PDO'); + + // Monkey patch `DateTime` with a specific instance. + allow('DateTime')->toBe(new DateTime('@123')); + + // Monkey patch `PDO` with a generic stub instance. + allow('PDO')->toBeOK(); +}); +``` diff --git a/docs/assets/phpspec_3.1_code_coverage.png b/docs/assets/phpspec_3.1_code_coverage.png new file mode 100644 index 00000000..7f815502 Binary files /dev/null and b/docs/assets/phpspec_3.1_code_coverage.png differ diff --git a/docs/assets/phpunit_5.7_code_coverage.png b/docs/assets/phpunit_5.7_code_coverage.png new file mode 100644 index 00000000..6abe7ec9 Binary files /dev/null and b/docs/assets/phpunit_5.7_code_coverage.png differ diff --git a/docs/cli-options.md b/docs/cli-options.md new file mode 100644 index 00000000..097010f5 --- /dev/null +++ b/docs/cli-options.md @@ -0,0 +1,53 @@ +## CLI Options + +Below are all of Kahlan's option obtained through the `kahlan --help` command line. + +``` +Configuration Options: + + --config= The PHP configuration file to use (default: `'kahlan-config.php'`). + --src= Paths of source directories (default: `['src']`). + --spec= Paths of specification directories (default: `['spec']`). + --pattern= A shell wildcard pattern (default: `'*Spec.php'`). + +Reporter Options: + + --reporter=[:] The name of the text reporter to use, the built-in text reporters + are `'dot'`, `'bar'`, `'json'`, `'tap'` & `'verbose'` (default: `'dot'`). + You can optionally redirect the reporter output to a file by using the + colon syntax (multiple --reporter options are also supported). + +Code Coverage Options: + + --coverage= Generate code coverage report. The value specify the level of + detail for the code coverage report (0-4). If a namespace, class, or + method definition is provided, it will generate a detailed code + coverage of this specific scope (default `''`). + --clover= Export code coverage report into a Clover XML format. + --istanbul= Export code coverage report into an istanbul compatible JSON format. + --lcov= Export code coverage report into a lcov compatible text format. + +Test Execution Options: + + --ff= Fast fail option. `0` mean unlimited (default: `0`). + --no-colors= To turn off colors. (default: `false`). + --no-header= To turn off header. (default: `false`). + --include= Paths to include for patching. (default: `['*']`). + --exclude= Paths to exclude from patching. (default: `[]`). + --persistent= Cache patched files (default: `true`). + --cc= Clear cache before spec run. (default: `false`). + --autoclear Classes to autoclear after each spec (default: [ + `'Kahlan\Plugin\Monkey'`, + `'Kahlan\Plugin\Call'`, + `'Kahlan\Plugin\Stub'`, + `'Kahlan\Plugin\Quit'` + ]) + +Miscellaneous Options: + + --help Prints this usage information. + --version Prints Kahlan version + +Note: The `[]` notation in default values mean that the related option can accepts an array of values. +To add additional values, just repeat the same option many times in the command line. +``` diff --git a/docs/config-file.md b/docs/config-file.md index 2ff4b45a..2c9cd969 100644 --- a/docs/config-file.md +++ b/docs/config-file.md @@ -1,8 +1,8 @@ ## The `kahlan-config.php` file -If you want to set some default options, change the execution workflow or load some custom plugins at a boostrap level, you will need to setup you own config file. +If you want to set some default options, change the execution workflow or load some custom plugins at a bootstrap level, you will need to setup you own config file. -Kahlan attempt to load the `kahlan-config.php` file from the current directory as the default config file. However you can define your own path using the `--config=myconfigfile.php` option in the command line. Custom `--config` can be useful if you want to use some specific configuration for Travis or something else. +Kahlan attempts to load the `kahlan-config.php` file from the current directory as the default config file. However you can define your own path using the `--config=myconfigfile.php` option in the command line. Custom `--config` can be useful if you want to use some specific configuration for Travis or something else. Example of a config file: @@ -13,11 +13,11 @@ use Kahlan\Reporter\Coverage\Exporter\Coveralls; // It overrides some default option values. // Note that the values passed in command line will overwrite the ones below. -$args = $this->args(); -$args->argument('ff', 'default', 1); -$args->argument('coverage', 'default', 3); -$args->argument('coverage-scrutinizer', 'default', 'scrutinizer.xml'); -$args->argument('coverage-coveralls', 'default', 'coveralls.json'); +$commandLine = $this->commandLine(); +$commandLine->argument('ff', 'default', 1); +$commandLine->argument('coverage', 'default', 3); +$commandLine->argument('coverage-scrutinizer', 'default', 'scrutinizer.xml'); +$commandLine->argument('coverage-coveralls', 'default', 'coveralls.json'); // The logic to include into the workflow. Filter::register('kahlan.coveralls', function($chain) { @@ -26,14 +26,14 @@ Filter::register('kahlan.coveralls', function($chain) { $coverage = $this->reporters()->get('coverage'); // Abort if no coverage is available. - if (!$coverage || !$this->args()->exists('coverage-coveralls')) { + if (!$coverage || !$this->commandLine()->exists('coverage-coveralls')) { return $chain->next(); } // Use the `Coveralls` class to write the JSON coverage into a file Coveralls::write([ 'coverage' => $coverage, - 'file' => $this->args()->get('coverage-coveralls'), + 'file' => $this->commandLine()->get('coverage-coveralls'), 'service_name' => 'travis-ci', 'service_job_id' => getenv('TRAVIS_JOB_ID') ?: null ]); @@ -47,7 +47,7 @@ Filter::apply($this, 'reporting', 'kahlan.coveralls'); ?> ``` -Above `'kahlan.coveralls'` is just a custom name and could be whatever as long as `Filter::register()` and `Filter::apply()` are consistent on the namings. +Above `'kahlan.coveralls'` is just a custom name and could be whatever as long as `Filter::register()` and `Filter::apply()` are named consistently. `$this` refer to the Kahlan instance so `$this->reporters()->get('coverage')` will give you the instance of the coverage reporter. This coverage reporter will contain all raw data which is passed to the `Coveralls` exporter to be formatter. @@ -68,18 +68,18 @@ The filterable entry points are the following: * `'quit`' # For some additional post processing before quitting -[You can see more details about how the workflow works here](https://github.com/kahlan/kahlan/blob/master/src/cli/Kahlan.php) (start reading with the `run()` method). +[You can see more details about how the workflow works here](https://github.com/kahlan/kahlan/blob/master/src/Cli/Kahlan.php) (start reading with the `run()` method). ### Optimizations Kahlan acts like a wrapper. It intercepts loaded classes Just It Time (i.e. during the autoloading step) and rewrites the source code on the fly to make it easily testable with PHP. That's why Monkey Patching or redefining a class's method can be done inside the testing environment without any PECL extensions like runkit, aop, etc. -Notice that this approach will make your code run a bit slower than your original code. However you can optimize Kahlan's interceptor to only patch the namespaces you want: +Notice that this approach will make your code run a bit slower. However you can optimize Kahlan's interceptor to only patch the namespaces you want. -For example, the following configuration will only limit the patching to a bunch of namespaces/classes: +For example, the following configuration will only limit the patching to a set of namespaces/classes: ```php -$this->args()->set('include', [ +$this->commandLine()->set('include', [ 'myapp', 'lithium', 'li3_zendserver\data\Job', @@ -87,16 +87,18 @@ $this->args()->set('include', [ ]); ``` -Conversely you can also exclude some external dependencies to speed up performances if you don't intend to Monkey Patch/Stub some namespaces/classes: +Conversely you can exclude some external dependencies to improve performance if you don't intend to Monkey Patch/Stub some namespaces/classes: + ```php -$this->args()->set('exclude', [ +$this->commandLine()->set('exclude', [ 'Symfony', 'Doctrine' ]); ``` -Finally you can also disable all the patching everywhere if you prefer to deal with DI only and are not interested by Kahlan's features: +Finally, you can disable all the patching if you prefer to deal with DI only and are not interested by Kahlan's features: + ```php -$this->args()->set('include', []); +$this->commandLine()->set('include', []); ``` -**Note:** You will still able to stub instances and classes created with `Stub::create()`/`Stub::classname()` anyway. +**Note:** You will still able to stub instances and classes created with `Double::instance()`/`Double::classname()`. diff --git a/docs/deprecated/README.md b/docs/deprecated/README.md deleted file mode 100644 index 9e00d75d..00000000 --- a/docs/deprecated/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# Kahlan -— The Unit/BDD PHP Test Framework for Freedom, Truth, and Justice — - -Kahlan is a full-featured Unit & BDD test framework a la RSpec/JSpec which uses a `describe-it` syntax and moves testing in PHP one step forward. - -Kahlan embraces the [KISS principle](http://en.wikipedia.org/wiki/KISS_principle) and makes Unit & BDD testing fun again! - -**Killer feature:** Kahlan allows to stub or monkey patch your code directly like in Ruby or JavaScript without any required PECL-extentions. - -## Video - -* Warren Seymour presentation at Unified Diff (2015) - -## IRC - -**chat.freenode.net** (server) -**#kahlan** (channel) - -## Download - -[Download Kahlan on Github](https://github.com/kahlan/kahlan) - -## Documentation - -* [Why This One?](why-this-one.md) -* [Getting Started](getting-started.md) -* [Overview](overview.md) -* [Matchers](matchers.md) - * [Classic matchers](matchers.md#classic) - * [Method invocation matchers](matchers.md#method) - * [Argument matchers](matchers.md#argument) - * [Custom matchers](matchers.md#custom) -* [Stubs](stubs.md) - * [Method Stubbing](stubs.md#method-stubbing) - * [Instance Stubbing](stubs.md#instance-stubbing) - * [Class Stubbing](stubs.md#class-stubbing) - * [Custom Stubbing](stubs.md#custom-stubbing) -* [Monkey Patching](monkey-patching.md) - * [Monkey Patch Quit Statements](monkey-patching.md#monkey-patch-quit-statements) -* [Reporters](reporters.md) -* [Pro Tips](pro-tips.md) - including CLI arguments -* [The `kahlan-config.php` file](config-file.md) -* [Integration with popular frameworks](integration.md) -* [FAQ](faq.md) diff --git a/docs/deprecated/assets/code_coverage.png b/docs/deprecated/assets/code_coverage.png deleted file mode 100644 index 385a2002..00000000 Binary files a/docs/deprecated/assets/code_coverage.png and /dev/null differ diff --git a/docs/deprecated/assets/custom_reporter.png b/docs/deprecated/assets/custom_reporter.png deleted file mode 100644 index 045d3983..00000000 Binary files a/docs/deprecated/assets/custom_reporter.png and /dev/null differ diff --git a/docs/deprecated/assets/dot_reporter.png b/docs/deprecated/assets/dot_reporter.png deleted file mode 100644 index c516e253..00000000 Binary files a/docs/deprecated/assets/dot_reporter.png and /dev/null differ diff --git a/docs/deprecated/assets/phpunit_4.4_code_coverage.png b/docs/deprecated/assets/phpunit_4.4_code_coverage.png deleted file mode 100644 index ef18825d..00000000 Binary files a/docs/deprecated/assets/phpunit_4.4_code_coverage.png and /dev/null differ diff --git a/docs/deprecated/assets/verbose_reporter.png b/docs/deprecated/assets/verbose_reporter.png deleted file mode 100644 index 567be6b7..00000000 Binary files a/docs/deprecated/assets/verbose_reporter.png and /dev/null differ diff --git a/docs/deprecated/assets/warning.png b/docs/deprecated/assets/warning.png deleted file mode 100644 index b8b6d017..00000000 Binary files a/docs/deprecated/assets/warning.png and /dev/null differ diff --git a/docs/deprecated/config-file.md b/docs/deprecated/config-file.md deleted file mode 100644 index d66f6beb..00000000 --- a/docs/deprecated/config-file.md +++ /dev/null @@ -1,102 +0,0 @@ -## The `kahlan-config.php` file - -If you want to set some default options, change the execution workflow or load some custom plugins at a boostrap level, you will need to setup you own config file. - -Kahlan attempt to load the `kahlan-config.php` file from the current directory as the default config file. However you can define your own path using the `--config=myconfigfile.php` option in the command line. Custom `--config` can be useful if you want to use some specific configuration for Travis or something else. - -Example of a config file: - -```php -args(); -$args->argument('ff', 'default', 1); -$args->argument('coverage', 'default', 3); -$args->argument('coverage-scrutinizer', 'default', 'scrutinizer.xml'); -$args->argument('coverage-coveralls', 'default', 'coveralls.json'); - -// The logic to include into the workflow. -Filter::register('kahlan.coveralls', function($chain) { - - // Get the reporter called `'coverage'` from the list of reporters - $coverage = $this->reporters()->get('coverage'); - - // Abort if no coverage is available. - if (!$coverage || !$this->args()->exists('coverage-coveralls')) { - return $chain->next(); - } - - // Use the `Coveralls` class to write the JSON coverage into a file - Coveralls::write([ - 'coverage' => $coverage, - 'file' => $this->args()->get('coverage-coveralls'), - 'service_name' => 'travis-ci', - 'service_job_id' => getenv('TRAVIS_JOB_ID') ?: null - ]); - - // Continue the chain - return $chain->next(); -}); - -// Apply the logic to the `'reporting'` entry point. -Filter::apply($this, 'reporting', 'kahlan.coveralls'); -?> -``` - -Above `'kahlan.coveralls'` is just a custom name and could be whatever as long as `Filter::register()` and `Filter::apply()` are consistent on the namings. - -`$this` refer to the Kahlan instance so `$this->reporters()->get('coverage')` will give you the instance of the coverage reporter. This coverage reporter will contain all raw data which is passed to the `Coveralls` exporter to be formatter. - -The filterable entry points are the following: - -* `'workflow`' # The one to rule them all - * `'interceptor`' # Operations on the autoloader - * `'namespaces`' # Adds some namespaces not managed by composer (like `spec`) - * `'patchers`' # Adds patchers - * `'loadSpecs`' # Loads specs - * `'reporters`' # Adds reporters - * `'console'` # Creates the console reporter - * `'coverage'` # Creates the coverage reporter - * `'matchers`' # Useful for registering some further matchers - * `'run`' # Runs the test suite - * `'reporting`' # Runs some additional reporting tasks - * `'stop`' # Trigger the stop event to reporters - * `'quit`' # For some additional post processing before quitting - - -[You can see more details about how the workflow works here](https://github.com/kahlan/kahlan/blob/master/src/cli/Kahlan.php) (start reading with the `run()` method). - -### Optimizations - -Kahlan acts like a wrapper. It intercepts loaded classes Just It Time (i.e. during the autoloading step) and rewrites the source code on the fly to make it easily testable with PHP. That's why Monkey Patching or redefining a class's method can be done inside the testing environment without any PECL extensions like runkit, aop, etc. - -Notice that this approach will make your code run a bit slower than your original code. However you can optimize Kahlan's interceptor to only patch the namespaces you want: - -For example, the following configuration will only limit the patching to a bunch of namespaces/classes: - -```php -$this->args()->set('include', [ - 'myapp', - 'lithium', - 'li3_zendserver\data\Job', - 'AuthorizeNetCIM' -]); -``` - -Conversely you can also exclude some external dependencies to speed up performances if you don't intend to Monkey Patch/Stub some namespaces/classes: -```php -$this->args()->set('exclude', [ - 'Symfony', - 'Doctrine' -]); -``` - -Finally you can also disable all the patching everywhere if you prefer to deal with DI only and are not interested by Kahlan's features: -```php -$this->args()->set('include', []); -``` -**Note:** You will still able to stub instances and classes created with `Stub::create()`/`Stub::classname()` anyway. diff --git a/docs/deprecated/faq.md b/docs/deprecated/faq.md deleted file mode 100644 index 93ab223a..00000000 --- a/docs/deprecated/faq.md +++ /dev/null @@ -1,16 +0,0 @@ -## FAQ - -**Q:** After an update my code doesn't work?
-**A:** Please, make sure that you cleared-up the **/tmp/kahlan** folder, and the updated version is a BC-compatible version. - -**Q:** How can I use my **favorite framework** with Kahlan?
-**A:** You can look up in [Framework integration](integration.md). - -**Q:** What if there is no info about my framework in integration?
-**A:** All frameworks which use a PSR-0 compatible loader can be integrated using the generic way. Otherwise you'll need to make a PR. - -**Q:** Where can i view current roadmap?
-**A:** You can view it [here](https://github.com/kahlan/kahlan/wiki/Roadmap) - -**Q:** I have a question and can't solve it by myself!
-**A:** If this question is related to **Kahlan**, please open issue. Or ask your question in live on [IRC](index.md). \ No newline at end of file diff --git a/docs/deprecated/getting-started.md b/docs/deprecated/getting-started.md deleted file mode 100644 index 80f30ddc..00000000 --- a/docs/deprecated/getting-started.md +++ /dev/null @@ -1,42 +0,0 @@ -## Getting started - -**Requirement: Just before continuing, make sure you have installed [Composer](https://getcomposer.org/).** - -To make a long story short let's take [the following repository](https://github.com/crysalead/text) as an example. - -It's a simple string class in PHP which give you a better understanding on how to structure a project to be easily testable with Kahlan. - -Here is the tree structure of this project: - -``` -├── bin -├── .gitignore -├── .scrutinizer.yml # Optional, it's for using https://scrutinizer-ci.com -├── .travis.yml # Optional, it's for using https://travis-ci.org -├── composer.json # Need at least the Kahlan dependency -├── LICENSE.txt -├── README.md -├── spec # The directory which contain specs -│   └── text -│   └── TextSpec.php # Name of spec should match pattern *Spec.php -├── src # The directory which contain sources code -│   └── Text.php -``` - -To start playing with it you'll need to: - -```bash -git clone git://github.com/crysalead/text.git -cd text -composer install -``` - -And then run the tests (referred to as 'specs') with: - -```bash -./bin/kahlan --coverage=4 -``` - -**Note:** the `--coverage=4` option is optional. - -PS: If your library is not compatible with composer, check the [integration section](integration.md). diff --git a/docs/deprecated/index.md b/docs/deprecated/index.md deleted file mode 120000 index 42061c01..00000000 --- a/docs/deprecated/index.md +++ /dev/null @@ -1 +0,0 @@ -README.md \ No newline at end of file diff --git a/docs/deprecated/integration.md b/docs/deprecated/integration.md deleted file mode 100644 index 3a35c676..00000000 --- a/docs/deprecated/integration.md +++ /dev/null @@ -1,126 +0,0 @@ -## Integration with popular frameworks - -Kahlan fits perfectly with the composer autoloader. However a couple of popular frameworks still use their own autoloader and you will need to make all your namespaces to be autoloaded correctly in the test environment to make it works. - -Hopefully It's easy and simple. Indeed almost all popular frameworks autoloaders are **PSR-0**/**PSR-4** compatible, so the only thing you will need to do is to correctly configure your **kahlan-config.php** config file to manually add to the composer autoloader all namespaces which are "ouside the composer scope". - -### Working with a PSR-0 compatible architecture. - -Let's take a situation where you have the following directories: `app/models/` and `app/controllers/` and each one are respectively attached to the `Api\Models` and `Api\Controllers` namespaces. To make them autoloaded with Kahlan you will need to manually add this **PSR-4** based namespaces in your **kahlan-config.php** config file: - -```php -use kahlan\filter\Filter; - -Filter::register('mycustom.namespaces', function($chain) { - - $this->autoloader()->addPsr4('Api\\Models\\', __DIR__ . '/app/models/'); - $this->autoloader()->addPsr4('Api\\Controllers\\', __DIR__ . '/app/controllers/'); - return $chain->next(); - -}); - -Filter::apply($this, 'namespaces', 'mycustom.namespaces'); -``` - -### Using the `Layer` patcher (Phalcon). - -When a class extends a built-in class (i.e. a non PHP class) it's not possible to stub core methods. Long story short, let's take the following example as an illustration: - -We have a model: - -```php -namespace Api\Models; - -class MyModel extends \Phalcon\Mvc\Model -{ - public $title; - public function getTitle() - { - return $this->title; - } -} -``` - -We have a controller: - -```php -namespace Api\Contollers; - -use Exception; -use Api\Models\MyModel; - -class MyController extends \Phalcon\Mvc\Controller -{ - public function indexAction() - { - $article = MyModel::findFirst(); - $this->view->setVar('title', $article->getTitle()); - } -} -``` - -And we want to check that `indexAction()` correctly sets the `'title'` view var. This check can be tranlsated into the following spec: - -```php -namespace Api\Spec\Contollers; - -use Api\Models\MyModel; -use Api\Contollers\MyController; -use kahlan\plugin\Stub; - -describe("MyController", function() { - - describe("->indexAction()", function() { - - it("correctly populates the view var", function() { - - $article = new MyModel(); - $article->title = 'Hello World'; - - Stub::on('Api\Models\MyModel')->method('::findFirst')->andReturn($article); - - $controller = new MyController(); - $controller->indexAction(); - - expect($controller->view->getVar('title'))->toBe('Hello World'); - - }); - - }); - -}); -``` - -Unfortunalty it doesn't work out of the box. Indeed `MyModel` extends `Phalcon\Mvc\Model` which is a core class (i.e a class compiled from C sources). Since the method `MyModel::findFirst()` doesn't exists in PHP land, it can't be stubbed. - -The workaround here is to configure the Kahlan's `Layer` patcher in [the `kahlan-config.php` file](config-file.md). The `Layer` patcher can dynamically replace all `extends` done on core class to an intermediate layer class in PHP. - -For a Phalcon project, the `Layer` patcher can be configured like the following in the Kahlan config file: - -```php -use kahlan\filter\Filter; -use kahlan\jit\Interceptor; -use kahlan\plugin\Layer; - -Filter::register('api.patchers', function($chain) { - if (!$interceptor = Interceptor::instance()) { - return; - } - $patchers = $interceptor->patchers(); - $patchers->add('layer', new Layer([ - 'override' => [ - 'Phalcon\Mvc\Model' // this will dynamically apply a layer on top of the `Phalcon\Mvc\Model` to make it stubbable. - ] - ])); - - return $chain->next(); -}); - -Filter::apply($this, 'patchers', 'api.patchers'); -``` - -**Note:** You will probably need to remove all cached files in `/tmp/kahlan` (or in `sys_get_temp_dir() . '/kahlan'` if you are not on linux) to make it works. - -### Working with a autoloader not compatible with PSR-0. - -In this case your must implement a `PSR-0` **Composer** compatible autoloader. To have a right direction you could see at [sources](https://github.com/composer/composer/blob/master/src/Composer/Autoload/ClassLoader.php), and take care of `findFile`, `loadClass` and `add` functions. diff --git a/docs/deprecated/matchers.md b/docs/deprecated/matchers.md deleted file mode 100644 index 0687798b..00000000 --- a/docs/deprecated/matchers.md +++ /dev/null @@ -1,362 +0,0 @@ -## Matchers - -* [Overview](#overview) -* [Classic matchers](#classic) -* [Method invocation matchers](#method) -* [Argument matchers](#argument) -* [Custom matchers](#custom) - -### Overview - -**Note:** Expectations can only be done inside `it` blocks. - -Kahlan have a lot of matchers, that can help you up in your testing journey. All matchers can be chained up. It shown in a code below. - -```php -it("can chain up a lots of matchers", function() { - expect([1, 2, 3])->toBeA('array')->toBe([1, 2, 3])->toContain(1); -}); -``` - -### Classic matchers - -**toBe($expected)** - -```php -it("passes if $actual === $expected", function() { - - expect(true)->toBe(true); - -}); -``` - -**toEqual($expected)** - -```php -it("passes if $actual == $expected", function() { - - expect(true)->toEqual(1); - -}); -``` - -**toBeTruthy()** - -```php -it("passes if $actual is truthy", function() { - - expect(1)->toBeTruthy(); - -}); -``` - -**toBeFalsy() / toBeEmpty()** - -```php -it("passes if $actual is falsy", function() { - - expect(0)->toBeFalsy(); - expect(0)->toBeEmpty(); - -}); -``` - -**toBeNull()** - -```php -it("passes if $actual is null", function() { - - expect(null)->toBeNull(); - -}); -``` - -**toBeA($expected)** - -```php -it("passes if $actual is of a specific type", function() { - - expect('Hello World!')->toBeA('string'); - expect(false)->toBeA('boolean'); - expect(new stdClass())->toBeA('object'); - -}); -``` - -*Supported types:* -* string -* integer -* float (floating point numbers - also called double) -* boolean -* array -* object -* null -* resource - -**toBeAnInstanceOf($expected)** - -```php -it("passes if $actual is an instance of stdObject", function() { - - expect(new stdClass())->toBeAnInstanceOf('stdObject'); - -}); -``` - -**toHaveLength($expected)** - -```php -it("passes if $actual has the correct length", function() { - - expect('Hello World!')->toHaveLength(12); - expect(['a', 'b', 'c'])->toHaveLength(3); - -}); -``` - -**toContain($expected)** - -```php -it("passes if $actual contain $expected", function() { - - expect([1, 2, 3])->toContain(3); - -}); -``` - -**toContainKey($expected)** - -```php -it("passes if $actual contain $expected key(s)", function() { - - expect([ 'a' =>1, 'b' => 2, 'c' => 3])->toContainKey(a); - expect([ 'a' =>1, 'b' => 2, 'c' => 3])->toContainKey(a, b); - expect([ 'a' =>1, 'b' => 2, 'c' => 3])->toContainKey([a, b]); - -}); -``` - -**toBeCloseTo($expected, $precision)** - -```php -it("passes if abs($actual - $expected)*2 < 0.01", function() { - - expect(1.23)->toBeCloseTo(1.225, 2); - expect(1.23)->not->toBeCloseTo(1.2249999, 2); - -}); -``` - -**toBeGreaterThan($expected)** - -```php -it("passes if $actual > $expected", function() { - - expect(1)->toBeGreaterThan(0.999); - -}); -``` - -**toBeLessThan($expected)** - -```php -it("passes if $actual < $expected", function() { - - expect(0.999)->toBeLessThan(1); - -}); -``` - -**toThrow($expected)** - -```php -it("passes if $closure throws the $expected exception", function() { - - $closure = function() { - // place the code that you expect to throw an exception in a closure, like so - throw new RuntimeException('exception message'); - }; - expect($closure)->toThrow(); - expect($closure)->toThrow(new RuntimeException()); - expect($closure)->toThrow(new RuntimeException('exception message')); - -}); -``` - -**toMatch($expected)** - -```php -it("passes if $actual matches the $expected regexp", function() { - - expect('Hello World!')->toMatch('/^H(.*?)!$/'); - -}); -``` - -```php -it("passes if $actual matches the $expected closure logic", function() { - - expect('Hello World!')->toMatch(function($actual) { - return $actual === 'Hello World!'; - }); - -}); -``` - -**toEcho($expected)** - -```php -it("passes if $closure echoes the expected output", function() { - - $closure = function() { - echo "Hello World!"; - }; - expect($closure)->toEcho("Hello World!"); - -}); -``` - -**toMatchEcho($expected)** - -```php -it("passes if $closure echoes the expected regex output", function() { - - $closure = function() { - echo "Hello World!"; - }; - expect($closure)->toMatchEcho('/^H(.*?)!$/'); - -}); -``` - -```php -it("passes if $actual matches the $expected closure logic", function() { - - expect('Hello World!')->toMatchEcho(function($actual) { - return $actual === 'Hello World!'; - }); - -}); -``` - -### Method invocation matchers - -**Note:** You should **always remember** to use `toReceive`, `toReceiveNext` function **before** you call a method. - -**toReceive($expected)** - -```php -it("expects $foo to receive message() with the correct param", function() { - - $foo = new Foo(); - expect($foo)->toReceive('message')->with('My Message'); - $foo->message('My Message'); - -}); -``` - -```php -it("expects $foo to receive ::message() with the correct param", function() { - - $foo = new Foo(); - expect($foo)->toReceive('::message')->with('My Message'); - $foo::message('My Message'); - -}); -``` - -**toReceiveNext($expected)** - -```php -it("expects $foo to receive message() followed by foo()", function() { - - $foo = new Foo(); - expect($foo)->toReceive('message'); - expect($foo)->toReceiveNext('foo'); - $foo->message(); - $foo->foo(); - -}); -``` -```php -it("expects $foo to receive message() but not followed by foo()", function() { - - $foo = new Foo(); - expect($foo)->toReceive('message'); - expect($foo)->not->toReceiveNext('foo'); - $foo->foo(); - $foo->message(); - -}); -``` - -### Argument Matchers - -To enable **Argument Matching** add the following `use` statement in the top of your tests: - -```php -use kahlan\Arg; -``` - -With the `Arg` class you can use any existing matchers to test arguments. - -```php -it("expects params to match the argument matchers", function() { - - $foo = new Foo(); - expect($foo)->toReceive('message')->with(Arg::toBeA('boolean')); - expect($foo)->toReceiveNext('message')->with(Arg::toBeA('string')); - $foo->message(true); - $foo->message('Hello World!'); - -}); -``` -```php -it("expects params match the toContain argument matcher", function() { - - $foo = new Foo(); - expect($foo)->toReceive('message')->with(Arg::toContain('My Message')); - $foo->message(['My Message', 'My Other Message']); - -}); -``` - -### Custom matchers - -With Kahlan you can easily create you own matchers. Long story short, a matcher is a simple class with a least a two static methods: `match()` and `description()`. - -Example of a `toBeZero()` matcher: - -```php -namespace my\namespace; - -class ToBeZero -{ - - public static function match($actual, $expected = null) - { - return $actual === 0; - } - - public static function description() - { - return "be equal to 0."; - } -} -``` - -Once created you only need to [register it](config-file.md) using the following syntax: - -```php -kahlan\Matcher::register('toBeZero', 'my\namespace\ToBeZero'); -``` - -**Note:** custom matcher should be reserved to frequently used matching. For other cases, just use the `toMatch` matcher using the matcher closure as parameter. - -It's also possible to register a matcher to work only for a specific class name to keep the API consistent. - -Example: - -```php -kahlan\Matcher::register('toContain', 'my\namespace\ToContain', ' SplObjectStorage'); -``` diff --git a/docs/deprecated/monkey-patching.md b/docs/deprecated/monkey-patching.md deleted file mode 100644 index b9ccf4a9..00000000 --- a/docs/deprecated/monkey-patching.md +++ /dev/null @@ -1,101 +0,0 @@ -## Monkey Patching - -* [Monkey Patch Quit Statements](#monkey-patch-quit-statements) - -To enable **Monkey Patching**, add the following `use` statement in the top of your specs: - -```php -use kahlan\plugin\Monkey; -``` - -Monkey Patching allows replacement of core functions and classes that can't be stubbed, for example [time()](http://php.net/manual/en/function.time.php), [DateTime](http://php.net/manual/en/class.datetime.php) or [MongoId](http://php.net/manual/en/class.mongoid.php) for example. - -With Kahlan, you can patch anything you want using `Monkey::patch()`! - -For example, I have the following class which needs to be patched: - -```php -namespace kahlan\monkey; - -use DateTime; - -class Foo -{ - public function time() - { - return time(); - } - - public function datetime($datetime = 'now') - { - return new DateTime($datetime); - } -} -``` - -You can patch the `time()` function on the fly like in the following spec: - -```php -namespace spec; - -use kahlan\monkey\Foo; - -function mytime() { - return 245026800; -} - -describe("Monkey patching", function() { - - it("patches a core function", function() { - - $foo = new Foo(); - Monkey::patch('time', 'spec\mytime'); - expect($foo->time())->toBe(245026800); - - }); - -}); -``` - -Unbelievable, right? Moreover, you can also replace the `time()` function by a simple closure: - -```php -it("patches a core function with a closure", function() { - - $foo = new Foo(); - Monkey::patch('time', function(){return 123;}); - expect($foo->time())->toBe(123); - -}); -``` - -Using the same syntax, you can also patch any core classes by just monkey patching a fully-namespaced class name to another fully-namespaced class name. - -You can find [another example of how to use Monkey Patching here](https://github.com/warrenseymour/kahlan-lightning-talk). - -### Monkey Patch Quit Statements - -When a unit test exercises code that contains an `exit()` or a `die()` statement, the execution of the whole test suite is aborted. With Kahlan, you can make all quit statements (i.e. like `exit()` or `die()`) throw a `QuitException` instead of quitting the test suite for real. - -To enable **Monkey Patching on Quit Statements** add the following `use` statements in the top of your tests: - -```php -use kahlan\QuitException; -use kahlan\plugin\Quit; -``` - -And then use `Quit::disable()` like in the following: -```php -it("throws an exception when an exit statement occurs if not allowed", function() { - Quit::disable(); - - $closure = function() { - $foo = new Foo(); - $foo->runCodeWithSomeQuitStatementInside(-1); - }; - - expect($closure)->toThrow(new QuitException('Exit statement occurred', -1)); -}); -``` - -**Note:** This only work **for classes loaded by Composer**. If you try to create a stub with a `exit()` statement inside a closure it won't get intercepted by patchers and the application will quit for real. Indeed, **code in `*Spec.php` files are not intercepted and patched**. diff --git a/docs/deprecated/pro-tips.md b/docs/deprecated/pro-tips.md deleted file mode 100644 index a71fb4a9..00000000 --- a/docs/deprecated/pro-tips.md +++ /dev/null @@ -1,139 +0,0 @@ -## Pro Tips - -### Use the `--ff` option (fast fail) - -`--ff` is the fast fail option. If used, the test suite will be stopped as soon as a failing test occurs. You can also specify a number of "allowed" fails before stopping the process. For example: - -``` -./bin/kahlan --ff=3 -``` - -will stop the process as soon as 3 specs `it` failed. - -### Use `--coverage` option - -Kahlan has some built-in code coverage exporter (e.g. Coveralls & Scrutinizer exporters) but it can also be used to generates some detailed code coverage report directly inside the console. - -**`--coverage=`** will generates some code coverage summary depending on the passed integer. - -* 0: no coverage -* 1: code coverage summary of the whole project -* 2: code coverage summary detailed by namespaces -* 3: code coverage summary detailed by classes -* 4: code coverage summary detailed by methods - -However sometimes it's interesting to see in details all covered/uncovered lines. To achieve this, you can pass a string to the `--coverage` option. - -**`--coverage=`** will generates some detailed code coverage according to the specified namespace, class or method definition. - -Example: - -```php -./bin/kahlan --coverage="kahlan\reporter\coverage\driver\Xdebug::stop()" -``` - -![warning](assets/warning.png) don't forget to correctly set the `--src` option if your source directory is not `src/`. - -Will give you the detailed code coverage of the `Xdebug::stop()` method. - -![code_coverage_example](assets/code_coverage.png) - -**Note:** -All available namespaces, classes or methods definitions can be extracted from a simple `--coverage=4` code coverage summary. - -**Note:** -You can use PHPDBG code coverage capabilities to generate code coverage report through the following command line: - -```bash -phpdbg -qrr ./bin/kahlan --coverage -``` - -Warning: while 80% faster than Xdebug it's currently not as accurate as Xdebug and more prone to Fatal Errors. - -**`--istanbul=`** - -You can also create an HTML Code Coverage report using istanbul like so: - -```bash -npm install -g istanbul -./bin/kahlan --istanbul="coverage.json" -istanbul report -``` - -You'll find the HTML Code Coverage report in `coverage/lcov-report/index.html`. - -**`--lcov=`** - -You can also use `genhtml` (from the [lcov](http://ltp.sourceforge.net/coverage/lcov.php) package): - -```bash -sudo apt-get install lcov -mkdir lcov -./bin/kahlan --lcov="lcov/coverage.info" -cd lcov -genhtml coverage.info -``` - -### Injecting variables in root scope - -To inject some variables to all scopes (e.g. database connection, helpers, etc.) and make it available in all you specs, one solution is to configure you `kahlan-config.php` file like the following: - -```php -Filter::register('registering.globals', function($chain) {= - $root = $this->suite(); // The top most suite. - $root->global = 'MyVariable'; - return $chain->next(); -}); - -Filter::apply($this, 'run', 'registering.globals'); -``` - -Then you can get it in any scopes like in the following: - -```php -describe("My Spec", function() { - it("echoes the global", function() { - echo $this->global; - }); -}); -``` - -### Use the focused mode - -When writing your tests sometimes you want to **only execute** the test(s) you are working on. For this, you can prefix your spec by doubling the first letter like in the following example: - -```php -describe("test focused mode", function() { - - it("will be ignored", function() { - }); - - it("will be ignored", function() { - }); - - fit("will be runned", function() { - }); -}); -``` - -If you want to run a subset instead of a single test you can use `fdescribe` or `fcontext` instead. - -**Tip:** combined with `--coverage=` this is a powerful combo to see exactly what part of the code is covered for a subset of specs only. - -### Comment out a spec - -To comment out a spec, you can use the `x` prefix i.e. `xdescribe`, `xcontext` or `xit`. - -### Skip a spec - -To skip a spec you should use a `skipIf()` function inside of it. This function takes a bolean, that mean you can provide a conditions to skip this spec up. In example: - -```php -it("should not run on weekends", function() { - - skipIf(date("w") == 0 || date("w") == 6); - - expect(true)->toBe(true); - -}); -``` diff --git a/docs/deprecated/reporters.md b/docs/deprecated/reporters.md deleted file mode 100644 index f816bdcb..00000000 --- a/docs/deprecated/reporters.md +++ /dev/null @@ -1,116 +0,0 @@ -## Reporters - -Kahlan provides a flexible reporter system which can be extended easily. - -There are three build-in reporters and the default is the dotted one: - -```php -./bin/kahlan --reporter=dot # Default value -``` - -To use a reporter which looks like more a progress bar use the following option: -```php -./bin/kahlan --reporter=bar -./bin/kahlan --reporter=verbose -``` - -You can easily roll you own if these reporters don't fit your needs. - -For example, if you want a console based reporter, create a PHP class which extends `kahlan\reporter\Terminal`. The `Terminal` class offers some useful methods like `write()` for doing some echos on the terminal. But if you wanted to create some kind of JSON reporter extending from `kahlan\reporter\Reporter` would be enough. - -Example of a custom console reporter: -```php -write('✓', "green"); - } - - /** - * Callback called on failure. - * - * @param object $report An expect report object. - */ - public function fail($report = null) - { - $this->write('☠', "red"); - $this->write("\n"); - $this->_report($report); - } - - /** - * Callback called when an exception occur. - * - * @param object $report An expect report object. - */ - public function exception($report = null) - { - $this->write('☠', "magenta"); - $this->write("\n"); - $this->_report($report); - } - - /** - * Callback called on a skipped spec. - * - * @param object $report An expect report object. - */ - public function skip($report = null) - { - $this->write('-', "cyan"); - } - - /** - * Callback called at the end of specs processing. - */ - public function end($results = []) - { - $this->write("\n"); - $this->_summary($results); - $this->_focused($results); - } -} -?> -``` - -**Note:** `_report()` and `_summary()` are also two inherited methods. Their roles are to format errors and to display a summary of passed tests respectively. Feel free to dig into the source code if you want some more specific output for that. - -The next step is to register your new reporter so you'll need to create you own custom [config file](config-file.md)). - -Example of config file: -```php -reporters(); - $reporters->add('myconsole', new MyReporter(['start' => $this->_start)); -}); - -// Apply our logic to the `'console'` entry point. -Filter::apply($this, 'console', 'kahlan.myconsole'); -?> -``` - -`$this->_start` is the timestamp in micro seconds of when the process has been started. If passed to reporter, it'll be able to display an accurate execution time. - -**Note:** `'myconsole'` is an arbitrary name, it can be anything. - -Let's run it: -```php -./bin/kahlan --config=my-config.php -``` -![custom_reporter](assets/custom_reporter.png) - -A bit ugly, but the check marks and the skulls are present. diff --git a/docs/deprecated/stubs.md b/docs/deprecated/stubs.md deleted file mode 100644 index 4291518d..00000000 --- a/docs/deprecated/stubs.md +++ /dev/null @@ -1,286 +0,0 @@ -## Stubs - -* [Method Stubbing](#method-stubbing) -* [Instance Stubbing](#instance-stubbing) -* [Class Stubbing](#class-stubbing) -* [Custom Stubbing](#custom-stubbing) - -To enable **Method Stubbing** add the following `use` statement in the top of your specs: - -```php -use kahlan\plugin\Stub; -``` - -### Method Stubbing - -`Stub::on()` can stub any existing methods on any class. - -```php -it("stubs a method", function() { - - $instance = new MyClass(); - Stub::on($instance)->method('myMethod')->andReturn('Good Morning World!'); - expect($instance->myMethod())->toBe('Good Morning World!'); - -}); -``` - -You can stub subsequent calls to different return values: - -```php -it("stubs a method with multiple return values", function() { - - $instance = new MyClass(); - Stub::on($instance)->method('sequential')->andReturn(1, 3, 2); - expect($instance->myMethod())->toBe(1); - expect($instance->myMethod())->toBe(3); - expect($instance->myMethod())->toBe(2); - -}); -``` - -You can also stub `static` methods using `::`: - -```php -it("stubs a static method", function() { - - $instance = new MyClass(); - Stub::on($instance)->method('::myMethod')->andReturn('Good Morning World!'); - expect($instance::myMethod())->toBe('Good Morning World!'); - -}); -``` - -And it's also possible to use a closure directly: - -```php -it("stubs a method using a closure", function() { - - Stub::on($foo)->method('message', function($param) { return $param; }); - -}); -``` - -You can use the `methods()` method for reducing verbosity: - -```php -it("stubs many methods", function() { - - $instance = new MyClass(); - Stub::on($instance)->methods([ - 'message' => ['Good Morning World!', 'Good Bye World!'], - 'bar' => ['Hello Bar!'] - ]); - - expect($instance->message())->toBe('Good Morning World!'); - expect($instance->message())->toBe('Good Bye World!'); - expect($instance->bar())->toBe('Hello Bar!'); - -}); -``` - -**Note:** Using the `'bar' => 'Hello Bar!'` syntax is not allowed here. Indeed, direct assignation is considered as a closure definition. For example, in `'bar' => function() {return 'hello'}` the closure is considered as the method definition for the `bar()` method. On the other hand, with `'bar' => [function() {return 'hello'}]`, the closure will be the return value of the `bar()` method. - -### Instance Stubbing - -When you are testing an application, sometimes you need a simple, polyvalent instance for receiving a couple of calls. `Stub::create()` can create such polyvalent instance: - -```php -it("generates a polyvalent instance", function() { - - $stub = Stub::create(); - expect(is_object($stub))->toBe(true); - expect($stub->something())->toBe(null); - -}); -``` - -**Note:** Generated stubs implements by default `__call()`, `__callStatic()`,`__get()`, `__set()` and some other magic methods for a maximum of polyvalence. - -So by default `Stub::on()` can be applied on any method name. Indeed `__call()` will catch everything. However, you should pay attention that `method_exists` won't work on this "virtual method stubs". - -To make it works, you will need to add the necessary "endpoint(s)" using the `'methods'` option like in the following example: - -```php -it("stubs a static method", function() { - - $stub = Stub::create(['methods' => ['myMethod']]); // Adds the method `'myMethod'` as an existing "endpoint" - expect(method_exists($stub, 'myMethod'))->toBe(true); // It works ! - -}); -``` - -### Class Stubbing - -You can also create class names (i.e a string) using `Stub::classname()`: - -```php -it("generates a polyvalent class", function() { - - $class = Stub::classname(); - expect(is_string($stub))->toBe(true); - - $stub = new $class() - expect($stub)->toBeAnInstanceOf($class); - -}); -``` - -Class stubbing is useful when you need to stub instance's methods of a specific class. Let's take the following code as example: - -```php -namespace controller; - -use Exception; -use model\User; - -class testController { - - public function testFunction() - { - $user = new User(); - $user->name = 'Username'; - $user->email = 'username@example.com'; - - if (!$user->save()) { - throw new Exception('Something gone wrong'); - } - } -} -``` - -To test that the above exception is correctly thrown when `$user->save()` is false, you can roll on : - -```php -use controller\testController; -use Exception; - -describe("testController", function() { - - describe("->testFunction()", function() { - - it("throws an exception when save fails", function() { - - // Note: the sub must provide all arguments required by the `User::save()` method. - Stub::on('model\User')->method("save", function() { - return false; - }); - - expect(function() { - $controller = new testController(); - $controller->testFunction(); - })->toThrow(new Exception('Something gone wrong')); - - }); - - }); - -}); -``` - -### Custom Stubbing - -There are also a couple of options for creating some stubs which inherit a class, implement interfaces or use traits. - -An example using `'extends'`: - -```php -it("stubs an instance with a parent class", function() { - - $stub = Stub::create(['extends' => 'kahlan\util\Text']); - expect(is_object($stub))->toBe(true); - expect(get_parent_class($stub))->toBe('kahlan\util\Text'); - -}); -``` -**Tip:** If you extends from an abstract class, all missing methods will be automatically added to your stub. - -**Note:** If the `'extends'` option is used, magic methods **won't be included**, so as to to avoid any conflict between your tested classes and the magic method behaviors. - -However, if you still want to include magic methods with the `'extends'` option, you can manually set the `'magicMethods'` option to `true`: - -```php -it("stubs an instance with a parent class and keeps magic methods", function() { - - $stub = Stub::create([ - 'extends' => 'kahlan\util\Text', - 'magicMethods' => true - ]); - - expect($stub)->toReceive('__get'); - expect($stub)->toReceiveNext('__set'); - expect($stub)->toReceiveNext('__isset'); - expect($stub)->toReceiveNext('__unset'); - expect($stub)->toReceiveNext('__sleep'); - expect($stub)->toReceiveNext('__toString'); - expect($stub)->toReceiveNext('__invoke'); - expect(get_class($stub))->toReceive('__wakeup'); - expect(get_class($stub))->toReceiveNext('__clone'); - - $prop = $stub->prop; - $stub->prop = $prop; - isset($stub->prop); - unset($stub->prop); - $serialized = serialize($stub); - unserialize($serialized); - $string = (string) $stub; - $stub(); - $stub2 = clone $stub; - -}); -``` - -An other example using `'implements'`: - -```php -it("stubs an instance implementing some interfaces", function() { - - $stub = Stub::create(['implements' => ['ArrayAccess', 'Iterator']]); - $interfaces = class_implements($stub); - expect($interfaces)->toHaveLength(3); - expect(isset($interfaces['ArrayAccess']))->toBe(true); - expect(isset($interfaces['Iterator']))->toBe(true); - expect(isset($interfaces['Traversable']))->toBe(true); //Comes with `'Iterator'` - -}); -``` - -A last example using `'uses'` to test your traits directly: - -```php -it("stubs an instance using a trait", function() { - $stub = Stub::create(['uses' => 'spec\mock\plugin\stub\HelloTrait']); - expect($stub->hello())->toBe('Hello World From Trait!'); -}); -``` - - -### Stubbing via a layer - -#### Using a `Stub` instance. - -With user defined classes, you can apply stubs everywhere. However this stubbing technique has some limitation with PHP core classes. Let's take the following example as an illustration: - -```php -it("can't stubs PHP core method", function() { - - $redis = Stub::create(['extends' => 'Redis']); - Stub::on($redis)->method('connect')->andReturn('stubbed'); - expect($stub->connect('127.0.0.1'))->toBe('stubbed'); //It fails - -}); -``` - -In the above example, `Redis` is a built-in class. So in this case, all inherited methods are not real PHP methods but some built-in C methods. And it's not possible to change the behavior of built-in C methods. - -So the alternative here is to override all parent methods using the `'layer'` option to be PHP methods. With The layer option set to `true`, all methods from the parent class will be overrided in PHP to call their parent method in C. So the following spec will now pass. - -```php -it("stubs overrided PHP core method", function() { - - $redis = Stub::create(['extends' => 'Redis', 'layer' => true]); - Stub::on($redis)->method('connect')->andReturn('stubbed'); - expect($stub->connect('127.0.0.1'))->toBe('stubbed'); //It passes - -}); -``` diff --git a/docs/deprecated/why-this-one.md b/docs/deprecated/why-this-one.md deleted file mode 100644 index d57f8830..00000000 --- a/docs/deprecated/why-this-one.md +++ /dev/null @@ -1,50 +0,0 @@ -## Why This One? - -One of PHP's assumptions is that once you define a function/constant/class it stays defined forever. Although this assumption is not really problematic when you are building an application, things get a bit more complicated if you want your application to be easily testable. - -**The main test frameworks for PHP are:** - -* [PHPUnit](https://phpunit.de) _(which reaches [23.80% of code coverage as of PHPUnit 4.4](assets/phpunit_4.4_code_coverage.png))_ -* [phpspec](http://phpspec.net) -* [atoum](http://docs.atoum.org) -* [SimpleTest](http://www.simpletest.org) -* [Enhance-PHP](https://github.com/Enhance-PHP/Enhance-PHP) -* etc. - -Whilst these "old school frameworks" are considered fairly mature, they don't allow easy testing of hard coded references. - -Furthermore, they don't use the `describe-it` syntax either; `describe-it` allows a clean organization of tests to simplify their maintenance (avoiding [this kind of organization](https://github.com/sebastianbergmann/phpunit/tree/master/tests/Regression), for example!). Moreover, the `describe-it` syntax makes tests more reader-friendly (even better than the [atoum fluent syntax organization](https://github.com/atoum/atoum/blob/master/tests/units/classes/asserters/dateInterval.php)) - -**So what about new test frameworks for PHP ?** - -* [Peridot](https://github.com/peridot-php/peridot) -* [pho](https://github.com/danielstjules/pho) -* [Testify](https://github.com/marco-fiset/Testify.php) -* [pecs](https://github.com/noonat/pecs) -* [speciphy](https://github.com/speciphy/speciphy) -* [dspec](https://github.com/davedevelopment/dspec) -* [preview](https://github.com/v2e4lisp/preview) -* etc. - -In the list above, although [Peridot](https://github.com/peridot-php/peridot) seems to be mature, it only provides the `describe-it` syntax. And all other frameworks seems to be some simple proof of concept of the `describe-it` philosophy. - -So, Kahlan was created out of frustration with all existing testing frameworks in PHP. Instead of introducing some new philosophical concepts, tools, java practices or other nonsense, Kahlan focuses on simply providing an environment which allows you to **easily test your code, even with hard coded references**. - -To achieve this goal, **Kahlan allows you to stub or monkey patch your code**, just like in Ruby or JavaScript, without any required PECL-extentions. This way, you don't need to put [DI everywhere just to be able to write tests](http://david.heinemeierhansson.com/2012/dependency-injection-is-not-a-virtue.html)! - -Some projects like [AspectMock](https://github.com/Codeception/AspectMock) attempted to bring this kind of metaprogramming flexibility for PHPUnit, but Kahlan aims to gather all of these facilities into a full-featured framework boasting a `describe-it` syntax, a lightweight approach and a simple API. - -### Main Features - -* Simple API -* Code Coverage metrics ([xdebug](http://xdebug.org) or [phpdbg](http://phpdbg.com/docs) required) -* Handy stubbing system ([mockery](https://github.com/padraic/mockery) or [prophecy](https://github.com/phpspec/prophecy) are no longer needed) -* Set stubs on your class methods directly (i.e allows dynamic mocking) -* Ability to Monkey Patch your code (i.e. allows replacement of core functions/classes on the fly) -* Ability to set stub on core classes through a Layer patcher (useful to set subs on Phalcon core classes for example) -* Check called methods on your class/instances -* Built-in Reporters/Exporters (Terminal, Coveralls, Code Climate, Scrutinizer, Clover) -* Extensible, customizable workflow -* Small code base (~10 times smaller than PHPUnit) - -All of these features work with the [Composer](https://getcomposer.org/) autoloader out of the box. If you have your own autoloader check the [integration section](integration.md). diff --git a/docs/deprecated/overview.md b/docs/dsl.md similarity index 82% rename from docs/deprecated/overview.md rename to docs/dsl.md index 4477ba17..503ee305 100644 --- a/docs/deprecated/overview.md +++ b/docs/dsl.md @@ -1,4 +1,4 @@ -## Overview +## DSL ### Describe Your Specs @@ -6,17 +6,11 @@ Because test organization is one of the key point of keeping clean and maintaina ```php describe("ToBe", function() { - describe("::match()", function() { - it("passes if true === true", function() { - expect(true)->toBe(true); - }); - }); - }); ``` @@ -30,42 +24,35 @@ As the name implies, the `beforeEach` function is called once before **each** sp ```php describe("Setup and Teardown", function() { - beforeEach(function() { $this->foo = 1; }); describe("Nested level", function() { - beforeEach(function() { $this->foo++; }); it("expects that the foo variable is equal to 2", function() { - expect($this->foo)->toBe(2); - }); - }); - }); ``` Setup and Teardown functions can be used at any `describe` or `context` level: -* `before`: Run once inside a `describe` or `context` before all contained specs. +* `beforeAll`: Run once inside a `describe` or `context` before all contained specs. * `beforeEach`: Run before each spec of the same level. * `afterEach`: Run after each spec of the same level. -* `after`: Run once inside a `describe` or `context` after all contained specs. +* `afterAll`: Run once inside a `describe` or `context` after all contained specs. ### Memoized Helper using `given()` -Since `beforeEach()` is runned before each spec, all defined variables are reinitialised on each specs even when not needed. So in this case it's possible to use `given()` instead. Given's blocks are only executed when referenced (i.e. lazy loading), which mean that ordering of these blocks are irrelevant. +Since `beforeEach()` is ran before each spec, all defined variables are reinitialised on each specs even when not needed. So in this case it's possible to use `given()` instead. Given's blocks are only executed when referenced (i.e. lazy loading), which mean that ordering of these blocks are irrelevant. ```php describe("Lazy loadable variables", function() { - given('firstname', function() { return 'Johnny'; }); given('fullname', function() { return "{$this->firstname} {$this->lastname}"; @@ -75,7 +62,6 @@ describe("Lazy loadable variables", function() { it("lazy loads variables in cascades", function() { expect($this->fullname)->toBe('Johnny Boy'); }); - }); ``` @@ -85,13 +71,9 @@ Expectations are built using the `expect` function which takes a value, called t ```php describe("Positive Expectation", function() { - it("expects that 5 > 4", function() { - expect(5)->toBeGreaterThan(4); - }); - }); ``` @@ -103,43 +85,33 @@ Any matcher can be evaluated negatively by chaining `expect` with `not` before c ```php describe("Negative Expectation", function() { - it("doesn't expect that 4 > 5", function() { - expect(4)->not->toBeGreaterThan(5); - }); - }); ``` ### Asynchronous Expectations -To perform some asynchronous tests it's possible to use the `waitsFor` statement. This statement runs a passed closure until all contained expectations passes or a timeout is reached. `waitsFor` can be useful to waits for elements to appear/disappear on a browser page during some functionnal testing for example. +To perform some asynchronous tests it's possible to use the `waitsFor` statement. This statement runs a passed closure until all contained expectations passes or a timeout is reached. `waitsFor` can be useful to waits for elements to appear/disappear on a browser page during some functional testing for example. ```php describe("Asynchronous Expectations", function() { - - it("waits in vain", function() { - + it("will timeout for sure", function() { waitsFor(function() { expect(false)->toBe(true); }); - }); it("waits to be lucky", function() { - waitsFor(function() { return mt_rand(0, 10); })->toBe(10); - }); - }, 10); ``` -In the example above, the timeout has been setted globally at the bottom of `describe()` statement. However it can also be overrided at a `context()/it()` level or simply by setting the second parameter of `waitsFor()`. If no timeout is defined, the default timeout will be set to `0`. +In the example above, the timeout has been set globally at the bottom of `describe()` statement. However it can also be overridden at a `context()/it()` level or simply by setting the second parameter of `waitsFor()`. If no timeout is defined, the default timeout will be set to `0`. ### Variable scope @@ -147,17 +119,13 @@ You can use `$this` for making a variable **available** for a sub scope: ```php describe("Scope inheritance", function() { - beforeEach(function() { $this->foo = 5; }); it("accesses variable defined in the parent scope", function() { - expect($this->foo)->toEqual(5); - }); - }); ``` @@ -165,27 +133,21 @@ You can also play with scope's data inside closures: ```php describe("Scope inheritance & closure", function() { - it("sets a scope variables inside a closure", function() { - $this->closure = function() { $this->foo = 'bar'; }; $this->closure(); expect($this->foo)->toEqual('bar'); - }); it("gets a scope variable inside closure", function() { - $this->foo = 'bar'; $this->closure = function() { return $this->foo; }; expect($this->closure())->toEqual('bar'); - }); - }); ``` @@ -195,53 +157,54 @@ describe("Scope inheritance & closure", function() { ```php describe("Scope isolation", function() { - it("sets a variable in the scope", function() { - $this->foo = 2; expect($this->foo)->toEqual(2); - }); it("doesn't find any foo variable in the scope", function() { - expect(isset($this->foo))->toBe(false); - }); - }); ``` ### Control-flow -Spec control flow is similar to `Jasmine`. In other words functions executed on a scope level using the following order `before`, `beforeEach`, `after` and `afterEach`. +Spec control flow is similar to `Jasmine`. In other words functions executed on a scope level using the following order `beforeAll`, `beforeEach`, `afterAll` and `afterEach`. ```php describe(function() { - before(function() { + beforeAll(function() { //b1 }); + describe(function() { - before(function() { + beforeAll(function() { //b2 }); + beforeEach(function() { //be1 }); + it("runs a spec", function() { //it1 }); + it("runs a spec", function() { //it2 }); + afterEach(function() { //ae1 }); - after(function() { + + afterAll(function() { //a2 }); }); - after(function() { + + afterAll(function() { //a1 }); }); diff --git a/docs/faq.md b/docs/faq.md deleted file mode 100644 index 93ab223a..00000000 --- a/docs/faq.md +++ /dev/null @@ -1,16 +0,0 @@ -## FAQ - -**Q:** After an update my code doesn't work?
-**A:** Please, make sure that you cleared-up the **/tmp/kahlan** folder, and the updated version is a BC-compatible version. - -**Q:** How can I use my **favorite framework** with Kahlan?
-**A:** You can look up in [Framework integration](integration.md). - -**Q:** What if there is no info about my framework in integration?
-**A:** All frameworks which use a PSR-0 compatible loader can be integrated using the generic way. Otherwise you'll need to make a PR. - -**Q:** Where can i view current roadmap?
-**A:** You can view it [here](https://github.com/kahlan/kahlan/wiki/Roadmap) - -**Q:** I have a question and can't solve it by myself!
-**A:** If this question is related to **Kahlan**, please open issue. Or ask your question in live on [IRC](index.md). \ No newline at end of file diff --git a/docs/getting-started.md b/docs/getting-started.md index 80f30ddc..4de22961 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,42 +1,48 @@ -## Getting started +# Getting Started -**Requirement: Just before continuing, make sure you have installed [Composer](https://getcomposer.org/).** + +## Requirements +- PHP 5.5+ +- [phpdbg](http://php.net/manual/en/debugger-about.php) or [Xdebug](http://xdebug.org/) (only required for code coverage analysis) -To make a long story short let's take [the following repository](https://github.com/crysalead/text) as an example. -It's a simple string class in PHP which give you a better understanding on how to structure a project to be easily testable with Kahlan. + +## Installation +The recommended way to install Kahlan is with [Composer](http://getcomposer.org/) as a *development* dependency of your project. -Here is the tree structure of this project: - -``` -├── bin -├── .gitignore -├── .scrutinizer.yml # Optional, it's for using https://scrutinizer-ci.com -├── .travis.yml # Optional, it's for using https://travis-ci.org -├── composer.json # Need at least the Kahlan dependency -├── LICENSE.txt -├── README.md -├── spec # The directory which contain specs -│   └── text -│   └── TextSpec.php # Name of spec should match pattern *Spec.php -├── src # The directory which contain sources code -│   └── Text.php +```bash +composer require --dev kahlan/kahlan ``` -To start playing with it you'll need to: +Alternatively, you may manually add `"kahlan/kahlan": "~3.0"` to the `require-dev` dependencies within your `composer.json`. -```bash -git clone git://github.com/crysalead/text.git -cd text -composer install -``` -And then run the tests (referred to as 'specs') with: + +## Running Kahlan +Once Kahlan is installed, you can run your tests (referred to as *specs*) with: ```bash -./bin/kahlan --coverage=4 +./bin/kahlan ``` -**Note:** the `--coverage=4` option is optional. +For a full list of the options, see the [CLI Options](cli-options.md). -PS: If your library is not compatible with composer, check the [integration section](integration.md). + + +## Directory Structure +The recommended directory structure is to add a `spec` directory at the top level of your project. You may then place your *Spec* files within this directory. Spec files should have a `Spec` suffix. The `spec` directory should mirror the structure of your source code directory. + +An example directory structure would be: + +``` +├── spec # The directory containing your specs +│ └── ClassASpec.php +│ └── subdir +│ └── ClassBSpec.php +├── src # The directory containing your source code +│ └── ClassA.php +│ └── subdir +│ └── ClassB.php +├── composer.json +└── README.md +``` diff --git a/docs/index.md b/docs/index.md index 42061c01..75cc59e1 120000 --- a/docs/index.md +++ b/docs/index.md @@ -1 +1 @@ -README.md \ No newline at end of file +overview.md \ No newline at end of file diff --git a/docs/integration.md b/docs/integration.md index 58d726bb..51dfbb63 100644 --- a/docs/integration.md +++ b/docs/integration.md @@ -1,12 +1,12 @@ ## Integration with popular frameworks -Kahlan fits perfectly with the composer autoloader. However a couple of popular frameworks still use their own autoloader and you will need to make all your namespaces to be autoloaded correctly in the test environment to make it works. +Kahlan relies on the Composer autoloader. As such, it is compatible with most frameworks. However a couple popular frameworks use their own autoloader, so you will need to add your namespaces to be autoloaded correctly in the test environment. -Hopefully It's easy and simple. Indeed almost all popular frameworks autoloaders are **PSR-0**/**PSR-4** compatible, so the only thing you will need to do is to correctly configure your **kahlan-config.php** config file to manually add to the composer autoloader all namespaces which are "ouside the composer scope". +You will need to configure your Kahlan config file to manually add to the Composer autoloader which are "outside the composer scope". ### Working with a PSR-0 compatible architecture. -Let's take a situation where you have the following directories: `app/models/` and `app/controllers/` and each one are respectively attached to the `Api\Models` and `Api\Controllers` namespaces. To make them autoloaded with Kahlan you will need to manually add this **PSR-4** based namespaces in your **kahlan-config.php** config file: +Let's take a situation where you have the following directories: `app/models/` and `app/controllers/` and each one are respectively attached to the `Api\Models` and `Api\Controllers` namespaces. To autoload them with Kahlan you will need to manually add these PSR-4 namespaces in your **kahlan-config.php** config file: ```php use Kahlan\Filter\Filter; @@ -22,80 +22,55 @@ Filter::register('mycustom.namespaces', function($chain) { Filter::apply($this, 'namespaces', 'mycustom.namespaces'); ``` -### Using the `Layer` patcher (Phalcon). +### Laravel -When a class extends a built-in class (i.e. a non PHP class) it's not possible to stub core methods. Long story short, let's take the following example as an illustration: +To import all Laravel "test facilities" into Kahlan you can make use of [this dedicated plugin](https://github.com/jarektkaczyk/laravel-kahlan). -We have a model: +### Phalcon -```php -namespace Api\Models; +When a class extends a built-in class (i.e. a non PHP class) it's not possible to stub parent class methods since they are not in PHP userland. -class MyModel extends \Phalcon\Mvc\Model -{ - public $title; - public function getTitle() - { - return $this->title; - } -} -``` +Long story short, let's take the following example as an illustration: -We have a controller: +We have a model: ```php -namespace Api\Contollers; - -use Exception; -use Api\Models\MyModel; +namespace Api\Models; -class MyController extends \Phalcon\Mvc\Controller +class MyModel extends \Phalcon\Mvc\Model { - public function indexAction() - { - $article = MyModel::findFirst(); - $this->view->setVar('title', $article->getTitle()); - } } ``` -And we want to check that `indexAction()` correctly sets the `'title'` view var. This check can be tranlsated into the following spec: +And we want `findFirst()` to return a stubbed result. This can be translated into the following spec: ```php namespace Api\Spec\Contollers; use Api\Models\MyModel; -use Api\Contollers\MyController; -use Kahlan\Plugin\Stub; - -describe("MyController", function() { - - describe("->indexAction()", function() { - it("correctly populates the view var", function() { +describe("MyModel", function() { - $article = new MyModel(); - $article->title = 'Hello World'; + it("stubs findFirst as an example", function() { - Stub::on('Api\Models\MyModel')->method('::findFirst')->andReturn($article); + $article = new MyModel(); - $controller = new MyController(); - $controller->indexAction(); + allow('Api\Models\MyModel')->toReceive('::findFirst')->andReturn($article); - expect($controller->view->getVar('title'))->toBe('Hello World'); + $actual = MyModel::findFirst(); - }); + expect($actual)->toBe($article); }); }); ``` -Unfortunalty it doesn't work out of the box. Indeed `MyModel` extends `Phalcon\Mvc\Model` which is a core class (i.e a class compiled from C sources). Since the method `MyModel::findFirst()` doesn't exists in PHP land, it can't be stubbed. +Unfortunately it doesn't work out of the box because `MyModel` extends `Phalcon\Mvc\Model` which is a core class (i.e a class compiled from C sources). So `MyModel::findFirst()` doesn't exist in PHP userland and can't be stubbed. -The workaround here is to configure the Kahlan's `Layer` patcher in [the `kahlan-config.php` file](config-file.md). The `Layer` patcher can dynamically replace all `extends` done on core class to an intermediate layer class in PHP. +The workaround here is to add the Kahlan's `Layer` patcher in [your `kahlan-config.php` file](config-file.md). The `Layer` patcher will dynamically replace all `extends` done on core class to an intermediate layer class in PHP. -For a Phalcon project, the `Layer` patcher can be configured like the following in the Kahlan config file: +So for our example above, the `Layer` patcher can be configured like the following in [your `kahlan-config.php` file](config-file.md): ```php use Kahlan\Filter\Filter; @@ -103,13 +78,11 @@ use Kahlan\Jit\Interceptor; use Kahlan\Plugin\Layer; Filter::register('api.patchers', function($chain) { - if (!$interceptor = Interceptor::instance()) { - return; - } + $interceptor = Interceptor::instance(); $patchers = $interceptor->patchers(); $patchers->add('layer', new Layer([ 'override' => [ - 'Phalcon\Mvc\Model' // this will dynamically apply a layer on top of the `Phalcon\Mvc\Model` to make it stubbable. + 'Phalcon\Mvc\Model' // apply a layer on top of all classes extending `Phalcon\Mvc\Model`. ] ])); @@ -119,8 +92,8 @@ Filter::register('api.patchers', function($chain) { Filter::apply($this, 'patchers', 'api.patchers'); ``` -**Note:** You will probably need to remove all cached files in `/tmp/kahlan` (or in `sys_get_temp_dir() . '/kahlan'` if you are not on linux) to make it works. +**Note:** You will probably need to remove all cached files using `kahlan --cc`. -### Working with a autoloader not compatible with PSR-0. +### Working with a custom autoloader not compatible with PSR-0. In this case your must implement a `PSR-0` **Composer** compatible autoloader. To have a right direction you could see at [sources](https://github.com/composer/composer/blob/master/src/Composer/Autoload/ClassLoader.php), and take care of `findFile`, `loadClass` and `add` functions. diff --git a/docs/matchers.md b/docs/matchers.md index ed6f602c..98b89581 100644 --- a/docs/matchers.md +++ b/docs/matchers.md @@ -1,32 +1,22 @@ ## Matchers -* [Overview](#overview) -* [Classic matchers](#classic) -* [Method invocation matchers](#method) -* [Argument matchers](#argument) -* [Custom matchers](#custom) - -### Overview - **Note:** Expectations can only be done inside `it` blocks. -Kahlan have a lot of matchers, that can help you up in your testing journey. All matchers can be chained up. It shown in a code below. +Kahlan has a lot of matchers that can help you in your testing journey. All matchers can be chained. ```php it("can chain up a lots of matchers", function() { expect([1, 2, 3])->toBeA('array')->toBe([1, 2, 3])->toContain(1); }); ``` - -### Classic matchers + +### Classic matchers **toBe($expected)** ```php it("passes if $actual === $expected", function() { - expect(true)->toBe(true); - }); ``` @@ -34,9 +24,7 @@ it("passes if $actual === $expected", function() { ```php it("passes if $actual == $expected", function() { - expect(true)->toEqual(1); - }); ``` @@ -44,9 +32,7 @@ it("passes if $actual == $expected", function() { ```php it("passes if $actual is truthy", function() { - expect(1)->toBeTruthy(); - }); ``` @@ -54,10 +40,8 @@ it("passes if $actual is truthy", function() { ```php it("passes if $actual is falsy", function() { - expect(0)->toBeFalsy(); expect(0)->toBeEmpty(); - }); ``` @@ -65,9 +49,7 @@ it("passes if $actual is falsy", function() { ```php it("passes if $actual is null", function() { - expect(null)->toBeNull(); - }); ``` @@ -75,11 +57,9 @@ it("passes if $actual is null", function() { ```php it("passes if $actual is of a specific type", function() { - expect('Hello World!')->toBeA('string'); expect(false)->toBeA('boolean'); expect(new stdClass())->toBeA('object'); - }); ``` @@ -97,9 +77,7 @@ it("passes if $actual is of a specific type", function() { ```php it("passes if $actual is an instance of stdObject", function() { - expect(new stdClass())->toBeAnInstanceOf('stdObject'); - }); ``` @@ -107,10 +85,8 @@ it("passes if $actual is an instance of stdObject", function() { ```php it("passes if $actual has the correct length", function() { - expect('Hello World!')->toHaveLength(12); expect(['a', 'b', 'c'])->toHaveLength(3); - }); ``` @@ -118,9 +94,7 @@ it("passes if $actual has the correct length", function() { ```php it("passes if $actual contain $expected", function() { - expect([1, 2, 3])->toContain(3); - }); ``` @@ -128,11 +102,9 @@ it("passes if $actual contain $expected", function() { ```php it("passes if $actual contain $expected key(s)", function() { - - expect([ 'a' =>1, 'b' => 2, 'c' => 3])->toContainKey(a); - expect([ 'a' =>1, 'b' => 2, 'c' => 3])->toContainKey(a, b); - expect([ 'a' =>1, 'b' => 2, 'c' => 3])->toContainKey([a, b]); - + expect(['a' => 1, 'b' => 2, 'c' => 3])->toContainKey(a); + expect(['a' => 1, 'b' => 2, 'c' => 3])->toContainKey(a, b); + expect(['a' => 1, 'b' => 2, 'c' => 3])->toContainKey([a, b]); }); ``` @@ -140,10 +112,8 @@ it("passes if $actual contain $expected key(s)", function() { ```php it("passes if abs($actual - $expected)*2 < 0.01", function() { - expect(1.23)->toBeCloseTo(1.225, 2); expect(1.23)->not->toBeCloseTo(1.2249999, 2); - }); ``` @@ -151,9 +121,7 @@ it("passes if abs($actual - $expected)*2 < 0.01", function() { ```php it("passes if $actual > $expected", function() { - expect(1)->toBeGreaterThan(0.999); - }); ``` @@ -161,9 +129,7 @@ it("passes if $actual > $expected", function() { ```php it("passes if $actual < $expected", function() { - expect(0.999)->toBeLessThan(1); - }); ``` @@ -171,15 +137,14 @@ it("passes if $actual < $expected", function() { ```php it("passes if $closure throws the $expected exception", function() { - $closure = function() { // place the code that you expect to throw an exception in a closure, like so throw new RuntimeException('exception message'); }; + expect($closure)->toThrow(); expect($closure)->toThrow(new RuntimeException()); expect($closure)->toThrow(new RuntimeException('exception message')); - }); ``` @@ -187,19 +152,15 @@ it("passes if $closure throws the $expected exception", function() { ```php it("passes if $actual matches the $expected regexp", function() { - expect('Hello World!')->toMatch('/^H(.*?)!$/'); - }); ``` ```php it("passes if $actual matches the $expected closure logic", function() { - expect('Hello World!')->toMatch(function($actual) { return $actual === 'Hello World!'; }); - }); ``` @@ -207,12 +168,11 @@ it("passes if $actual matches the $expected closure logic", function() { ```php it("passes if $closure echoes the expected output", function() { - $closure = function() { echo "Hello World!"; }; - expect($closure)->toEcho("Hello World!"); + expect($closure)->toEcho("Hello World!"); }); ``` @@ -220,94 +180,164 @@ it("passes if $closure echoes the expected output", function() { ```php it("passes if $closure echoes the expected regex output", function() { - $closure = function() { echo "Hello World!"; }; - expect($closure)->toMatchEcho('/^H(.*?)!$/'); + expect($closure)->toMatchEcho('/^H(.*?)!$/'); }); ``` ```php it("passes if $actual matches the $expected closure logic", function() { - expect('Hello World!')->toMatchEcho(function($actual) { return $actual === 'Hello World!'; }); - }); ``` ### Method invocation matchers -**Note:** You should **always remember** to use `toReceive`, `toReceiveNext` function **before** you call a method. +**Note:** You should **always remember** to use `toReceive` function **before** you call a method. **toReceive($expected)** ```php it("expects $foo to receive message() with the correct param", function() { - $foo = new Foo(); - expect($foo)->toReceive('message')->with('My Message'); - $foo->message('My Message'); + expect($foo)->toReceive('message')->with('My Message'); + expect($foo->message('My Message'))->toBe($foo); }); ``` ```php -it("expects $foo to receive ::message() with the correct param", function() { - +it("expects $foo to receive message() and bail out using a stub", function() { $foo = new Foo(); - expect($foo)->toReceive('::message')->with('My Message'); - $foo::message('My Message'); + expect($foo)->toReceive('message')->andReturn('something'); + expect($foo->message('My Message'))->toBe('something'); }); ``` -```php -it("expects $foo to receive ::message() with the correct param only once", function() { +**Note:** When `andReturn()/andRun()` is not applied, `toReceive()` simply act as a "spy" and let the code execution flow to be unchanged. However when applied, the code execution will bail out with the stub value. +```php +it("expects $foo to receive message() and bail out using a closure for stub", function() { $foo = new Foo(); - expect($foo)->toReceive('::message')->with('My Message')->once(); - $foo::message('My Message'); + expect($foo)->toReceive('message')->andRun(function() { + return 'something'; + }); + expect($foo->message('My Message'))->toBe('something'); +}); +``` + +```php +it("expects Foo to receive ::message() with the correct param", function() { + expect(Foo::class)->toReceive('::message')->with('My Message'); + Foo::message('My Message'); }); ``` ```php -it("expects $foo to receive ::message() with the correct param a specified number of times", function() { +it("expects Foo to receive ::message() with the correct param only once", function() { + expect(Foo::class)->toReceive('::message')->with('My Message')->once(); + Foo::message('My Message'); +}); +``` - $foo = new Foo(); - expect($foo)->toReceive('::message')->with('My Message')->time(2); +```php +it("expects Foo to receive ::message() with the correct param a specified number of times", function() { + expect(Foo::class)->toReceive('::message')->with('My Message')->time(2); $foo::message('My Message'); $foo::message('My Message'); - }); ``` -**toReceiveNext($expected)** - ```php it("expects $foo to receive message() followed by foo()", function() { - $foo = new Foo(); - expect($foo)->toReceive('message'); - expect($foo)->toReceiveNext('foo'); + expect($foo)->toReceive('message')->ordered; + expect($foo)->toReceive('foo')->ordered; $foo->message(); $foo->foo(); - }); ``` + ```php it("expects $foo to receive message() but not followed by foo()", function() { - $foo = new Foo(); - expect($foo)->toReceive('message'); - expect($foo)->not->toReceiveNext('foo'); + expect($foo)->toReceive('message')->ordered; + expect($foo)->not->toReceive('foo')->ordered; $foo->foo(); $foo->message(); +}); +``` +**Note:** You should pay attention that using such matchers will make your tests more "fragile" and can be identified as code smells even though not all code smells indicate real problems. + +### Function invocation matchers + +**Note:** You should **always remember** to use `toBeCalled` function **before** you call a method. + +**toBeCalled()** + +```php +it("expects `time()` to be called", function() { + $foo = new Foo(); + expect('time')->toBeCalled(); + $foo->date(); +}); +``` + +**Note:** When `andReturn()/andRun()` is not applied, `toBeCalled()` simply act as a "spy" and let the code execution flow to be unchanged. However when applied, the code execution will bail out with the stub value. + +```php +it("expects `time()` to be called", function() { + $foo = new Foo(); + expect('time')->toBeCalled()->andReturn(strtotime("now")); + $foo->date(); +}); +``` + +```php +it("expects `time()` to be called", function() { + $foo = new Foo(); + + expect('time')->toBeCalled()->andRun(function() { + return strtotime("now") + }); + + $foo->date(); +}); +``` + +```php +it("expects `time()` to be called with the correct param only once", function() { + $foo = new Foo(); + expect('time')->toBeCalled()->with()->once(); + $foo->date(); +}); +``` + +```php +it("expects `time()` to be called and followed by `rand()`", function() { + $foo = new Foo(); + expect('time')->toBeCalled()->ordered; + expect('rand')->toBeCalled()->ordered; + $foo->date(); + $foo->random(); +}); +``` + +```php +it("expects `time()` to be called and followed by `rand()`", function() { + $foo = new Foo(); + expect('time')->toBeCalled()->ordered; + expect('rand')->toBeCalled()->ordered; + $foo->random(); + $foo->date(); }); ``` @@ -322,23 +352,20 @@ use Kahlan\Arg; With the `Arg` class you can use any existing matchers to test arguments. ```php -it("expects params to match the argument matchers", function() { - +it("expects args to match the argument matchers", function() { $foo = new Foo(); - expect($foo)->toReceive('message')->with(Arg::toBeA('boolean')); - expect($foo)->toReceiveNext('message')->with(Arg::toBeA('string')); + expect($foo)->toReceive('message')->with(Arg::toBeA('boolean'))->ordered; + expect($foo)->toReceive('message')->with(Arg::toBeA('string'))->ordered; $foo->message(true); $foo->message('Hello World!'); - }); ``` -```php -it("expects params match the toContain argument matcher", function() { +```php +it("expects args match the toContain argument matcher", function() { $foo = new Foo(); expect($foo)->toReceive('message')->with(Arg::toContain('My Message')); $foo->message(['My Message', 'My Other Message']); - }); ``` @@ -353,7 +380,6 @@ namespace my\namespace; class ToBeZero { - public static function match($actual, $expected = null) { return $actual === 0; diff --git a/docs/monkey-patching.md b/docs/monkey-patching.md deleted file mode 100644 index d4c608a3..00000000 --- a/docs/monkey-patching.md +++ /dev/null @@ -1,101 +0,0 @@ -## Monkey Patching - -* [Monkey Patch Quit Statements](#monkey-patch-quit-statements) - -To enable **Monkey Patching**, add the following `use` statement in the top of your specs: - -```php -use Kahlan\Plugin\Monkey; -``` - -Monkey Patching allows replacement of core functions and classes that can't be stubbed, for example [time()](http://php.net/manual/en/function.time.php), [DateTime](http://php.net/manual/en/class.datetime.php) or [MongoId](http://php.net/manual/en/class.mongoid.php) for example. - -With Kahlan, you can patch anything you want using `Monkey::patch()`! - -For example, I have the following class which needs to be patched: - -```php -namespace Kahlan\Monkey; - -use DateTime; - -class Foo -{ - public function time() - { - return time(); - } - - public function datetime($datetime = 'now') - { - return new DateTime($datetime); - } -} -``` - -You can patch the `time()` function on the fly like in the following spec: - -```php -namespace spec; - -use Kahlan\Monkey\Foo; - -function mytime() { - return 245026800; -} - -describe("Monkey patching", function() { - - it("patches a core function", function() { - - $foo = new Foo(); - Monkey::patch('time', 'spec\mytime'); - expect($foo->time())->toBe(245026800); - - }); - -}); -``` - -Unbelievable, right? Moreover, you can also replace the `time()` function by a simple closure: - -```php -it("patches a core function with a closure", function() { - - $foo = new Foo(); - Monkey::patch('time', function(){return 123;}); - expect($foo->time())->toBe(123); - -}); -``` - -Using the same syntax, you can also patch any core classes by just monkey patching a fully-namespaced class name to another fully-namespaced class name. - -You can find [another example of how to use Monkey Patching here](https://github.com/warrenseymour/kahlan-lightning-talk). - -### Monkey Patch Quit Statements - -When a unit test exercises code that contains an `exit()` or a `die()` statement, the execution of the whole test suite is aborted. With Kahlan, you can make all quit statements (i.e. like `exit()` or `die()`) throw a `QuitException` instead of quitting the test suite for real. - -To enable **Monkey Patching on Quit Statements** add the following `use` statements in the top of your tests: - -```php -use Kahlan\QuitException; -use Kahlan\Plugin\Quit; -``` - -And then use `Quit::disable()` like in the following: -```php -it("throws an exception when an exit statement occurs if not allowed", function() { - Quit::disable(); - - $closure = function() { - $foo = new Foo(); - $foo->runCodeWithSomeQuitStatementInside(-1); - }; - - expect($closure)->toThrow(new QuitException('Exit statement occurred', -1)); -}); -``` - -**Note:** This only work **for classes loaded by Composer**. If you try to create a stub with a `exit()` statement inside a closure it won't get intercepted by patchers and the application will quit for real. Indeed, **code in `*Spec.php` files are not intercepted and patched**. diff --git a/docs/overview.md b/docs/overview.md index 4477ba17..912ed240 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -1,250 +1,70 @@ -## Overview - -### Describe Your Specs - -Because test organization is one of the key point of keeping clean and maintainable tests, Kahlan allow to group tests syntactically using a closure syntax. - -```php -describe("ToBe", function() { - - describe("::match()", function() { - - it("passes if true === true", function() { - - expect(true)->toBe(true); - - }); - - }); - -}); -``` - -* `describe`: generally contains all specs for a method. Using the class method's name is probably the best option for a clean description. -* `context`: is used to group tests related to a specific use case. Using "when" or "with" followed by the description of the use case is generally a good practice. -* `it`: contains the code to test. Keep its description short and clear. - -### Setup and Teardown - -As the name implies, the `beforeEach` function is called once before **each** spec contained in a `describe`. - -```php -describe("Setup and Teardown", function() { - - beforeEach(function() { - $this->foo = 1; - }); - - describe("Nested level", function() { - - beforeEach(function() { - $this->foo++; - }); - - it("expects that the foo variable is equal to 2", function() { - - expect($this->foo)->toBe(2); - - }); - - }); - -}); -``` - -Setup and Teardown functions can be used at any `describe` or `context` level: - -* `before`: Run once inside a `describe` or `context` before all contained specs. -* `beforeEach`: Run before each spec of the same level. -* `afterEach`: Run after each spec of the same level. -* `after`: Run once inside a `describe` or `context` after all contained specs. - -### Memoized Helper using `given()` - -Since `beforeEach()` is runned before each spec, all defined variables are reinitialised on each specs even when not needed. So in this case it's possible to use `given()` instead. Given's blocks are only executed when referenced (i.e. lazy loading), which mean that ordering of these blocks are irrelevant. - -```php -describe("Lazy loadable variables", function() { - - given('firstname', function() { return 'Johnny'; }); - given('fullname', function() { - return "{$this->firstname} {$this->lastname}"; - }); - given('lastname', function() { return 'Boy'; }); - - it("lazy loads variables in cascades", function() { - expect($this->fullname)->toBe('Johnny Boy'); - }); - -}); -``` - -### Expectations - -Expectations are built using the `expect` function which takes a value, called the **actual**, as parameter and chained with a matcher function taking the **expected** value and some optional extra arguments as parameters. - -```php -describe("Positive Expectation", function() { - - it("expects that 5 > 4", function() { - - expect(5)->toBeGreaterThan(4); - - }); - -}); -``` - -You can find [all built-in matchers here](matchers.md). - -### Negative Expectations - -Any matcher can be evaluated negatively by chaining `expect` with `not` before calling the matcher: - -```php -describe("Negative Expectation", function() { - - it("doesn't expect that 4 > 5", function() { - - expect(4)->not->toBeGreaterThan(5); - - }); - -}); -``` - -### Asynchronous Expectations - -To perform some asynchronous tests it's possible to use the `waitsFor` statement. This statement runs a passed closure until all contained expectations passes or a timeout is reached. `waitsFor` can be useful to waits for elements to appear/disappear on a browser page during some functionnal testing for example. - -```php -describe("Asynchronous Expectations", function() { - - it("waits in vain", function() { - - waitsFor(function() { - expect(false)->toBe(true); - }); - - }); - - it("waits to be lucky", function() { - - waitsFor(function() { - return mt_rand(0, 10); - })->toBe(10); - - }); - -}, 10); -``` - -In the example above, the timeout has been setted globally at the bottom of `describe()` statement. However it can also be overrided at a `context()/it()` level or simply by setting the second parameter of `waitsFor()`. If no timeout is defined, the default timeout will be set to `0`. - -### Variable scope - -You can use `$this` for making a variable **available** for a sub scope: - -```php -describe("Scope inheritance", function() { - - beforeEach(function() { - $this->foo = 5; - }); - - it("accesses variable defined in the parent scope", function() { - - expect($this->foo)->toEqual(5); - - }); - -}); -``` - -You can also play with scope's data inside closures: - -```php -describe("Scope inheritance & closure", function() { - - it("sets a scope variables inside a closure", function() { - - $this->closure = function() { - $this->foo = 'bar'; - }; - $this->closure(); - expect($this->foo)->toEqual('bar'); - - }); - - it("gets a scope variable inside closure", function() { - - $this->foo = 'bar'; - $this->closure = function() { - return $this->foo; - }; - expect($this->closure())->toEqual('bar'); - - }); - -}); -``` - -#### Scope isolation - -**Note:** A variable assigned with `$this` inside either a `describe/context` or an `it` will **not** be available to a parent scope. - -```php -describe("Scope isolation", function() { - - it("sets a variable in the scope", function() { - - $this->foo = 2; - expect($this->foo)->toEqual(2); - - }); - - it("doesn't find any foo variable in the scope", function() { - - expect(isset($this->foo))->toBe(false); - - }); - -}); -``` - -### Control-flow - -Spec control flow is similar to `Jasmine`. In other words functions executed on a scope level using the following order `before`, `beforeEach`, `after` and `afterEach`. - -```php -describe(function() { - before(function() { - //b1 - }); - describe(function() { - before(function() { - //b2 - }); - beforeEach(function() { - //be1 - }); - it("runs a spec", function() { - //it1 - }); - it("runs a spec", function() { - //it2 - }); - afterEach(function() { - //ae1 - }); - after(function() { - //a2 - }); - }); - after(function() { - //a1 - }); -}); -``` - -That code will give a following execution flow: `b1 - b2 - be1 - it1 - ae1 - be1 - it2 - ae1 - a2 - a1` +# Kahlan +> The Unit/BDD PHP Test Framework for Freedom, Truth, and Justice + +Kahlan is a full-featured Unit & BDD test framework a la RSpec/JSpec which uses a `describe-it` syntax and moves testing in PHP one step forward. + +**Kahlan allows to stub or monkey patch your code directly like in Ruby or JavaScript without any required PECL-extentions.** + + + +### Features +- `describe-it` syntax similar to modern BDD testing frameworks +- Code Coverage metrics ([xdebug](http://xdebug.org) or [phpdbg](http://phpdbg.com/docs) required) +- Handy stubbing system ([mockery](https://github.com/padraic/mockery) or [prophecy](https://github.com/phpspec/prophecy) are no longer needed) +- Set stubs on your class methods directly (i.e allows dynamic mocking) +- Ability to Monkey Patch your code (i.e. allows replacement of core functions/classes on the fly) +- Check called methods on your classes/instances +- Built-in Reporters (Terminal or HTML reporting through [istanbul](https://gotwarlost.github.io/istanbul/) or [lcov](http://ltp.sourceforge.net/coverage/lcov.php)) +- Built-in Exporters (Coveralls, Code Climate, Scrutinizer, Clover) +- Extensible, customizable workflow + + + +## License +Licensed using the [MIT license](http://opensource.org/licenses/MIT). + +> The MIT License (MIT) +> +> Copyright (c) 2014 CrysaLEAD +> +> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +## Contributing +To contribute to Kahlan, [open a pull request](https://help.github.com/articles/creating-a-pull-request/) against the `dev` branch with your change. Be sure to update the specs to verify your change works as expected and to prevent regressions. + + +## Documentation +- [Overview](overview.md) + - [Features](overview.md#features) + - [License](overview.md#license) + - [Contributing](overview.md#contributing) +- [Getting Started](getting-started.md) + - [Requirements](getting-started.md#requirements) + - [Installation](getting-started.md#installation) + - [Running Kahlan](getting-started.md#running-kahlan) + - [Directory Structure](getting-started.md#directory-structure) +- [DSL](dsl.md) +- [Matchers](matchers.md) + - [Classic matchers](matchers.md#classic) + - [Method invocation matchers](matchers.md#method) + - [Argument matchers](matchers.md#argument) + - [Custom matchers](matchers.md#custom) +- [Method Stubbing & Monkey Patching](allow.md) + - [Replacing a method](allow.md#method-stubbing) + - [Replacing a function](allow.md#function-stubbing) + - [Replacing a class](allow.md#monkey-patching) +- [Test Double](test-double.md) + - [Instance Double](test-double.md#instance-double) + - [Class Double](test-double.md#class-double) +- [Quit Statement Patching](quit.md) +- [CLI Options](cli-options.md) +- [Reporters](reporters.md) +- [Pro Tips](pro-tips.md) - including CLI arguments +- [The `kahlan-config.php` file](config-file.md) +- [Integration with popular frameworks](integration.md) diff --git a/docs/pro-tips.md b/docs/pro-tips.md index c449d34a..819ad1a5 100644 --- a/docs/pro-tips.md +++ b/docs/pro-tips.md @@ -100,18 +100,17 @@ describe("My Spec", function() { ### Use the focused mode -When writing your tests sometimes you want to **only execute** the test(s) you are working on. For this, you can prefix your spec by doubling the first letter like in the following example: +When writing your tests sometimes you want to **only execute** the test(s) you are working on. For this, you can prefix your spec with an "f" like in the following example: ```php describe("test focused mode", function() { - it("will be ignored", function() { }); it("will be ignored", function() { }); - fit("will be runned", function() { + fit("will be run", function() { }); }); ``` @@ -126,14 +125,12 @@ To comment out a spec, you can use the `x` prefix i.e. `xdescribe`, `xcontext` o ### Skip a spec -To skip a spec you should use a `skipIf()` function inside of it. This function takes a bolean, that mean you can provide a conditions to skip this spec up. In example: +To skip a spec you should use a `skipIf()` function inside of it. This function takes a boolean, that mean you can provide a conditions to skip this spec up. In example: ```php it("should not run on weekends", function() { - skipIf(date("w") == 0 || date("w") == 6); expect(true)->toBe(true); - }); ``` diff --git a/docs/quit.md b/docs/quit.md new file mode 100644 index 00000000..e2c2a900 --- /dev/null +++ b/docs/quit.md @@ -0,0 +1,27 @@ +### Quit Statement Patching + +When a unit test exercises code that contains an `exit()` or a `die()` statement, the execution of the whole test suite is aborted. With Kahlan, you can make all quit statements (i.e. `exit()` or `die()`) throw a `QuitException` instead. + +To enable **Quit Statements Patching** add the following `use` statements in the top of your tests: + +```php +use Kahlan\QuitException; +use Kahlan\Plugin\Quit; +``` + +And then use `Quit::disable()` like so: + +```php +it("throws an exception when an exit statement occurs if not allowed", function() { + Quit::disable(); + + $closure = function() { + $foo = new Foo(); + $foo->runCodeWithSomeQuitStatementInside(-1); + }; + + expect($closure)->toThrow(new QuitException('Exit statement occurred', -1)); +}); +``` + +**Note:** monkey patching only works **for classes loaded by Composer**. If you try to create a stub with an `exit()` statement inside a spec file it won't get intercepted by patchers. **All code in `*Spec.php` files are not intercepted or patched**. diff --git a/docs/reporters.md b/docs/reporters.md index 6fc01ce7..bf5b986f 100644 --- a/docs/reporters.md +++ b/docs/reporters.md @@ -40,63 +40,58 @@ namespace My\Namespace; class MyReporter extends \Kahlan\Reporter\Terminal { /** - * Callback called on successful expectation. + * Callback called after a spec execution. * - * @param object $report An expect report object. + * @param object $log The log object of the whole spec. */ - public function pass($report = null) + public function specEnd($log = null) { - $this->write('✓', "green"); - } - - /** - * Callback called on failure. - * - * @param object $report An expect report object. - */ - public function fail($report = null) - { - $this->write('☠', "red"); - $this->write("\n"); - $this->_report($report); - } - - /** - * Callback called when an exception occur. - * - * @param object $report An expect report object. - */ - public function exception($report = null) - { - $this->write('☠', "magenta"); - $this->write("\n"); - $this->_report($report); - } - - /** - * Callback called on a skipped spec. - * - * @param object $report An expect report object. - */ - public function skip($report = null) - { - $this->write('-', "cyan"); + switch($log->type()) { + case 'passed': + $this->write('✓', "green"); + break; + case 'skipped': + $this->_write('S'); + break; + case 'pending': + $this->_write('P', 'cyan'); + break; + case 'excluded': + $this->_write('X', 'yellow'); + break; + case 'failed': + '☠', "red"); + $this->write("\n"); + $this->_report($log); + break; + case 'errored': + $this->write('☠', "magenta"); + $this->write("\n"); + $this->_report($log); + break; + } } /** * Callback called at the end of specs processing. + * + * @param object $summary The execution summary instance. */ - public function end($results = []) + public function end($summary) { - $this->write("\n"); - $this->_summary($results); - $this->_focused($results); + $this->write('total:' . $summary->total() . "\n"); + $this->write('passed:' . $summary->passed() . "\n"); + $this->write('pending:' . $summary->pending() . "\n"); + $this->write('skipped:' . $summary->skipped() . "\n"); + $this->write('excluded:' . $summary->excluded() . "\n"); + $this->write('failed:' . $summary->failed() . "\n"); + $this->write('errored:'. $summary->errored() . "\n"); } } ?> ``` -**Note:** `_report()` and `_summary()` are also two inherited methods. Their roles are to format errors and to display a summary of passed tests respectively. Feel free to dig into the source code if you want some more specific output for that. +**Note:** `_report()` is an inherited method. Its role is to print a report of passed log as parameter. Feel free to dig into the source code if you want some more specific output for that. The next step is to register your new reporter so you'll need to create you own custom [config file](config-file.md)). diff --git a/docs/stubs.md b/docs/stubs.md deleted file mode 100644 index d4981f1a..00000000 --- a/docs/stubs.md +++ /dev/null @@ -1,286 +0,0 @@ -## Stubs - -* [Method Stubbing](#method-stubbing) -* [Instance Stubbing](#instance-stubbing) -* [Class Stubbing](#class-stubbing) -* [Custom Stubbing](#custom-stubbing) - -To enable **Method Stubbing** add the following `use` statement in the top of your specs: - -```php -use Kahlan\Plugin\Stub; -``` - -### Method Stubbing - -`Stub::on()` can stub any existing methods on any class. - -```php -it("stubs a method", function() { - - $instance = new MyClass(); - Stub::on($instance)->method('myMethod')->andReturn('Good Morning World!'); - expect($instance->myMethod())->toBe('Good Morning World!'); - -}); -``` - -You can stub subsequent calls to different return values: - -```php -it("stubs a method with multiple return values", function() { - - $instance = new MyClass(); - Stub::on($instance)->method('sequential')->andReturn(1, 3, 2); - expect($instance->sequential())->toBe(1); - expect($instance->sequential())->toBe(3); - expect($instance->sequential())->toBe(2); - -}); -``` - -You can also stub `static` methods using `::`: - -```php -it("stubs a static method", function() { - - $instance = new MyClass(); - Stub::on($instance)->method('::myMethod')->andReturn('Good Morning World!'); - expect($instance::myMethod())->toBe('Good Morning World!'); - -}); -``` - -And it's also possible to use a closure directly: - -```php -it("stubs a method using a closure", function() { - - Stub::on($foo)->method('message', function($param) { return $param; }); - -}); -``` - -You can use the `methods()` method for reducing verbosity: - -```php -it("stubs many methods", function() { - - $instance = new MyClass(); - Stub::on($instance)->methods([ - 'message' => ['Good Morning World!', 'Good Bye World!'], - 'bar' => ['Hello Bar!'] - ]); - - expect($instance->message())->toBe('Good Morning World!'); - expect($instance->message())->toBe('Good Bye World!'); - expect($instance->bar())->toBe('Hello Bar!'); - -}); -``` - -**Note:** Using the `'bar' => 'Hello Bar!'` syntax is not allowed here. Indeed, direct assignation is considered as a closure definition. For example, in `'bar' => function() {return 'hello'}` the closure is considered as the method definition for the `bar()` method. On the other hand, with `'bar' => [function() {return 'hello'}]`, the closure will be the return value of the `bar()` method. - -### Instance Stubbing - -When you are testing an application, sometimes you need a simple, polyvalent instance for receiving a couple of calls. `Stub::create()` can create such polyvalent instance: - -```php -it("generates a polyvalent instance", function() { - - $stub = Stub::create(); - expect(is_object($stub))->toBe(true); - expect($stub->something())->toBe(null); - -}); -``` - -**Note:** Generated stubs implements by default `__call()`, `__callStatic()`,`__get()`, `__set()` and some other magic methods for a maximum of polyvalence. - -So by default `Stub::on()` can be applied on any method name. Indeed `__call()` will catch everything. However, you should pay attention that `method_exists` won't work on this "virtual method stubs". - -To make it works, you will need to add the necessary "endpoint(s)" using the `'methods'` option like in the following example: - -```php -it("stubs a static method", function() { - - $stub = Stub::create(['methods' => ['myMethod']]); // Adds the method `'myMethod'` as an existing "endpoint" - expect(method_exists($stub, 'myMethod'))->toBe(true); // It works ! - -}); -``` - -### Class Stubbing - -You can also create class names (i.e a string) using `Stub::classname()`: - -```php -it("generates a polyvalent class", function() { - - $class = Stub::classname(); - expect(is_string($stub))->toBe(true); - - $stub = new $class() - expect($stub)->toBeAnInstanceOf($class); - -}); -``` - -Class stubbing is useful when you need to stub instance's methods of a specific class. Let's take the following code as example: - -```php -namespace controller; - -use Exception; -use model\User; - -class testController { - - public function testFunction() - { - $user = new User(); - $user->name = 'Username'; - $user->email = 'username@example.com'; - - if (!$user->save()) { - throw new Exception('Something gone wrong'); - } - } -} -``` - -To test that the above exception is correctly thrown when `$user->save()` is false, you can roll on : - -```php -use controller\testController; -use Exception; - -describe("testController", function() { - - describe("->testFunction()", function() { - - it("throws an exception when save fails", function() { - - // Note: the sub must provide all arguments required by the `User::save()` method. - Stub::on('model\User')->method("save", function() { - return false; - }); - - expect(function() { - $controller = new testController(); - $controller->testFunction(); - })->toThrow(new Exception('Something gone wrong')); - - }); - - }); - -}); -``` - -### Custom Stubbing - -There are also a couple of options for creating some stubs which inherit a class, implement interfaces or use traits. - -An example using `'extends'`: - -```php -it("stubs an instance with a parent class", function() { - - $stub = Stub::create(['extends' => 'Kahlan\Util\Text']); - expect(is_object($stub))->toBe(true); - expect(get_parent_class($stub))->toBe('Kahlan\Util\Text'); - -}); -``` -**Tip:** If you extends from an abstract class, all missing methods will be automatically added to your stub. - -**Note:** If the `'extends'` option is used, magic methods **won't be included**, so as to to avoid any conflict between your tested classes and the magic method behaviors. - -However, if you still want to include magic methods with the `'extends'` option, you can manually set the `'magicMethods'` option to `true`: - -```php -it("stubs an instance with a parent class and keeps magic methods", function() { - - $stub = Stub::create([ - 'extends' => 'Kahlan\Util\Text', - 'magicMethods' => true - ]); - - expect($stub)->toReceive('__get'); - expect($stub)->toReceiveNext('__set'); - expect($stub)->toReceiveNext('__isset'); - expect($stub)->toReceiveNext('__unset'); - expect($stub)->toReceiveNext('__sleep'); - expect($stub)->toReceiveNext('__toString'); - expect($stub)->toReceiveNext('__invoke'); - expect(get_class($stub))->toReceive('__wakeup'); - expect(get_class($stub))->toReceiveNext('__clone'); - - $prop = $stub->prop; - $stub->prop = $prop; - isset($stub->prop); - unset($stub->prop); - $serialized = serialize($stub); - unserialize($serialized); - $string = (string) $stub; - $stub(); - $stub2 = clone $stub; - -}); -``` - -An other example using `'implements'`: - -```php -it("stubs an instance implementing some interfaces", function() { - - $stub = Stub::create(['implements' => ['ArrayAccess', 'Iterator']]); - $interfaces = class_implements($stub); - expect($interfaces)->toHaveLength(3); - expect(isset($interfaces['ArrayAccess']))->toBe(true); - expect(isset($interfaces['Iterator']))->toBe(true); - expect(isset($interfaces['Traversable']))->toBe(true); //Comes with `'Iterator'` - -}); -``` - -A last example using `'uses'` to test your traits directly: - -```php -it("stubs an instance using a trait", function() { - $stub = Stub::create(['uses' => 'spec\mock\plugin\stub\HelloTrait']); - expect($stub->hello())->toBe('Hello World From Trait!'); -}); -``` - - -### Stubbing via a layer - -#### Using a `Stub` instance. - -With user defined classes, you can apply stubs everywhere. However this stubbing technique has some limitation with PHP core classes. Let's take the following example as an illustration: - -```php -it("can't stubs PHP core method", function() { - - $redis = Stub::create(['extends' => 'Redis']); - Stub::on($redis)->method('connect')->andReturn('stubbed'); - expect($stub->connect('127.0.0.1'))->toBe('stubbed'); //It fails - -}); -``` - -In the above example, `Redis` is a built-in class. So in this case, all inherited methods are not real PHP methods but some built-in C methods. And it's not possible to change the behavior of built-in C methods. - -So the alternative here is to override all parent methods using the `'layer'` option to be PHP methods. With The layer option set to `true`, all methods from the parent class will be overrided in PHP to call their parent method in C. So the following spec will now pass. - -```php -it("stubs overrided PHP core method", function() { - - $redis = Stub::create(['extends' => 'Redis', 'layer' => true]); - Stub::on($redis)->method('connect')->andReturn('stubbed'); - expect($stub->connect('127.0.0.1'))->toBe('stubbed'); //It passes - -}); -``` diff --git a/docs/test-double.md b/docs/test-double.md new file mode 100644 index 00000000..350f9fc1 --- /dev/null +++ b/docs/test-double.md @@ -0,0 +1,129 @@ +## Test Double + +First add the following `use` statement in the top of your specs to be able to create test doubles: + +```php +use Kahlan\Plugin\Double; +``` + + +### Instance Double + +When you are testing an application, sometimes you need a simple, polyvalent instance for receiving a couple of calls. `Double::instance()` can create such polyvalent instance: + +```php +it("makes a instance double", function() { + $double = Double::instance(); + + expect(is_object($double))->toBe(true); + expect($double->something())->toBe(null); +}); +``` + +There are also a couple of options for creating some stubs which inherit a class, implement interfaces or use traits. + +Examples using `'extends'`: + +```php +it("makes a instance double with a parent class", function() { + $double = Double::instance(['extends' => 'Kahlan\Util\Text']); + + expect(is_object($double))->toBe(true); + expect(get_parent_class($double))->toBe('Kahlan\Util\Text'); +}); +``` +**Tip:** If you extend an abstract class, all missing methods will be automatically added to your stub. + +**Note:** If the `'extends'` option is used, magic methods **won't be included**, so as to avoid any conflict between your tested classes and the magic methods. + +However, if you still want to include magic methods with the `'extends'` option, you can manually set the `'magicMethods'` option to `true`: + +```php +it("makes a instance double with a parent class and keeps magic methods", function() { + $double = Double::instance([ + 'extends' => 'Kahlan\Util\Text', + 'magicMethods' => true + ]); + + expect($double)->toReceive('__get')->ordered; + expect($double)->toReceive('__set')->ordered; + expect($double)->toReceive('__isset')->ordered; + expect($double)->toReceive('__unset')->ordered; + expect($double)->toReceive('__sleep')->ordered; + expect($double)->toReceive('__toString')->ordered; + expect($double)->toReceive('__invoke')->ordered; + expect(get_class($double))->toReceive('__wakeup')->ordered; + expect(get_class($double))->toReceiveNext('__clone')->ordered; + + $prop = $double->prop; + $double->prop = $prop; + isset($double->prop); + unset($double->prop); + $serialized = serialize($double); + $string = (string) $double; + $double(); + unserialize($serialized); + $double2 = clone $double; +}); +``` + +And it's also possible to extends built-in PHP classes. + +```php +it("makes a instance double of a PHP core class", function() { + $redis = Double::instance(['extends' => 'Redis']); + allow($redis)->method('connect')->andReturn(true); + + expect($double->connect('127.0.0.1'))->toBe(true); +}); +``` + +If you need your stub to implement a couple of interfaces you can use the `'implements'` option like so: + +```php +it("makes a instance double implementing some interfaces", function() { + $double = Double::instance(['implements' => ['ArrayAccess', 'Iterator']]); + $interfaces = class_implements($double); + + expect($interfaces)->toHaveLength(3); + expect(isset($interfaces['ArrayAccess']))->toBe(true); + expect(isset($interfaces['Iterator']))->toBe(true); + expect(isset($interfaces['Traversable']))->toBe(true); //Comes with `'Iterator'` +}); +``` + +And if you need your stub to implement a couple of traits you can use the `'uses'` option like so: + +```php +it("makes a instance double using a trait", function() { + $double = Double::instance(['uses' => 'spec\mock\plugin\stub\HelloTrait']); + + expect($double->hello())->toBe('Hello World From Trait!'); +}); +``` + +**Note:** Generated stubs implements by default `__call()`, `__callStatic()`,`__get()`, `__set()` and some other magic methods for a maximum of polyvalence. + +So `allow()` on stubs can be applied on any method name. Under the hood `__call()` will catch everything. You should pay attention that `method_exists` won't work on this "virtual method stubs". To make it works, you will need to add the necessary "endpoint(s)" using the `'methods'` option like in the following example: + +```php +it("adds a custom endpoint", function() { + $double = Double::instance(['methods' => ['myMethod']]); + + expect(method_exists($double, 'myMethod'))->toBe(true); +}); +``` + +### Class Double + +You can also create class double names (i.e a string) using `Double::classname()`: + +```php +it("makes a class double", function() { + $class = Double::classname(); + expect(is_string($class))->toBe(true); + + $double = new $class() + expect($double)->toBeAnInstanceOf($class); +}); +``` diff --git a/docs/why-this-one.md b/docs/why-this-one.md deleted file mode 100644 index d57f8830..00000000 --- a/docs/why-this-one.md +++ /dev/null @@ -1,50 +0,0 @@ -## Why This One? - -One of PHP's assumptions is that once you define a function/constant/class it stays defined forever. Although this assumption is not really problematic when you are building an application, things get a bit more complicated if you want your application to be easily testable. - -**The main test frameworks for PHP are:** - -* [PHPUnit](https://phpunit.de) _(which reaches [23.80% of code coverage as of PHPUnit 4.4](assets/phpunit_4.4_code_coverage.png))_ -* [phpspec](http://phpspec.net) -* [atoum](http://docs.atoum.org) -* [SimpleTest](http://www.simpletest.org) -* [Enhance-PHP](https://github.com/Enhance-PHP/Enhance-PHP) -* etc. - -Whilst these "old school frameworks" are considered fairly mature, they don't allow easy testing of hard coded references. - -Furthermore, they don't use the `describe-it` syntax either; `describe-it` allows a clean organization of tests to simplify their maintenance (avoiding [this kind of organization](https://github.com/sebastianbergmann/phpunit/tree/master/tests/Regression), for example!). Moreover, the `describe-it` syntax makes tests more reader-friendly (even better than the [atoum fluent syntax organization](https://github.com/atoum/atoum/blob/master/tests/units/classes/asserters/dateInterval.php)) - -**So what about new test frameworks for PHP ?** - -* [Peridot](https://github.com/peridot-php/peridot) -* [pho](https://github.com/danielstjules/pho) -* [Testify](https://github.com/marco-fiset/Testify.php) -* [pecs](https://github.com/noonat/pecs) -* [speciphy](https://github.com/speciphy/speciphy) -* [dspec](https://github.com/davedevelopment/dspec) -* [preview](https://github.com/v2e4lisp/preview) -* etc. - -In the list above, although [Peridot](https://github.com/peridot-php/peridot) seems to be mature, it only provides the `describe-it` syntax. And all other frameworks seems to be some simple proof of concept of the `describe-it` philosophy. - -So, Kahlan was created out of frustration with all existing testing frameworks in PHP. Instead of introducing some new philosophical concepts, tools, java practices or other nonsense, Kahlan focuses on simply providing an environment which allows you to **easily test your code, even with hard coded references**. - -To achieve this goal, **Kahlan allows you to stub or monkey patch your code**, just like in Ruby or JavaScript, without any required PECL-extentions. This way, you don't need to put [DI everywhere just to be able to write tests](http://david.heinemeierhansson.com/2012/dependency-injection-is-not-a-virtue.html)! - -Some projects like [AspectMock](https://github.com/Codeception/AspectMock) attempted to bring this kind of metaprogramming flexibility for PHPUnit, but Kahlan aims to gather all of these facilities into a full-featured framework boasting a `describe-it` syntax, a lightweight approach and a simple API. - -### Main Features - -* Simple API -* Code Coverage metrics ([xdebug](http://xdebug.org) or [phpdbg](http://phpdbg.com/docs) required) -* Handy stubbing system ([mockery](https://github.com/padraic/mockery) or [prophecy](https://github.com/phpspec/prophecy) are no longer needed) -* Set stubs on your class methods directly (i.e allows dynamic mocking) -* Ability to Monkey Patch your code (i.e. allows replacement of core functions/classes on the fly) -* Ability to set stub on core classes through a Layer patcher (useful to set subs on Phalcon core classes for example) -* Check called methods on your class/instances -* Built-in Reporters/Exporters (Terminal, Coveralls, Code Climate, Scrutinizer, Clover) -* Extensible, customizable workflow -* Small code base (~10 times smaller than PHPUnit) - -All of these features work with the [Composer](https://getcomposer.org/) autoloader out of the box. If you have your own autoloader check the [integration section](integration.md). diff --git a/kahlan-config.travis.php b/kahlan-config.travis.php index 381cba89..faaffb5d 100644 --- a/kahlan-config.travis.php +++ b/kahlan-config.travis.php @@ -5,8 +5,8 @@ use Kahlan\Reporter\Coverage\Exporter\Coveralls; use Kahlan\Reporter\Coverage\Exporter\CodeClimate; -$args = $this->args(); -$args->argument('coverage', 'default', 3); +$commandLine = $this->commandLine(); +$commandLine->option('coverage', 'default', 3); Filter::register('kahlan.coverage', function($chain) { if (!extension_loaded('xdebug')) { @@ -14,12 +14,13 @@ } $reporters = $this->reporters(); $coverage = new Coverage([ - 'verbosity' => $this->args()->get('coverage'), + 'verbosity' => $this->commandLine()->get('coverage'), 'driver' => new Xdebug(), - 'path' => $this->args()->get('src'), + 'path' => $this->commandLine()->get('src'), 'exclude' => [ //Exclude init script 'src/init.php', + 'src/functions.php', //Exclude Workflow from code coverage reporting 'src/Cli/Kahlan.php', //Exclude coverage classes from code coverage reporting (don't know how to test the tester) @@ -37,7 +38,7 @@ 'src/Reporter/Json.php', 'src/Reporter/Tap.php', ], - 'colors' => !$this->args()->get('no-colors') + 'colors' => !$this->commandLine()->get('no-colors') ]); $reporters->add('coverage', $coverage); }); diff --git a/mkdocs.yml b/mkdocs.yml index bccc16f7..6c71693b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,17 +3,16 @@ site_description: Unit/BDD PHP Test Framework for Freedom, Truth, and Justice repo_name: 'GitHub' repo_url: https://github.com/kahlan/kahlan pages: -- ['index.md', 'Index'] -- ['why-this-one.md', 'Why This One?'] -- ['getting-started.md', 'Getting Started'] -- ['overview.md', 'Overview'] -- ['matchers.md', 'Matchers'] -- ['stubs.md', 'Stubs'] -- ['monkey-patching.md', 'Monkey Patching'] -- ['reporters.md', 'Reporters'] -- ['pro-tips.md', 'Pro Tips'] -- ['config-file.md', 'Kahlan-config.php'] -- ['integration.md', 'Integration with popular frameworks'] -- ['faq.md', 'FAQ'] + - 'Overview': 'index.md' + - 'Getting Started': 'getting-started.md' + - 'DSL': 'dsl.md' + - 'Matchers': 'matchers.md' + - 'Method Stubbing & Monkey Patching': 'allow.md' + - 'Test Double': 'test-double.md' + - 'Quit Statement Patching': 'quit.md' + - 'CLI Options': 'cli-options.md' + - 'Reporters': 'reporters.md' + - 'Pro Tips': 'pro-tips.md' + - 'Kahlan-config.php': 'config-file.md' + - 'Integration with popular frameworks': 'integration.md' theme: readthedocs -include_search: true diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 00000000..67fc6fbf --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,24 @@ + + + + The Kahlan coding standard. + + + + + + + + + + + + + + + src + spec + + spec/Fixture/* + + diff --git a/spec/Fixture/Jit/Patcher/Monkey/Class.php b/spec/Fixture/Jit/Patcher/Monkey/Class.php index 2f956d46..33339fcd 100644 --- a/spec/Fixture/Jit/Patcher/Monkey/Class.php +++ b/spec/Fixture/Jit/Patcher/Monkey/Class.php @@ -53,6 +53,15 @@ public function instantiate() new stdClass; } + public function instantiateWithArguments() + { + $this->_db = new PDO( + "mysql:dbname=testdb;host=localhost", + 'root', + '' + ); + } + public function instantiateRootBased() { new \stdClass; @@ -89,6 +98,11 @@ public function staticCallFromUsed() return Text::hash((object) 'hello'); } + public function staticCallAndinstantiation() { + $node = Parser::parse($string); + return new Parser($node); + } + public function noIndent() { rand(); @@ -145,6 +159,9 @@ public function ignoreControlStructure() extract(); for($i=0;$i<1;$i++) {}; foreach($array as $key=>$value) {} + func_get_arg(); + func_get_args(); + func_num_args(); function(){}; if(true){} include('filename'); @@ -165,6 +182,7 @@ function(){}; break; default: } + throw($e); unset($a); while(false){}; true xor(true); @@ -210,3 +228,6 @@ public function ignoreControlStructureInUpperCase() TRUE XOR(TRUE); } } + +Exemple::reset(); +$time = time(); diff --git a/spec/Fixture/Jit/Patcher/Monkey/ClassProcessed.php b/spec/Fixture/Jit/Patcher/Monkey/ClassProcessed.php index 43a989d8..a35159b0 100644 --- a/spec/Fixture/Jit/Patcher/Monkey/ClassProcessed.php +++ b/spec/Fixture/Jit/Patcher/Monkey/ClassProcessed.php @@ -1,5 +1,5 @@ _db = ($__KMONKEY__6__?$__KMONKEY__6__:new $__KMONKEY__6( + "mysql:dbname=testdb;host=localhost", + 'root', + '' + )); } public function instantiateRootBased() - {$__KMONKEY__6 = \Kahlan\Plugin\Monkey::patched(null , 'stdClass'); - new $__KMONKEY__6; + {$__KMONKEY__7__=null;$__KMONKEY__7=\Kahlan\Plugin\Monkey::patched(null , 'stdClass', false, $__KMONKEY__7__); + ($__KMONKEY__7__?$__KMONKEY__7__:new $__KMONKEY__7); } public function instantiateFromUsed() - {$__KMONKEY__7 = \Kahlan\Plugin\Monkey::patched(null, 'Kahlan\MongoId'); - new $__KMONKEY__7; + {$__KMONKEY__8__=null;$__KMONKEY__8=\Kahlan\Plugin\Monkey::patched(null, 'Kahlan\MongoId', false, $__KMONKEY__8__); + ($__KMONKEY__8__?$__KMONKEY__8__:new $__KMONKEY__8); } public function instantiateRootBasedFromUsed() - {$__KMONKEY__8 = \Kahlan\Plugin\Monkey::patched(null , 'MongoId'); - new $__KMONKEY__8; + {$__KMONKEY__9__=null;$__KMONKEY__9=\Kahlan\Plugin\Monkey::patched(null , 'MongoId', false, $__KMONKEY__9__); + ($__KMONKEY__9__?$__KMONKEY__9__:new $__KMONKEY__9); } public function instantiateFromUsedSubnamespace() - {$__KMONKEY__9 = \Kahlan\Plugin\Monkey::patched(null, 'sub\name\space\MyClass'); - new $__KMONKEY__9; + {$__KMONKEY__10__=null;$__KMONKEY__10=\Kahlan\Plugin\Monkey::patched(null, 'sub\name\space\MyClass', false, $__KMONKEY__10__); + ($__KMONKEY__10__?$__KMONKEY__10__:new $__KMONKEY__10); } public function instantiateVariable() @@ -80,24 +89,29 @@ public function instantiateVariable() } public function staticCall() - {$__KMONKEY__10 = \Kahlan\Plugin\Monkey::patched(__NAMESPACE__ , 'Debugger', false); - return $__KMONKEY__10::trace(); + {$__KMONKEY__11__=null;$__KMONKEY__11=\Kahlan\Plugin\Monkey::patched(__NAMESPACE__ , 'Debugger', false, $__KMONKEY__11__); + return $__KMONKEY__11::trace(); } public function staticCallFromUsed() - {$__KMONKEY__11 = \Kahlan\Plugin\Monkey::patched(null, 'Kahlan\Util\Text'); - return $__KMONKEY__11::hash((object) 'hello'); + {$__KMONKEY__12__=null;$__KMONKEY__12=\Kahlan\Plugin\Monkey::patched(null, 'Kahlan\Util\Text', false, $__KMONKEY__12__); + return $__KMONKEY__12::hash((object) 'hello'); + } + + public function staticCallAndinstantiation() {$__KMONKEY__13__=null;$__KMONKEY__13=\Kahlan\Plugin\Monkey::patched(__NAMESPACE__ , 'Parser', false, $__KMONKEY__13__); + $node = $__KMONKEY__13::parse($string); + return ($__KMONKEY__13__?$__KMONKEY__13__:new $__KMONKEY__13($node)); } public function noIndent() - {$__KMONKEY__12 = \Kahlan\Plugin\Monkey::patched(__NAMESPACE__ , 'rand', true); -$__KMONKEY__12(); + {$__KMONKEY__14=\Kahlan\Plugin\Monkey::patched(__NAMESPACE__ , 'rand'); +$__KMONKEY__14(); } public function closure() { - $func = function() {$__KMONKEY__13 = \Kahlan\Plugin\Monkey::patched(__NAMESPACE__ , 'rand', true); - $__KMONKEY__13(2.5); + $func = function() {$__KMONKEY__15=\Kahlan\Plugin\Monkey::patched(__NAMESPACE__ , 'rand'); + $__KMONKEY__15(2.5); }; $func(); } @@ -110,23 +124,23 @@ public function staticAttribute() public function lambda() { $initializers = [ - 'name' => function($self) {$__KMONKEY__14 = \Kahlan\Plugin\Monkey::patched(__NAMESPACE__ , 'basename', true);$__KMONKEY__15 = \Kahlan\Plugin\Monkey::patched(__NAMESPACE__ , 'str_replace', true); - return $__KMONKEY__14($__KMONKEY__15('\\', '/', $self)); + 'name' => function($self) {$__KMONKEY__16=\Kahlan\Plugin\Monkey::patched(__NAMESPACE__ , 'str_replace');$__KMONKEY__17=\Kahlan\Plugin\Monkey::patched(__NAMESPACE__ , 'basename'); + return $__KMONKEY__17($__KMONKEY__16('\\', '/', $self)); }, - 'source' => function($self) {$__KMONKEY__16 = \Kahlan\Plugin\Monkey::patched(__NAMESPACE__ , 'Inflector', false); - return $__KMONKEY__16::tableize($self::meta('name')); + 'source' => function($self) {$__KMONKEY__18__=null;$__KMONKEY__18=\Kahlan\Plugin\Monkey::patched(__NAMESPACE__ , 'Inflector', false, $__KMONKEY__18__); + return $__KMONKEY__18::tableize($self::meta('name')); }, - 'title' => function($self) {$__KMONKEY__17 = \Kahlan\Plugin\Monkey::patched(__NAMESPACE__ , 'array_merge', true); + 'title' => function($self) {$__KMONKEY__19=\Kahlan\Plugin\Monkey::patched(__NAMESPACE__ , 'array_merge'); $titleKeys = array('title', 'name'); - $titleKeys = $__KMONKEY__17($titleKeys, (array) $self::meta('key')); + $titleKeys = $__KMONKEY__19($titleKeys, (array) $self::meta('key')); return $self::hasField($titleKeys); } ]; } - public function subChild() {$__KMONKEY__18 = \Kahlan\Plugin\Monkey::patched(__NAMESPACE__ , 'RecursiveIteratorIterator', false); + public function subChild() {$__KMONKEY__20__=null;$__KMONKEY__20=\Kahlan\Plugin\Monkey::patched(__NAMESPACE__ , 'RecursiveIteratorIterator', false, $__KMONKEY__20__); if ($options['recursive']) { - $worker = new $__KMONKEY__18($worker, $iteratorFlags); + $worker = ($__KMONKEY__20__?$__KMONKEY__20__:new $__KMONKEY__20($worker, $iteratorFlags)); } } @@ -145,6 +159,9 @@ public function ignoreControlStructure() extract(); for($i=0;$i<1;$i++) {}; foreach($array as $key=>$value) {} + func_get_arg(); + func_get_args(); + func_num_args(); function(){}; if(true){} include('filename'); @@ -165,6 +182,7 @@ function(){}; break; default: } + throw($e); unset($a); while(false){}; true xor(true); @@ -210,3 +228,6 @@ public function ignoreControlStructureInUpperCase() TRUE XOR(TRUE); } } + +$__KMONKEY__21::reset(); +$time = $__KMONKEY__22(); diff --git a/spec/Fixture/Jit/Patcher/Monkey/Errored.php b/spec/Fixture/Jit/Patcher/Monkey/Errored.php new file mode 100644 index 00000000..9eb084c9 --- /dev/null +++ b/spec/Fixture/Jit/Patcher/Monkey/Errored.php @@ -0,0 +1,2 @@ +suite()->loaded = true; diff --git a/spec/Fixture/Plugin/Stub/AbstractDoz.php b/spec/Fixture/Plugin/Double/AbstractDoz.php similarity index 90% rename from spec/Fixture/Plugin/Stub/AbstractDoz.php rename to spec/Fixture/Plugin/Double/AbstractDoz.php index e3339fdb..b96a2465 100644 --- a/spec/Fixture/Plugin/Stub/AbstractDoz.php +++ b/spec/Fixture/Plugin/Double/AbstractDoz.php @@ -1,5 +1,5 @@ _db = new PDO( + "mysql:dbname=testdb;host=localhost", + 'root', + '' + ); + } + + public function all() + { + $stmt = $this->_db->prepare('SELECT * FROM users'); + $this->_success = $stmt->execute(); + return $stmt->fetchAll(); + } + + public function db() + { + return $this->_db; + } + + public function success() + { + return $this->_success; + } + + public static function create() + { + return new static(); + } +} diff --git a/spec/Fixture/Plugin/Pointcut/Foo.php b/spec/Fixture/Plugin/Pointcut/Foo.php index cd90ac2e..e2e53763 100644 --- a/spec/Fixture/Plugin/Pointcut/Foo.php +++ b/spec/Fixture/Plugin/Pointcut/Foo.php @@ -1,12 +1,10 @@ 'Kahlan\Spec\Fixture\Plugin\Pointcut\Bar' - ]; - protected $_inited = false; protected $_status = 'none'; @@ -43,16 +41,15 @@ public static function messageStatic($message = null) public function bar() { - $bar = $this->_classes['bar']; - $bar = new $bar(); + $bar = new Bar(); return $bar->send(); } - public function __call($name, $params) + public function __call($name, $args) { } - public static function __callStatic($name, $params) + public static function __callStatic($name, $args) { } diff --git a/spec/Fixture/Plugin/Stub/Doz.php b/spec/Fixture/Plugin/Stub/Doz.php deleted file mode 100644 index f279c8ed..00000000 --- a/spec/Fixture/Plugin/Stub/Doz.php +++ /dev/null @@ -1,14 +0,0 @@ -previous = Interceptor::instance(); + Interceptor::unpatch(); + + $cachePath = rtrim(sys_get_temp_dir(), DS) . DS . 'kahlan'; + $include = ['Kahlan\Spec\\']; + $interceptor = Interceptor::patch(compact('include', 'cachePath')); + $interceptor->patchers()->add('pointcut', new PointcutPatcher()); + $interceptor->patchers()->add('monkey', new MonkeyPatcher()); + }); + + /** + * Restore Interceptor class. + */ + afterAll(function () { + Interceptor::load($this->previous); + }); + + it("monkey patches a class", function () { + + $bar = Double::instance(); + allow($bar)->toReceive('send')->andReturn('EOF'); + allow('Kahlan\Spec\Fixture\Plugin\Pointcut\Bar')->toBe($bar); + + $foo = new Foo(); + expect($foo->bar())->toBe('EOF'); + + }); + + it("monkey patches a function", function () { + + $mon = new Mon(); + allow('time')->toBe(function () { + return 123; + }); + expect($mon->time())->toBe(123); + + }); + + it("throws an exception when trying to monkey patch an instance", function () { + + expect(function () { + $foo = new Foo(); + allow($foo)->toBe(Double::instance()); + })->toThrow(new Exception("Error `toBe()` need to be applied on a fully-namespaced class or function name.")); + + }); + + it("throws an exception when trying to monkey patch an instance using a generic stub", function () { + + expect(function () { + $foo = new Foo(); + allow($foo)->toBeOK(); + })->toThrow(new Exception("Error `toBeOK()` need to be applied on a fully-namespaced class or function name.")); + + }); + + context("with an instance", function () { + + it("stubs a method", function () { + + $foo = new Foo(); + allow($foo)->toReceive('message')->andReturn('Good Bye!'); + expect($foo->message())->toBe('Good Bye!'); + + }); + + it("stubs with multiple return value", function () { + + $foo = new Foo(); + allow($foo)->toReceive('message')->andReturn(null, 'Hello World!', 'Good Bye!'); + expect($foo->message())->toBe(null); + expect($foo->message())->toBe('Hello World!'); + expect($foo->message())->toBe('Good Bye!'); + + }); + + it("stubs only on the stubbed instance", function () { + + $foo = new Foo(); + allow($foo)->toReceive('message')->andReturn('Good Bye!'); + expect($foo->message())->toBe('Good Bye!'); + + $foo2 = new Foo(); + expect($foo2->message())->toBe('Hello World!'); + + }); + + it("stubs a method using a closure", function () { + + $foo = new Foo(); + allow($foo)->toReceive('message')->andRun(function ($param) { + return $param; + }); + expect($foo->message('Good Bye!'))->toBe('Good Bye!'); + + }); + + it("stubs a magic method", function () { + + $foo = new Foo(); + allow($foo)->toReceive('magicCall')->andReturn('Magic Call!'); + expect($foo->magicCall())->toBe('Magic Call!'); + + }); + + it("stubs a magic method using a closure", function () { + + $foo = new Foo(); + allow($foo)->toReceive('magicHello')->andRun(function ($message) { + return $message; + }); + expect($foo->magicHello('Hello World!'))->toBe('Hello World!'); + + }); + + it("stubs a static magic method", function () { + + $foo = new Foo(); + allow($foo)->toReceive('::magicCallStatic')->andReturn('Magic Call Static!'); + expect($foo::magicCallStatic())->toBe('Magic Call Static!'); + + }); + + it("stubs a static magic method using a closure", function () { + + $foo = new Foo(); + allow($foo)->toReceive('::magicHello')->andRun(function ($message) { + return $message; + }); + expect($foo::magicHello('Hello World!'))->toBe('Hello World!'); + + }); + + it("overrides previously applied stubs", function () { + + $foo = new Foo(); + allow($foo)->toReceive('magicHello')->andReturn('Hello World!'); + allow($foo)->toReceive('magicHello')->andReturn('Good Bye!'); + expect($foo->magicHello())->toBe('Good Bye!'); + + }); + + it("throws an exception when trying to spy an invalid empty method", function () { + + expect(function () { + $foo = new Foo(); + allow($foo)->toReceive(); + })->toThrow(new InvalidArgumentException("Method name can't be empty.")); + + }); + + it("throws an exception when trying to call `toReceive()`", function () { + + expect(function () { + $foo = new Foo(); + allow($foo)->toBeCalled('magicHello')->andReturn('Hello World!'); + })->toThrow(new Exception("Error `toBeCalled()` are are only available on functions not classes/instances.")); + + }); + + context("with several applied stubs on a same method", function () { + + it("stubs a magic method multiple times", function () { + + $foo = new Foo(); + allow($foo)->toReceive('magic')->with('hello')->andReturn('world'); + allow($foo)->toReceive('magic')->with('world')->andReturn('hello'); + expect($foo->magic('hello'))->toBe('world'); + expect($foo->magic('world'))->toBe('hello'); + + }); + + it("stubs a static magic method multiple times", function () { + + $foo = new Foo(); + allow($foo)->toReceive('::magic')->with('hello')->andReturn('world'); + allow($foo)->toReceive('::magic')->with('world')->andReturn('hello'); + expect($foo::magic('hello'))->toBe('world'); + expect($foo::magic('world'))->toBe('hello'); + + }); + + }); + + context("using the with() parameter", function () { + + it("stubs on matched arguments", function () { + + $foo = new Foo(); + allow($foo)->toReceive('message')->with('Hello World!')->andReturn('Good Bye!'); + expect($foo->message('Hello World!'))->toBe('Good Bye!'); + + }); + + it("doesn't stubs on unmatched arguments", function () { + + $foo = new Foo(); + allow($foo)->toReceive('message')->with('Hello World!')->andReturn('Good Bye!'); + expect($foo->message('Hello!'))->not->toBe('Good Bye!'); + + + }); + + }); + + context("using the with() parameter and the argument matchers", function () { + + it("stubs on matched arguments", function () { + + $foo = new Foo(); + allow($foo)->toReceive('message')->with(Arg::toBeA('string'))->andReturn('Good Bye!'); + expect($foo->message('Hello World!'))->toBe('Good Bye!'); + expect($foo->message('Hello'))->toBe('Good Bye!'); + + }); + + it("doesn't stubs on unmatched arguments", function () { + + $foo = new Foo(); + allow($foo)->toReceive('message')->with(Arg::toBeA('string'))->andReturn('Good Bye!'); + expect($foo->message(false))->not->toBe('Good Bye!'); + expect($foo->message(['Hello World!']))->not->toBe('Good Bye!'); + + }); + + }); + + context("with multiple return values", function () { + + it("stubs a method", function () { + + $foo = new Foo(); + allow($foo)->toReceive('message')->andReturn('Good Evening World!', 'Good Bye World!'); + expect($foo->message())->toBe('Good Evening World!'); + expect($foo->message())->toBe('Good Bye World!'); + expect($foo->message())->toBe('Good Bye World!'); + + }); + + }); + + context("with chain of methods", function () { + + it("expects stubbed chain to be stubbed", function () { + + $foo = new Foo(); + allow($foo)->toReceive('a', 'b', 'c')->andReturn('something'); + $query = $foo->a(); + $select = $query->b(); + expect($select->c())->toBe('something'); + + }); + + it('auto monkey patch core classes using a stub when possible', function () { + + allow('PDO')->toReceive('prepare', 'fetchAll')->andReturn([['name' => 'bob']]); + $user = new User(); + expect($user->all())->toBe([['name' => 'bob']]); + + }); + + it('allows to stubs a same method twice', function () { + + allow('PDO')->toReceive('prepare', 'fetchAll')->andReturn([['name' => 'bob']]); + allow('PDO')->toReceive('prepare', 'execute')->andReturn(true); + $user = new User(); + expect($user->all())->toBe([['name' => 'bob']]); + expect($user->success())->toBe(true); + + }); + + it('allows to mix static/dynamic methods', function () { + + allow('Kahlan\Spec\Fixture\Plugin\Monkey\User')->toReceive('::create', 'all')->andReturn([['name' => 'bob']]); + $user = User::create(); + expect($user->all())->toBe([['name' => 'bob']]); + + }); + + it("throws an exception when trying to stub an instance of a built-in class", function () { + + expect(function () { + allow(new DateTime()); + })->toThrow(new InvalidArgumentException("Can't Stub built-in PHP instances, create a test double using `Double::instance()`.")); + + }); + + }); + + context("with chain of methods and arguments requirements", function () { + + it("stubs on matched arguments", function () { + + $foo = new Foo(); + allow($foo)->toReceive('message')->where(['message' => ['Hello World!']])->andReturn('Good Bye!'); + expect($foo->message('Hello World!'))->toBe('Good Bye!'); + + }); + + it("expects stubbed chain to return the stubbed value when required arguments are matching", function () { + + $foo = new Foo(); + allow($foo)->toReceive('a', 'b', 'c')->where([ + 'a' => [1], 'b' => [2], 'c' => [3] + ])->andReturn('something'); + + $query = $foo->a(1); + $select = $query->b(2); + expect($select->c(3))->toBe('something'); + + }); + + it("expects stubbed chain to not return the stubbed value when required arguments doesn't match", function () { + + $foo = new Foo(); + allow($foo)->toReceive('a', 'b', 'c')->where([ + 'a' => [1], 'b' => [2], 'c' => [3] + ])->andReturn('something'); + + $query = $foo->a(1); + $select = $query->b(2); + expect($select->c(5))->not->toBe('something'); + + }); + + + it("throws an exception when required arguments are applied on a method not present in the chain", function () { + + expect(function () { + $foo = new Foo(); + allow($foo)->toReceive('a')->where(['b' => [2]])->andReturn('something'); + })->toThrow(new InvalidArgumentException("Unexisting `b` as method as part of the chain definition.")); + + }); + + it("throws an exception when required arguments are not an array", function () { + + expect(function () { + $foo = new Foo(); + allow($foo)->toReceive('a')->where(['a' => 2])->andReturn('something'); + })->toThrow(new InvalidArgumentException("Argument requirements must be an arrays for `a` method.")); + + }); + + }); + + }); + + context("with an class", function () { + + it("stubs a method", function () { + + allow('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo') + ->toReceive('message') + ->andReturn('Good Bye!'); + + $foo = new Foo(); + expect($foo->message())->toBe('Good Bye!'); + $foo2 = new Foo(); + expect($foo2->message())->toBe('Good Bye!'); + + }); + + it("stubs a static method", function () { + + allow('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('::messageStatic')->andReturn('Good Bye!'); + expect(Foo::messageStatic())->toBe('Good Bye!'); + + }); + + it("stubs a method using a closure", function () { + + allow('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('message')->andRun(function ($param) { + return $param; + }); + $foo = new Foo(); + expect($foo->message('Good Bye!'))->toBe('Good Bye!'); + + }); + + it("stubs a static method using a closure", function () { + + allow('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('::messageStatic')->andRun(function ($param) { + return $param; + }); + expect(Foo::messageStatic('Good Bye!'))->toBe('Good Bye!'); + + }); + + it("stubs a magic method multiple times", function () { + + allow('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('::magic')->with('hello')->andReturn('world'); + allow('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('::magic')->with('world')->andReturn('hello'); + expect(Foo::magic('hello'))->toBe('world'); + expect(Foo::magic('world'))->toBe('hello'); + + }); + + it("throws an exception when trying to call `toReceive()`", function () { + + expect(function () { + allow('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toBeCalled('magicHello')->andReturn('Hello World!'); + })->toThrow(new Exception("Error `toBeCalled()` are are only available on functions not classes/instances.")); + + }); + + context("with multiple return values", function () { + + it("stubs a method", function () { + + allow('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo') + ->toReceive('message') + ->andReturn('Good Evening World!', 'Good Bye World!'); + + $foo = new Foo(); + expect($foo->message())->toBe('Good Evening World!'); + + $foo2 = new Foo(); + expect($foo2->message())->toBe('Good Bye World!'); + + }); + + }); + + context("with chain of methods", function () { + + it("expects called chain to be called", function () { + + allow('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('::getQuery', '::newQuery', '::from')->andReturn('something'); + $query = Foo::getQuery(); + $select = $query::newQuery(); + expect($select::from())->toBe('something'); + + }); + + }); + + context("with chain of methods and arguments requirements", function () { + + it("expects stubbed chain to return the stubbed value when required arguments are matching", function () { + + allow('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('::getQuery', '::newQuery', '::from')->where([ + '::getQuery' => [1], + '::newQuery' => [2], + '::from' => [3] + ])->andReturn('something'); + + $query = Foo::getQuery(1); + $select = $query::newQuery(2); + expect($select::from(3))->toBe('something'); + + }); + + it("expects stubbed chain to not return the stubbed value when required arguments doesn't match", function () { + + allow('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('::getQuery', '::newQuery', '::from')->where([ + '::getQuery' => [1], + '::newQuery' => [2], + '::from' => [3] + ])->andReturn('something'); + + $query = Foo::getQuery(1); + $select = $query::newQuery(2); + expect($select::from(0))->not->toBe('something'); + + }); + + }); + + it('makes built-in PHP class to work', function () { + + allow('PDO')->toBeOK(); + $user = new User(); + expect($user->all())->toBeTruthy(); + + }); + + }); + + context("with a trait", function () { + + it("stubs a method", function () { + + allow('Kahlan\Spec\Fixture\Plugin\Pointcut\SubBar') + ->toReceive('traitMethod') + ->andReturn('trait method stubbed !'); + + $subBar = new SubBar(); + expect($subBar->traitMethod())->toBe('trait method stubbed !'); + $subBar2 = new SubBar(); + expect($subBar2->traitMethod())->toBe('trait method stubbed !'); + + }); + + }); + + context("with functions", function () { + + it("expects stubbed method to be stubbed as expected", function () { + + $mon = new Mon(); + allow('time')->toBeCalled()->andReturn(123, 456); + expect($mon->time())->toBe(123); + expect($mon->time())->toBe(456); + + }); + + it("expects stubbed method to be stubbed as expected using return closures", function () { + + $mon = new Mon(); + allow('time')->toBeCalled()->andRun(function () { + return 123; + }, function () { + return 456; + }); + expect($mon->time())->toBe(123); + expect($mon->time())->toBe(456); + + }); + + it("expects stubbed method to be stubbed as expected using closures", function () { + + $mon = new Mon(); + allow('time')->toBe(function () { + return 123; + }, function () { + return 456; + }); + expect($mon->time())->toBe(123); + expect($mon->time())->toBe(456); + + }); + + it("expects stubbed method to be stubbed only when the with constraint is respected", function () { + + $mon = new Mon(); + allow('Kahlan\Spec\Fixture\Plugin\Monkey\rand')->toBeCalled()->with(10, 20)->andReturn(40); + expect($mon->rand(0, 10))->toBe(5); + expect($mon->rand(10, 20))->toBe(40); + }); + + it('makes built-in PHP function to work', function () { + + allow('file_get_contents')->toBeOK(); + + $mon = new Mon(); + expect($mon->loadFile())->toBe(null); + + }); + + it("throws an exception when trying to call `toReceive()`", function () { + + expect(function () { + allow('time')->toReceive('something')->andReturn(123, 456); + })->toThrow(new Exception("Error `toReceive()` are only available on classes/instances not functions.")); + + }); + + }); + + it("throws an exception when trying to call `andReturn()` right away", function () { + + expect(function () { + allow('time')->andReturn(123); + })->toThrow(new Exception("You must to call `toReceive()/toBeCalled()` before defining a return value.")); + + }); + + it("throws an exception when trying to call `andReturn()` right away", function () { + + expect(function () { + allow('time')->andRun(function (){}); + })->toThrow(new Exception("You must to call `toReceive()/toBeCalled()` before defining a return value.")); + + }); + +}); diff --git a/spec/Suite/Analysis/DebuggerSpec.php b/spec/Suite/Analysis/Debugger.spec.php similarity index 81% rename from spec/Suite/Analysis/DebuggerSpec.php rename to spec/Suite/Analysis/Debugger.spec.php index aa414ba9..4e723bdb 100644 --- a/spec/Suite/Analysis/DebuggerSpec.php +++ b/spec/Suite/Analysis/Debugger.spec.php @@ -3,21 +3,21 @@ use Exception; use Kahlan\Analysis\Debugger; -use Kahlan\Plugin\Stub; +use Kahlan\Plugin\Double; -describe("Debugger", function() { +describe("Debugger", function () { - beforeEach(function() { + beforeEach(function () { $this->loader = Debugger::loader(); }); - afterEach(function() { + afterEach(function () { Debugger::loader($this->loader); }); - describe("::trace()", function() { + describe("::trace()", function () { - it("returns a default backtrace string", function() { + it("returns a default backtrace string", function () { $backtrace = Debugger::trace(); expect($backtrace)->toBeA('string'); @@ -27,7 +27,7 @@ }); - it("returns a custom backtrace string", function() { + it("returns a custom backtrace string", function () { $backtrace = Debugger::trace(['trace' => debug_backtrace()]); expect($backtrace)->toBeA('string'); @@ -37,7 +37,7 @@ }); - it("returns a backtrace of an Exception", function() { + it("returns a backtrace of an Exception", function () { $backtrace = Debugger::trace(['trace' => new Exception('World Destruction Error!')]); expect($backtrace)->toBeA('string'); @@ -47,7 +47,7 @@ }); - it("returns a trace from eval'd code", function() { + it("returns a trace from eval'd code", function () { $trace = debug_backtrace(); $trace[1]['file'] = "eval()'d code"; @@ -60,20 +60,20 @@ }); - describe("::_line()", function() { + describe("::_line()", function () { - beforeEach(function() { - $this->debugger = Stub::classname([ + beforeEach(function () { + $this->debugger = Double::classname([ 'extends' => 'Kahlan\Analysis\Debugger', 'methods' => ['::line'] ]); - Stub::on($this->debugger)->method('::line', function($trace) { + allow($this->debugger)->toReceive('::line')->andRun(function ($trace) { return static::_line($trace); }); }); - it("returns `null` with non-existing files", function() { + it("returns `null` with non-existing files", function () { $debugger = $this->debugger; @@ -85,14 +85,14 @@ }); - it("returns `null` when a line can't be found", function() { + it("returns `null` when a line can't be found", function () { $debugger = $this->debugger; - $nbline = count(file('spec' . DS . 'Suite' . DS . 'Analysis' . DS . 'DebuggerSpec.php')) + 1; + $nbline = count(file('spec' . DS . 'Suite' . DS . 'Analysis' . DS . 'Debugger.spec.php')) + 1; $trace = [ - 'file' => 'spec' . DS . 'Suite' . DS . 'Analysis' . DS . 'DebuggerSpec.php', + 'file' => 'spec' . DS . 'Suite' . DS . 'Analysis' . DS . 'Debugger.spec.php', 'line' => $nbline + 1 ]; expect($debugger::line($trace))->toBe(null); @@ -103,16 +103,16 @@ }); - describe("::message()", function() { + describe("::message()", function () { - it("formats an exception as a string message", function() { + it("formats an exception as a string message", function () { $message = Debugger::message(new Exception('World Destruction Error!')); expect($message)->toBe('`Exception` Code(0): World Destruction Error!'); }); - it("formats a backtrace array as a string message", function() { + it("formats a backtrace array as a string message", function () { $backtrace = [ 'message' => 'E_ERROR Error!', @@ -134,20 +134,20 @@ }); - describe("::loader()", function() { + describe("::loader()", function () { - it("gets/sets a loader", function() { + it("gets/sets a loader", function () { - $loader = Stub::create(); + $loader = Double::instance(); expect(Debugger::loader($loader))->toBe($loader); }); }); - describe("::errorType()", function() { + describe("::errorType()", function () { - it("returns some reader-friendly error type string", function() { + it("returns some reader-friendly error type string", function () { expect(Debugger::errorType(E_ERROR))->toBe('E_ERROR'); expect(Debugger::errorType(E_WARNING))->toBe('E_WARNING'); @@ -169,7 +169,7 @@ }); - it("returns for undefined error type", function() { + it("returns for undefined error type", function () { expect(Debugger::errorType(123456))->toBe(''); diff --git a/spec/Suite/Analysis/InspectorSpec.php b/spec/Suite/Analysis/Inspector.spec.php similarity index 70% rename from spec/Suite/Analysis/InspectorSpec.php rename to spec/Suite/Analysis/Inspector.spec.php index c26d89cb..2f90adf1 100644 --- a/spec/Suite/Analysis/InspectorSpec.php +++ b/spec/Suite/Analysis/Inspector.spec.php @@ -3,15 +3,32 @@ use Kahlan\Analysis\Inspector; -describe("Inspector", function() { - - before(function() { +class Parameter +{ + protected $_parameter; + public function __construct($parameter) + { + $this->_parameter = $parameter; + } + public function getClass() + { + return false; + } + public function __toString() + { + return $this->_parameter; + } +} + +describe("Inspector", function () { + + beforeEach(function () { $this->class = 'Kahlan\Spec\Fixture\Analysis\SampleClass'; }); - describe('::inspect()', function() { + describe('::inspect()', function () { - it("gets the reflexion layer of a class", function() { + it("gets the reflexion layer of a class", function () { $inspector = Inspector::inspect($this->class); expect($inspector)->toBeAnInstanceOf('ReflectionClass'); @@ -21,9 +38,9 @@ }); - describe('::parameters()', function() { + describe('::parameters()', function () { - it("gets method's parameters details", function() { + it("gets method's parameters details", function () { $inspector = Inspector::parameters($this->class, 'parametersExample'); expect($inspector)->toBeA('array'); @@ -45,7 +62,7 @@ }); - it("merges defauts values with populated values when the third argument is not empty", function() { + it("merges defauts values with populated values when the third argument is not empty", function () { $inspector = Inspector::parameters($this->class, 'parametersExample', [ 'first', @@ -64,9 +81,9 @@ }); - describe("::typehint()", function() { + describe("::typehint()", function () { - it("returns an empty string when no typehint is present", function() { + it("returns an empty string when no typehint is present", function () { $inspector = Inspector::parameters($this->class, 'parametersExample'); expect(Inspector::typehint($inspector[0]))->toBe(''); @@ -76,7 +93,7 @@ }); - it("returns parameter typehint", function() { + it("returns parameter typehint", function () { $inspector = Inspector::parameters($this->class, 'exceptionTypeHint'); $typehint = Inspector::typehint(current($inspector)); @@ -95,22 +112,26 @@ }); - it("returns parameter typehint for scalar type hints", function() { - - skipIf(PHP_MAJOR_VERSION < 7); + it("returns parameter typehint for scalar type hints", function () { - $inspector = Inspector::parameters('Kahlan\Spec\Fixture\Analysis\ScalarTypeHintsClass', 'intTypeHint'); - $typehint = Inspector::typehint(current($inspector)); + $typehint = Inspector::typehint(new Parameter('Parameter #0 [ integer $values ]')); expect($typehint)->toBeA('string'); expect($typehint)->toBe('int'); - $inspector = Inspector::parameters('Kahlan\Spec\Fixture\Analysis\ScalarTypeHintsClass', 'boolTypeHint'); - $typehint = Inspector::typehint(current($inspector)); + $typehint = Inspector::typehint(new Parameter('Parameter #0 [ boolean $values ]')); expect($typehint)->toBeA('string'); expect($typehint)->toBe('bool'); }); + it("returns empty typehint for HHVM `mixed` type hint", function () { + + $typehint = Inspector::typehint(new Parameter('Parameter #0 [ mixed $values ]')); + expect($typehint)->toBeA('string'); + expect($typehint)->toBe(''); + + }); + }); }); diff --git a/spec/Suite/ArgSpec.php b/spec/Suite/Arg.spec.php similarity index 67% rename from spec/Suite/ArgSpec.php rename to spec/Suite/Arg.spec.php index ba9c3a14..87254adf 100644 --- a/spec/Suite/ArgSpec.php +++ b/spec/Suite/Arg.spec.php @@ -7,15 +7,15 @@ use SplMaxHeap; use Kahlan\Arg; use Kahlan\Matcher; -use Kahlan\Plugin\Stub; +use Kahlan\Plugin\Double; -describe("Arg", function() { +describe("Arg", function () { - beforeEach(function() { + beforeEach(function () { $this->matchers = Matcher::get(); }); - afterEach(function() { + afterEach(function () { Matcher::reset(); foreach ($this->matchers as $name => $value) { foreach ($value as $for => $class) { @@ -24,9 +24,9 @@ } }); - describe("::__callStatic()", function() { + describe("::__callStatic()", function () { - it("creates matcher", function() { + it("creates matcher", function () { $arg = Arg::toBe(true); expect($arg->match(true))->toBe(true); @@ -34,7 +34,7 @@ }); - it("creates a negative matcher", function() { + it("creates a negative matcher", function () { $arg = Arg::notToBe(true); expect($arg->match(true))->not->toBe(true); @@ -42,9 +42,9 @@ }); - it("registers a matcher for a specific class", function() { + it("registers a matcher for a specific class", function () { - Matcher::register('toEqualCustom', Stub::classname(['extends' => 'Kahlan\Matcher\ToEqual']), 'stdClass'); + Matcher::register('toEqualCustom', Double::classname(['extends' => 'Kahlan\Matcher\ToEqual']), 'stdClass'); $arg = Arg::toEqualCustom(new stdClass()); expect($arg->match(new stdClass()))->toBe(true); @@ -54,29 +54,29 @@ }); - it("makes registered matchers for a specific class available for sub classes", function() { + it("makes registered matchers for a specific class available for sub classes", function () { - Matcher::register('toEqualCustom', Stub::classname(['extends' => 'Kahlan\Matcher\ToEqual']), 'SplHeap'); + Matcher::register('toEqualCustom', Double::classname(['extends' => 'Kahlan\Matcher\ToEqual']), 'SplHeap'); $arg = Arg::toEqualCustom(new SplMaxHeap()); expect($arg->match(new SplMaxHeap()))->toBe(true); }); - it("throws an exception using an undefined matcher name", function() { + it("throws an exception using an undefined matcher name", function () { - $closure = function() { + $closure = function () { $arg = Arg::toHelloWorld(true); }; expect($closure)->toThrow(new Exception("Unexisting matchers attached to `'toHelloWorld'`.")); }); - it("throws an exception using an matcher name which doesn't match actual", function() { + it("throws an exception using an matcher name which doesn't match actual", function () { - Matcher::register('toEqualCustom', Stub::classname(['extends' => 'Kahlan\Matcher\ToEqual']), 'SplHeap'); + Matcher::register('toEqualCustom', Double::classname(['extends' => 'Kahlan\Matcher\ToEqual']), 'SplHeap'); - $closure = function() { + $closure = function () { $arg = Arg::toEqualCustom(new SplMaxHeap()); $arg->match(true); }; @@ -86,4 +86,4 @@ }); -}); \ No newline at end of file +}); diff --git a/spec/Suite/Box/BoxSpec.php b/spec/Suite/Box/Box.spec.php similarity index 71% rename from spec/Suite/Box/BoxSpec.php rename to spec/Suite/Box/Box.spec.php index 2b6ee8fc..c306a15b 100644 --- a/spec/Suite/Box/BoxSpec.php +++ b/spec/Suite/Box/Box.spec.php @@ -15,31 +15,35 @@ public function __construct() } } -describe("Box", function() { +describe("Box", function () { - describe("->factory()", function() { + describe("->factory()", function () { - beforeEach(function() { + beforeEach(function () { $this->box = new Box(); }); - it("binds a closure", function() { + it("binds a closure", function () { - $this->box->factory('spec.stdClass', function() { return new stdClass; }); + $this->box->factory('spec.stdClass', function () { + return new stdClass; + }); expect($this->box->get('spec.stdClass'))->toBeAnInstanceOf("stdClass"); }); - it("binds a classname", function() { + it("binds a classname", function () { $this->box->factory('spec.stdClass', "stdClass"); expect($this->box->get('spec.stdClass'))->toBeAnInstanceOf("stdClass"); }); - it("passes all arguments to the Closure", function() { + it("passes all arguments to the Closure", function () { - $this->box->factory('spec.arguments', function() { return func_get_args(); }); + $this->box->factory('spec.arguments', function () { + return func_get_args(); + }); $params = [ 'params1', 'params2' @@ -48,7 +52,7 @@ public function __construct() }); - it("passes all arguments to the constructor", function() { + it("passes all arguments to the constructor", function () { $this->box->factory('spec.arguments', 'box\spec\suite\MyTestClass'); $params = [ @@ -59,7 +63,7 @@ public function __construct() }); - it("creates different instances", function() { + it("creates different instances", function () { $this->box->factory('spec.stdClass', "stdClass"); @@ -69,16 +73,16 @@ public function __construct() }); - it("throws an exception if the definition is not a string or a Closure", function() { + it("throws an exception if the definition is not a string or a Closure", function () { $expected = new BoxException("Error `spec.instance` is not a closure definition dependency can't use it as a factory definition."); - $closure = function(){ + $closure = function () { $this->box->factory('spec.instance', new stdClass); }; expect($closure)->toThrow($expected); - $closure = function(){ + $closure = function () { $this->box->factory('spec.instance', []); }; expect($closure)->toThrow($expected); @@ -87,20 +91,20 @@ public function __construct() }); - describe("->service()", function() { + describe("->service()", function () { - beforeEach(function() { + beforeEach(function () { $this->box = new Box(); }); - it("shares a string", function() { + it("shares a string", function () { $this->box->service('spec.stdClass', "stdClass"); expect($this->box->get('spec.stdClass'))->toBe("stdClass"); }); - it("shares an instance", function() { + it("shares an instance", function () { $instance = new stdClass; $this->box->service('spec.instance', $instance); @@ -108,7 +112,7 @@ public function __construct() }); - it("gets the same instance", function() { + it("gets the same instance", function () { $this->box->service('spec.stdClass', new stdClass); $instance1 = $this->box->get('spec.stdClass'); @@ -117,9 +121,11 @@ public function __construct() }); - it("shares a singleton using the closure syntax", function() { + it("shares a singleton using the closure syntax", function () { - $this->box->service('spec.stdClass', function() { return new stdClass; }); + $this->box->service('spec.stdClass', function () { + return new stdClass; + }); $instance1 = $this->box->get('spec.stdClass'); $instance2 = $this->box->get('spec.stdClass'); expect($instance1)->toBe($instance2); @@ -127,12 +133,14 @@ public function __construct() }); - it("shares a closure", function() { + it("shares a closure", function () { - $closure = function() { + $closure = function () { return "Hello World!"; }; - $this->box->service('spec.closure', function() use ($closure) { return $closure; }); + $this->box->service('spec.closure', function () use ($closure) { + return $closure; + }); $closure1 = $this->box->get('spec.closure'); $closure2 = $this->box->get('spec.closure'); @@ -144,36 +152,38 @@ public function __construct() }); - describe("has", function() { + describe("has", function () { - beforeEach(function() { + beforeEach(function () { $this->box = new Box(); }); - it("returns `false` if the Box is empty", function() { + it("returns `false` if the Box is empty", function () { expect($this->box->has('spec.hello'))->toBe(false); }); - it("returns `true` if the Box contain the bind dependency", function() { - $this->box->factory('spec.stdClass', function() { return new stdClass; }); + it("returns `true` if the Box contain the bind dependency", function () { + $this->box->factory('spec.stdClass', function () { + return new stdClass; + }); expect($this->box->has('spec.stdClass'))->toBe(true); }); - it("returns `true` if the Box contain the share dependency", function() { + it("returns `true` if the Box contain the share dependency", function () { $this->box->service('spec.hello', "Hello World!"); expect($this->box->has('spec.hello'))->toBe(true); }); }); - describe("->get()", function() { + describe("->get()", function () { - beforeEach(function() { + beforeEach(function () { $this->box = new Box(); }); - it("throws an exception if the dependency doesn't exists", function() { + it("throws an exception if the dependency doesn't exists", function () { - $closure = function(){ + $closure = function () { $this->box->get('spec.stdUnexistingClass'); }; expect($closure)->toThrow(new BoxException("Unexisting `spec.stdUnexistingClass` definition dependency.")); @@ -181,15 +191,17 @@ public function __construct() }); }); - describe("->wrap()", function() { + describe("->wrap()", function () { - beforeEach(function() { + beforeEach(function () { $this->box = new Box(); }); - it("returns a dependency container", function() { + it("returns a dependency container", function () { - $this->box->factory('spec.stdClass', function() { return new stdClass; }); + $this->box->factory('spec.stdClass', function () { + return new stdClass; + }); $wrapper = $this->box->wrap('spec.stdClass'); expect($wrapper)->toBeAnInstanceOf('Kahlan\Box\Wrapper'); @@ -200,9 +212,9 @@ public function __construct() }); - it("throws an exception if the dependency definition is not a closure doesn't exists", function() { + it("throws an exception if the dependency definition is not a closure doesn't exists", function () { - $closure = function() { + $closure = function () { $this->box->service('spec.stdClass', new stdClass); $wrapper = $this->box->wrap('spec.stdClass'); }; @@ -211,9 +223,9 @@ public function __construct() }); - it("throws an exception if the dependency doesn't exists", function() { + it("throws an exception if the dependency doesn't exists", function () { - $closure = function() { + $closure = function () { $this->box->wrap('spec.stdUnexistingClass'); }; @@ -222,15 +234,17 @@ public function __construct() }); }); - describe("->remove()", function() { + describe("->remove()", function () { - beforeEach(function() { + beforeEach(function () { $this->box = new Box(); }); - it("remove a bind", function() { + it("remove a bind", function () { - $this->box->factory('spec.stdClass', function() { return new stdClass; }); + $this->box->factory('spec.stdClass', function () { + return new stdClass; + }); expect($this->box->has('spec.stdClass'))->toBe(true); $this->box->remove('spec.stdClass'); @@ -240,13 +254,13 @@ public function __construct() }); - describe("->clear()", function() { + describe("->clear()", function () { - beforeEach(function() { + beforeEach(function () { $this->box = new Box(); }); - it("clears all binds & shares", function() { + it("clears all binds & shares", function () { $this->box->factory('spec.stdClass', "stdClass"); $this->box->service('spec.hello', "Hello World!"); @@ -257,12 +271,12 @@ public function __construct() expect($this->box->has('spec.stdClass'))->toBe(false); expect($this->box->has('spec.hello'))->toBe(false); - $closure = function() { + $closure = function () { $this->box->get('spec.stdClass'); }; expect($closure)->toThrow(new BoxException("Unexisting `spec.stdClass` definition dependency.")); - $closure = function() { + $closure = function () { $this->box->get('spec.hello'); }; expect($closure)->toThrow(new BoxException("Unexisting `spec.hello` definition dependency.")); @@ -273,13 +287,13 @@ public function __construct() }); -describe("box()", function() { +describe("box()", function () { - beforeEach(function() { + beforeEach(function () { box(false); }); - it("adds a box", function() { + it("adds a box", function () { $box = new Box(); $actual = box('box.spec', $box); @@ -287,7 +301,7 @@ public function __construct() expect($actual)->toBe($box); }); - it("gets a box", function() { + it("gets a box", function () { $box = new Box(); box('box.spec', $box); @@ -296,7 +310,7 @@ public function __construct() expect($actual)->toBe($box); }); - it("adds a default box", function() { + it("adds a default box", function () { $box = new Box(); @@ -305,7 +319,7 @@ public function __construct() }); - it("gets a default box", function() { + it("gets a default box", function () { $box = box(); expect($box)->toBeAnInstanceOf('Kahlan\Box\Box'); @@ -313,38 +327,38 @@ public function __construct() }); - it("removes a box", function() { + it("removes a box", function () { $box = new Box(); box('box.spec', $box); box('box.spec', false); - $closure = function() { + $closure = function () { box('box.spec'); }; expect($closure)->toThrow(new BoxException("Unexisting box `'box.spec'`.")); }); - it("removes all boxes", function() { + it("removes all boxes", function () { $box = new Box(); box('box.spec1', $box); box('box.spec2', $box); box(false); - $closure = function() { + $closure = function () { box('box.spec1'); }; expect($closure)->toThrow(new BoxException("Unexisting box `'box.spec1'`.")); - $closure = function() { + $closure = function () { box('box.spec2'); }; expect($closure)->toThrow(new BoxException("Unexisting box `'box.spec2'`.")); }); - it("throws an exception when trying to get an unexisting box", function() { - $closure = function() { + it("throws an exception when trying to get an unexisting box", function () { + $closure = function () { box('box.spec'); }; expect($closure)->toThrow(new BoxException("Unexisting box `'box.spec'`.")); diff --git a/spec/Suite/Box/WrapperSpec.php b/spec/Suite/Box/Wrapper.spec.php similarity index 67% rename from spec/Suite/Box/WrapperSpec.php rename to spec/Suite/Box/Wrapper.spec.php index 62c4aa31..3302de68 100644 --- a/spec/Suite/Box/WrapperSpec.php +++ b/spec/Suite/Box/Wrapper.spec.php @@ -8,17 +8,17 @@ use Kahlan\Box\Wrapper; use Kahlan\Box\BoxException; -describe("Wrapper", function() { +describe("Wrapper", function () { - beforeEach(function() { + beforeEach(function () { $this->box = new Box(); }); - describe("->__construct()", function() { + describe("->__construct()", function () { - it("throws an exception if the `'box'` parameter is empty", function() { + it("throws an exception if the `'box'` parameter is empty", function () { - $closure = function(){ + $closure = function () { $wrapper = new Wrapper(['box' => null, 'name' => 'spec.stdClass']); }; @@ -26,11 +26,13 @@ }); - it("throws an exception if the `'name'` parameter is empty", function() { + it("throws an exception if the `'name'` parameter is empty", function () { - $this->box->factory('spec.stdClass', function() { return new stdClass; }); + $this->box->factory('spec.stdClass', function () { + return new stdClass; + }); - $closure = function(){ + $closure = function () { $wrapper = new Wrapper(['box' => $this->box, 'name' => '']); }; @@ -40,11 +42,13 @@ }); - describe("->get()", function() { + describe("->get()", function () { - it("resolve a dependency", function() { + it("resolve a dependency", function () { - $this->box->factory('spec.stdClass', function() { return new stdClass; }); + $this->box->factory('spec.stdClass', function () { + return new stdClass; + }); $wrapper = new Wrapper(['box' => $this->box, 'name' => 'spec.stdClass']); $dependency = $wrapper->get(); @@ -54,16 +58,20 @@ }); - it("throws an exception if the dependency doesn't exists", function() { + it("throws an exception if the dependency doesn't exists", function () { $wrapper = new Wrapper(['box' => $this->box, 'name' => 'spec.stdUnexistingClass']); - expect(function() use ($wrapper) { $wrapper->get(); })->toThrow(new BoxException()); + expect(function () use ($wrapper) { + $wrapper->get(); + })->toThrow(new BoxException()); }); - it("passes parameters to the Closure", function() { + it("passes parameters to the Closure", function () { - $this->box->factory('spec.arguments', function() { return func_get_args(); }); + $this->box->factory('spec.arguments', function () { + return func_get_args(); + }); $params = [ 'param1', 'param2' @@ -77,9 +85,11 @@ }); - it("override passed parameters to the Closure", function() { + it("override passed parameters to the Closure", function () { - $this->box->factory('spec.arguments', function() { return func_get_args(); }); + $this->box->factory('spec.arguments', function () { + return func_get_args(); + }); $params = [ 'param1', 'param2' @@ -99,4 +109,4 @@ }); -}); \ No newline at end of file +}); diff --git a/spec/Suite/Cli/ArgsSpec.php b/spec/Suite/Cli/ArgsSpec.php deleted file mode 100644 index 4557b3b0..00000000 --- a/spec/Suite/Cli/ArgsSpec.php +++ /dev/null @@ -1,348 +0,0 @@ -argument()", function() { - - it("sets an argument config", function() { - - $args = new Args(); - $args->argument('argument1', ['type' => 'boolean']); - expect($args->argument('argument1'))->toEqual([ - 'type' => 'boolean', - 'array' => false, - 'value' => null, - 'default' => null - ]); - - $arguments = $args->arguments(); - expect($arguments)->toBeAn('array'); - expect(isset($arguments['argument1']))->toBe(true); - expect($arguments['argument1'])->toEqual([ - 'type' => 'boolean', - 'array' => false, - 'value' => null, - 'default' => null - ]); - - }); - - it("gets the default config", function() { - - $args = new Args(); - expect($args->argument('argument1'))->toEqual([ - 'type' => 'string', - 'array' => false, - 'value' => null, - 'default' => null - ]); - - }); - - it("sets/updates an attribute of an argument", function() { - - $args = new Args(); - $args->argument('argument1', ['type' => 'boolean']); - expect($args->argument('argument1'))->toEqual([ - 'type' => 'boolean', - 'array' => false, - 'value' => null, - 'default' => null - ]); - - $args->argument('argument1', 'default', 'value1'); - expect($args->argument('argument1'))->toEqual([ - 'type' => 'boolean', - 'array' => false, - 'value' => null, - 'default' => 'value1' - ]); - - }); - - }); - - describe("->parse()", function() { - - it("parses command line arguments", function() { - - $args = new Args(); - $actual = $args->parse([ - 'command', '--argument1', '--argument3=value3', '--', '--ingored' - ]); - expect($actual)->toEqual([ - 'argument1' => '', - 'argument3' => 'value3' - ]); - - }); - - it("parses command line arguments with dashed names", function() { - - $args = new Args([ - 'double-dashed-argument' => ['type' => 'boolean'] - ]); - $actual = $args->parse([ - 'command', '--dashed-argument=value', '--double-dashed-argument' - ]); - expect($actual)->toEqual([ - 'dashed-argument' => 'value', - 'double-dashed-argument' => true - ]); - - }); - - it("provides an array when some multiple occurences of a same argument are present", function() { - - $args = new Args(['argument1' => ['array' => true]]); - $actual = $args->parse([ - 'command', '--argument1', '--argument1=value1' , '--argument1=value2' - ]); - expect($actual)->toEqual([ - 'argument1' => [ - '', - 'value1', - 'value2' - ] - ]); - - }); - - it("casts booleans", function() { - - $args = new Args([ - 'argument1' => ['type' => 'boolean'], - 'argument2' => ['type' => 'boolean'], - 'argument3' => ['type' => 'boolean'], - 'argument4' => ['type' => 'boolean'], - 'argument5' => ['type' => 'boolean'] - ]); - $actual = $args->parse([ - 'command', '--argument1', '--argument2=true' , '--argument3=false', '--argument4=0' - ]); - expect($actual)->toEqual([ - 'argument1' => true, - 'argument2' => true, - 'argument3' => false, - 'argument4' => false - ]); - - expect($args->get('argument5'))->toBe(false); - - }); - - it("casts integers", function() { - - $args = new Args([ - 'argument' => ['type' => 'numeric'], - 'argument0' => ['type' => 'numeric'], - 'argument1' => ['type' => 'numeric'], - 'argument2' => ['type' => 'numeric'] - ]); - $actual = $args->parse([ - 'command', '--argument', '--argument0=0', '--argument1=1', '--argument2=2' - ]); - expect($actual)->toEqual([ - 'argument' => 1, - 'argument0' => 0, - 'argument1' => 1, - 'argument2' => 2 - ]); - - }); - - it("casts string", function() { - - $args = new Args([ - 'argument1' => ['type' => 'string'], - 'argument2' => ['type' => 'string'], - 'argument3' => ['type' => 'string'], - 'argument4' => ['type' => 'string'], - 'argument5' => ['type' => 'string'] - ]); - $actual = $args->parse([ - 'command', '--argument1', '--argument2=' , '--argument3=value' - ]); - expect($actual)->toEqual([ - 'argument1' => null, - 'argument2' => '', - 'argument3' => 'value' - ]); - - expect($args->get('argument5'))->toBe(null); - - }); - - context("with defaults arguments", function() { - - it("allows boolean casting", function() { - - $args = new Args([ - 'argument1' => ['type' => 'boolean', 'default' => true], - 'argument2' => ['type' => 'boolean', 'default' => false], - 'argument3' => ['type' => 'boolean', 'default' => true], - 'argument4' => ['type' => 'boolean', 'default' => false] - ]); - - $actual = $args->parse([ - 'command', '--argument1', '--argument2' - ]); - expect($actual)->toEqual([ - 'argument1' => true, - 'argument2' => true, - 'argument3' => true, - 'argument4' => false - ]); - - }); - - }); - - context("with override set to `false`", function() { - - it("doesn't override existing arguments when the override params is set to `false`", function() { - - $args = new Args(); - $args->set('argument1', 'value1'); - $actual = $args->parse(['--argument1=valueX']); - expect($actual)->toBe(['argument1' => 'valueX']); - - $args = new Args(); - $args->set('argument1', 'value1'); - $actual = $args->parse(['--argument1=valueX'], false); - expect($actual)->toBe(['argument1' => 'value1']); - - }); - - }); - - }); - - describe("->get()", function() { - - it("ignores argument value if the value option is set", function() { - - $args = new Args(['argument1' => [ - 'type' => 'string', - 'value' => 'config_value' - ]]); - - $actual = $args->parse(['command']); - expect($args->get('argument1'))->toEqual('config_value'); - - $actual = $args->parse(['command', '--argument1']); - expect($args->get('argument1'))->toEqual('config_value'); - - $actual = $args->parse(['command', '--argument1="some_value"']); - expect($args->get('argument1'))->toEqual('config_value'); - - }); - - it("formats value according to value function", function() { - - $args = new Args(['argument1' => [ - 'type' => 'string', - 'default' => 'default_value', - 'value' => function($value, $name, $args) { - if (!$value) { - return 'empty_value'; - } - return 'non_empty_value'; - } - ]]); - - $actual = $args->parse(['command']); - expect($args->get('argument1'))->toEqual('default_value'); - - $actual = $args->parse(['command', '--argument1']); - expect($args->get('argument1'))->toEqual('empty_value'); - - $actual = $args->parse(['command', '--argument1="some_value"']); - expect($args->get('argument1'))->toEqual('non_empty_value'); - - }); - - }); - - describe("->exists()", function() { - - it("returns `true` if the argument exists", function() { - - $args = new Args(); - $actual = $args->parse([ - 'command', '--argument1', '--argument2=true' , '--argument3=false', '--argument4=0' - ]); - expect($args->exists('argument1'))->toBe(true); - expect($args->exists('argument2'))->toBe(true); - expect($args->exists('argument3'))->toBe(true); - expect($args->exists('argument4'))->toBe(true); - expect($args->exists('argument5'))->toBe(false); - - }); - - it("returns `true` if the argument as a default value", function() { - - $args = new Args(); - $args->argument('argument1', ['type' => 'boolean']); - $args->argument('argument2', ['type' => 'boolean', 'default' => false]); - - expect($args->exists('argument1'))->toBe(false); - expect($args->exists('argument2'))->toBe(true); - - }); - - }); - - describe("->cast()", function() { - - it("casts array", function() { - - $args = new Args(); - $cast = $args->cast(["some", "string", "and", 10], "string"); - expect($cast)->toBeAn('array'); - foreach($cast as $c) { - expect($c)->toBeA('string'); - } - - }); - - it("casts boolean", function() { - - $args = new Args(); - $cast = $args->cast(["true", "false", "some_string", null, 10], "boolean"); - expect($cast)->toBeAn('array'); - expect(count($cast))->toBe(5); - list($bTrue, $bFalse, $string, $null, $number) = $cast; - expect($bTrue)->toBeA('boolean')->toBe(true); - expect($bFalse)->toBeA('boolean')->toBe(false); - expect($string)->toBeA('boolean')->toBe(true); - expect($null)->toBeA('boolean')->toBe(false); - expect($number)->toBeA('boolean')->toBe(true); - - }); - - it("casts numeric", function() { - - $args = new Args(); - $cast = $args->cast([true, "false", "some_string", null, 10], "numeric"); - expect($cast)->toBeAn('array'); - expect(count($cast))->toBe(5); - expect(implode($cast))->toBe("100110"); - - }); - - it("casts value into array", function() { - - $args = new Args(); - $cast = $args->cast("string", "string", true); - expect($cast)->toBeA("array"); - expect($cast)->toContain("string"); - - }); - - }); - -}); diff --git a/spec/Suite/Cli/CliSpec.php b/spec/Suite/Cli/Cli.spec.php similarity index 73% rename from spec/Suite/Cli/CliSpec.php rename to spec/Suite/Cli/Cli.spec.php index 7bc83a4a..e5b74ff1 100644 --- a/spec/Suite/Cli/CliSpec.php +++ b/spec/Suite/Cli/Cli.spec.php @@ -3,36 +3,38 @@ use Kahlan\Cli\Cli; -describe("Cli", function() { +describe("Cli", function () { - describe('->color()', function() { + describe('->color()', function () { - before(function() { - $this->check = function($actual, $expected) { + beforeAll(function () { + $this->check = function ($actual, $expected) { expect(strlen($actual))->toBe(strlen($expected)); for ($i=0; $i < strlen($actual); $i++) { $check = (ord($actual[$i]) == ord($expected[$i])) ? true : false; - if ($check) break; + if ($check) { + break; + } } expect($check)->toBe(true); }; }); - it("leaves string unchanged whith no options", function() { + it("leaves string unchanged whith no options", function () { expect(Cli::color("String"))->toBe("String"); }); - it("applies a color using a string as options", function() { + it("applies a color using a string as options", function () { $this->check(Cli::color("String", "yellow"), "\e[0;33;49mSrting\e[0m"); }); - it("applies a complex color using a semicolon separated string as options", function() { + it("applies a complex color using a semicolon separated string as options", function () { $this->check(Cli::color("String", "n;yellow;100"), "\e[0;33;110mSrting\e[0m"); $this->check(Cli::color("String", "4;red;100"), "\e[0;31;110mSrting\e[0m"); @@ -41,7 +43,7 @@ }); - it("applies the 39 default color with unknown color name", function() { + it("applies the 39 default color with unknown color name", function () { $this->check(Cli::color("String", "some_strange_color"), "\e[0;39;49mSrting\e[0m"); @@ -49,11 +51,11 @@ }); - describe('->bell()', function() { + describe('->bell()', function () { - it("bells", function() { + it("bells", function () { - expect(function() { + expect(function () { Cli::bell(2); })->toEcho(str_repeat("\007", 2)); diff --git a/spec/Suite/Cli/CommandLine.spec.php b/spec/Suite/Cli/CommandLine.spec.php new file mode 100644 index 00000000..7ff295bf --- /dev/null +++ b/spec/Suite/Cli/CommandLine.spec.php @@ -0,0 +1,424 @@ +option", function () { + + it("sets an option config", function () { + + $commandLine = new CommandLine(); + $commandLine->option('option1', ['type' => 'boolean']); + expect($commandLine->option('option1'))->toEqual([ + 'type' => 'boolean', + 'group' => false, + 'array' => false, + 'value' => null, + 'default' => null + ]); + + $options = $commandLine->options(); + expect($options)->toBeAn('array'); + expect(isset($options['option1']))->toBe(true); + expect($options['option1'])->toEqual([ + 'type' => 'boolean', + 'group' => false, + 'array' => false, + 'value' => null, + 'default' => null + ]); + + }); + + it("gets the default config", function () { + + $commandLine = new CommandLine(); + expect($commandLine->option('option1'))->toEqual([ + 'type' => 'string', + 'group' => false, + 'array' => false, + 'value' => null, + 'default' => null + ]); + + }); + + it("sets/updates an attribute of an option", function () { + + $commandLine = new CommandLine(); + $commandLine->option('option1', ['type' => 'boolean']); + expect($commandLine->option('option1'))->toEqual([ + 'type' => 'boolean', + 'group' => false, + 'array' => false, + 'value' => null, + 'default' => null + ]); + + $commandLine->option('option1', 'default', 'value1'); + expect($commandLine->option('option1'))->toEqual([ + 'type' => 'boolean', + 'group' => false, + 'array' => false, + 'value' => null, + 'default' => 'value1' + ]); + + }); + + }); + + describe("->parse()", function () { + + it("parses command line options", function () { + + $commandLine = new CommandLine(); + $actual = $commandLine->parse([ + 'command', '--option1', '--option3=value3', '--', '--ingored' + ]); + expect($actual)->toEqual([ + 'option1' => '', + 'option3' => 'value3' + ]); + + }); + + it("parses command line options with dashed names", function () { + + $commandLine = new CommandLine([ + 'double-dashed-option' => ['type' => 'boolean'] + ]); + $actual = $commandLine->parse([ + 'command', '--dashed-option=value', '--double-dashed-option' + ]); + expect($actual)->toEqual([ + 'dashed-option' => 'value', + 'double-dashed-option' => true + ]); + + }); + + it("provides an array when some multiple occurences of a same option are present", function () { + + $commandLine = new CommandLine(['option1' => ['array' => true]]); + $actual = $commandLine->parse([ + 'command', '--option1', '--option1=value1' , '--option1=value2' + ]); + expect($actual)->toEqual([ + 'option1' => [ + '', + 'value1', + 'value2' + ] + ]); + + }); + + it("casts booleans", function () { + + $commandLine = new CommandLine([ + 'option1' => ['type' => 'boolean'], + 'option2' => ['type' => 'boolean'], + 'option3' => ['type' => 'boolean'], + 'option4' => ['type' => 'boolean'], + 'option5' => ['type' => 'boolean'] + ]); + $actual = $commandLine->parse([ + 'command', '--option1', '--option2=true' , '--option3=false', '--option4=0' + ]); + expect($actual)->toEqual([ + 'option1' => true, + 'option2' => true, + 'option3' => false, + 'option4' => false + ]); + + expect($commandLine->get('option5'))->toBe(false); + + }); + + it("casts integers", function () { + + $commandLine = new CommandLine([ + 'option' => ['type' => 'numeric'], + 'option0' => ['type' => 'numeric'], + 'option1' => ['type' => 'numeric'], + 'option2' => ['type' => 'numeric'] + ]); + $actual = $commandLine->parse([ + 'command', '--option', '--option0=0', '--option1=1', '--option2=2' + ]); + expect($actual)->toEqual([ + 'option' => 1, + 'option0' => 0, + 'option1' => 1, + 'option2' => 2 + ]); + + }); + + it("casts string", function () { + + $commandLine = new CommandLine([ + 'option1' => ['type' => 'string'], + 'option2' => ['type' => 'string'], + 'option3' => ['type' => 'string'], + 'option4' => ['type' => 'string'], + 'option5' => ['type' => 'string'] + ]); + $actual = $commandLine->parse([ + 'command', '--option1', '--option2=' , '--option3=value' + ]); + expect($actual)->toEqual([ + 'option1' => null, + 'option2' => '', + 'option3' => 'value' + ]); + + expect($commandLine->get('option5'))->toBe(null); + + }); + + it("provides an array when some multiple occurences of a same option are present", function () { + + $commandLine = new CommandLine([ + 'option1:sub1' => ['array' => true], + 'option1:sub3' => ['type' => 'boolean', 'default' => true], + ]); + $actual = $commandLine->parse([ + 'command', '--option1:sub1', '--option1:sub1=value1', '--option1:sub1=value2', '--option1:sub2=value3' + ]); + expect($actual)->toEqual([ + 'option1' => [ + 'sub3' => true, + 'sub1' => [ + null, + 'value1', + 'value2' + ], + 'sub2' => 'value3' + ] + ]); + + }); + + context("with defaults options", function () { + + it("allows boolean casting", function () { + + $commandLine = new CommandLine([ + 'option1' => ['type' => 'boolean', 'default' => true], + 'option2' => ['type' => 'boolean', 'default' => false], + 'option3' => ['type' => 'boolean', 'default' => true], + 'option4' => ['type' => 'boolean', 'default' => false] + ]); + + $actual = $commandLine->parse([ + 'command', '--option1', '--option2' + ]); + expect($actual)->toEqual([ + 'option1' => true, + 'option2' => true, + 'option3' => true, + 'option4' => false + ]); + + }); + + }); + + context("with override set to `false`", function () { + + it("doesn't override existing options when the override params is set to `false`", function () { + + $commandLine = new CommandLine(); + $commandLine->set('option1', 'value1'); + $actual = $commandLine->parse(['--option1=valueX']); + expect($actual)->toBe(['option1' => 'valueX']); + + $commandLine = new CommandLine(); + $commandLine->set('option1', 'value1'); + $actual = $commandLine->parse(['--option1=valueX'], false); + expect($actual)->toBe(['option1' => 'value1']); + + }); + + }); + + }); + + describe("->get()", function () { + + it("ignores option value if the value option is set", function () { + + $commandLine = new CommandLine(['option1' => [ + 'type' => 'string', + 'value' => 'config_value' + ]]); + + $actual = $commandLine->parse(['command']); + expect($commandLine->get('option1'))->toEqual('config_value'); + + $actual = $commandLine->parse(['command', '--option1']); + expect($commandLine->get('option1'))->toEqual('config_value'); + + $actual = $commandLine->parse(['command', '--option1="some_value"']); + expect($commandLine->get('option1'))->toEqual('config_value'); + + }); + + it("formats value according to value function", function () { + + $commandLine = new CommandLine(['option1' => [ + 'type' => 'string', + 'default' => 'default_value', + 'value' => function ($value, $name, $commandLine) { + if (!$value) { + return 'empty_value'; + } + if ($value === 'default_value') { + return 'default_value'; + } + return 'non_empty_value'; + } + ]]); + + $actual = $commandLine->parse(['command']); + expect($commandLine->get('option1'))->toEqual('default_value'); + + $actual = $commandLine->parse(['command', '--option1']); + expect($commandLine->get('option1'))->toEqual('empty_value'); + + $actual = $commandLine->parse(['command', '--option1="some_value"']); + expect($commandLine->get('option1'))->toEqual('non_empty_value'); + + }); + + it("returns a group subset", function () { + + $commandLine = new CommandLine([ + 'option1:sub1' => ['array' => true], + 'option1:sub3' => ['type' => 'boolean', 'default' => true], + ]); + $actual = $commandLine->parse([ + 'command', '--option1:sub1', '--option1:sub1=value1', '--option1:sub1=value2', '--option1:sub2=value3' + ]); + + expect($commandLine->get('option1'))->toBe([ + 'sub3' => true, + 'sub1' => [ + null, + 'value1', + 'value2' + ], + 'sub2' => 'value3' + ]); + + }); + + it("returns a group subset even when no explicitly defined", function () { + + $commandLine = new CommandLine(); + $actual = $commandLine->parse([ + 'command', '--option1:sub1=value1', '--option1:sub2=value2' + ]); + + expect($commandLine->get('option1'))->toBe([ + 'sub1' => 'value1', + 'sub2' => 'value2' + ]); + + }); + + it("returns an array by default for group subsets", function () { + + $commandLine = new CommandLine(['option1:sub1' => ['array' => true],]); + $actual = $commandLine->parse(['command']); + + expect($commandLine->get('option1'))->toBe([]); + + }); + + }); + + describe("->exists()", function () { + + it("returns `true` if the option exists", function () { + + $commandLine = new CommandLine(); + $actual = $commandLine->parse([ + 'command', '--option1', '--option2=true' , '--option3=false', '--option4=0' + ]); + expect($commandLine->exists('option1'))->toBe(true); + expect($commandLine->exists('option2'))->toBe(true); + expect($commandLine->exists('option3'))->toBe(true); + expect($commandLine->exists('option4'))->toBe(true); + expect($commandLine->exists('option5'))->toBe(false); + + }); + + it("returns `true` if the option as a default value", function () { + + $commandLine = new CommandLine(); + $commandLine->option('option1', ['type' => 'boolean']); + $commandLine->option('option2', ['type' => 'boolean', 'default' => false]); + + expect($commandLine->exists('option1'))->toBe(false); + expect($commandLine->exists('option2'))->toBe(true); + + }); + + }); + + describe("->cast()", function () { + + it("casts array", function () { + + $commandLine = new CommandLine(); + $cast = $commandLine->cast(["some", "string", "and", 10], "string"); + expect($cast)->toBeAn('array'); + foreach ($cast as $c) { + expect($c)->toBeA('string'); + } + + }); + + it("casts boolean", function () { + + $commandLine = new CommandLine(); + $cast = $commandLine->cast(["true", "false", "some_string", null, 10], "boolean"); + expect($cast)->toBeAn('array'); + expect(count($cast))->toBe(5); + list($bTrue, $bFalse, $string, $null, $number) = $cast; + expect($bTrue)->toBeA('boolean')->toBe(true); + expect($bFalse)->toBeA('boolean')->toBe(false); + expect($string)->toBeA('boolean')->toBe(true); + expect($null)->toBeA('boolean')->toBe(false); + expect($number)->toBeA('boolean')->toBe(true); + + }); + + it("casts numeric", function () { + + $commandLine = new CommandLine(); + $cast = $commandLine->cast([true, "false", "some_string", null, 10], "numeric"); + expect($cast)->toBeAn('array'); + expect(count($cast))->toBe(5); + expect(implode($cast))->toBe("100110"); + + }); + + it("casts value into array", function () { + + $commandLine = new CommandLine(); + $cast = $commandLine->cast("string", "string", true); + expect($cast)->toBeA("array"); + expect($cast)->toContain("string"); + + }); + + }); + +}); diff --git a/spec/Suite/Cli/KahlanSpec.php b/spec/Suite/Cli/Kahlan.spec.php similarity index 74% rename from spec/Suite/Cli/KahlanSpec.php rename to spec/Suite/Cli/Kahlan.spec.php index 61fe6447..1498166e 100644 --- a/spec/Suite/Cli/KahlanSpec.php +++ b/spec/Suite/Cli/Kahlan.spec.php @@ -9,12 +9,12 @@ use Kahlan\Cli\Kahlan; use Kahlan\Plugin\Quit; -describe("Kahlan", function() { +describe("Kahlan", function () { /** * Save current & reinitialize the Interceptor class. */ - before(function() { + beforeAll(function () { $this->previous = Interceptor::instance(); Interceptor::unpatch(); }); @@ -22,11 +22,11 @@ /** * Restore Interceptor class. */ - after(function() { + afterAll(function () { Interceptor::load($this->previous); }); - beforeEach(function() { + beforeEach(function () { $this->specs = new Kahlan([ 'autoloader' => Interceptor::composer()[0], 'suite' => new Suite([ @@ -35,11 +35,11 @@ ]); }); - describe("->loadConfig()", function() { + describe("->loadConfig()", function () { - it("sets passed arguments to specs", function() { + it("sets passed arguments to specs", function () { - $args = [ + $argv = [ '--src=src', '--spec=spec/Fixture/Kahlan/Spec', '--pattern=*MySpec.php', @@ -59,14 +59,15 @@ '--autoclear=Kahlan\Plugin\Quit' ]; - $this->specs->loadConfig($args); - expect($this->specs->args()->get())->toBe([ + $this->specs->loadConfig($argv); + expect($this->specs->commandLine()->get())->toBe([ 'src' => ['src'], 'spec' => ['spec/Fixture/Kahlan/Spec'], + 'pattern' => "*MySpec.php", 'reporter' => [ "verbose" ], - 'pattern' => "*MySpec.php", + 'coverage' => '3', 'config' => "spec/Fixture/Kahlan/kahlan-config.php", 'ff' => 5, 'cc' => true, @@ -80,13 +81,12 @@ 'Kahlan\Plugin\Call', 'Kahlan\Plugin\Stub', 'Kahlan\Plugin\Quit' - ], - 'coverage' => '3' + ] ]); }); - it("loads the config file", function() { + it("loads the config file", function () { $this->specs->loadConfig([ '--spec=spec/Fixture/Kahlan/Spec/PassTest.php', @@ -102,7 +102,7 @@ }); - it("echoes version if --version if provided", function() { + it("echoes version if --version if provided", function () { $version = Kahlan::VERSION; @@ -122,10 +122,11 @@ EOD; - $closure = function() { + $closure = function () { try { $this->specs->loadConfig(['--version']); - } catch (Exception $e) {} + } catch (Exception $e) { + } }; Quit::disable(); @@ -133,7 +134,7 @@ }); - it("echoes the help if --help is provided", function() { + it("echoes the help if --help is provided", function () { $help = << The PHP configuration file to use (default: `'kahlan-config.php'`). --src= Paths of source directories (default: `['src']`). --spec= Paths of specification directories (default: `['spec']`). - --pattern= A shell wildcard pattern (default: `'*Spec.php'`). + --pattern= A shell wildcard pattern (default: `['*Spec.php', '*.spec.php']`). Reporter Options: @@ -174,12 +175,12 @@ Test Execution Options: --ff= Fast fail option. `0` mean unlimited (default: `0`). - --no-colors= To turn off colors. (default: `false`). - --no-header= To turn off header. (default: `false`). + --no-colors To turn off colors. (default: `false`). + --no-header To turn off header. (default: `false`). --include= Paths to include for patching. (default: `['*']`). --exclude= Paths to exclude from patching. (default: `[]`). --persistent= Cache patched files (default: `true`). - --cc= Clear cache before spec run. (default: `false`). + --cc Clear cache before spec run. (default: `false`). --autoclear Classes to autoclear after each spec (default: [ `'Kahlan\Plugin\Monkey'`, `'Kahlan\Plugin\Call'`, @@ -198,11 +199,10 @@ EOD; - $closure = function() { + $closure = function () { try { $this->specs->loadConfig(['--help']); } catch (Exception $e) { - } }; @@ -211,7 +211,7 @@ }); - it("doesn't display header with --no-header", function() { + it("doesn't display header with --no-header", function () { $version = Kahlan::VERSION; @@ -223,10 +223,11 @@ EOD; - $closure = function() { + $closure = function () { try { $this->specs->loadConfig(['--version', '--no-header']); - } catch (Exception $e) {} + } catch (Exception $e) { + } }; Quit::disable(); @@ -234,7 +235,7 @@ }); - it("isolates `kahlan-config.php` execution in a dedicated scope", function() { + it("isolates `kahlan-config.php` execution in a dedicated scope", function () { $version = Kahlan::VERSION; @@ -246,14 +247,15 @@ EOD; - $closure = function() { + $closure = function () { try { $this->specs->loadConfig([ '--config=spec/Fixture/Kahlan/kahlan-config.php', '--version', '--no-header' ]); - } catch (Exception $e) {} + } catch (Exception $e) { + } }; Quit::disable(); @@ -261,30 +263,30 @@ }); - it("doesn't filter empty string from include & exclude", function() { + it("doesn't filter empty string from include & exclude", function () { - $args = [ + $argv = [ '--include=', '--exclude=', ]; - $this->specs->loadConfig($args); - expect($this->specs->args()->get()['include'])->toBe([]); - expect($this->specs->args()->get()['exclude'])->toBe([]); + $this->specs->loadConfig($argv); + expect($this->specs->commandLine()->get()['include'])->toBe([]); + expect($this->specs->commandLine()->get()['exclude'])->toBe([]); }); }); - describe("->run()", function() { + describe("->run()", function () { - it("defines the KAHLAN_VERSION constant", function() { + it("defines the KAHLAN_VERSION constant", function () { expect(KAHLAN_VERSION)->toBe(Kahlan::VERSION); }); - it("runs a spec which pass", function() { + it("runs a spec which pass", function () { $this->specs->loadConfig([ '--spec=spec/Fixture/Kahlan/Spec/PassTest.php', @@ -299,7 +301,7 @@ }); - it("runs a spec which fail", function() { + it("runs a spec which fail", function () { $this->specs->loadConfig([ '--spec=spec/Fixture/Kahlan/Spec/FailTest.php', @@ -314,7 +316,7 @@ }); - it("runs filters in the correct order", function() { + it("runs filters in the correct order", function () { $this->specs->loadConfig([ '--spec=spec/Fixture/Kahlan/Spec/PassTest.php', @@ -324,37 +326,61 @@ $order = []; - Filter::register('spec.bootstrap', function($chain) use (&$order) { $order[] = 'bootstrap';}); + Filter::register('spec.bootstrap', function ($chain) use (&$order) { + $order[] = 'bootstrap'; + }); Filter::apply($this->specs, 'bootstrap', 'spec.bootstrap'); - Filter::register('spec.interceptor', function($chain) use (&$order) { $order[] = 'interceptor';}); + $previous = $this->previous; + Filter::register('spec.interceptor', function ($chain) use (&$order, $previous) { + Interceptor::load($previous); + $order[] = 'interceptor'; + }); Filter::apply($this->specs, 'interceptor', 'spec.interceptor'); - Filter::register('spec.namespaces', function($chain) use (&$order) { $order[] = 'namespaces';}); + Filter::register('spec.namespaces', function ($chain) use (&$order) { + $order[] = 'namespaces'; + }); Filter::apply($this->specs, 'namespaces', 'spec.namespaces'); - Filter::register('spec.patchers', function($chain) use (&$order) { $order[] = 'patchers';}); + Filter::register('spec.patchers', function ($chain) use (&$order) { + $order[] = 'patchers'; + }); Filter::apply($this->specs, 'patchers', 'spec.patchers'); - Filter::register('spec.load', function($chain) use (&$order) { $order[] = 'load';}); + Filter::register('spec.load', function ($chain) use (&$order) { + $order[] = 'load'; + }); Filter::apply($this->specs, 'load', 'spec.load'); - Filter::register('spec.reporters', function($chain) use (&$order) { $order[] = 'reporters';}); + Filter::register('spec.reporters', function ($chain) use (&$order) { + $order[] = 'reporters'; + }); Filter::apply($this->specs, 'reporters', 'spec.reporters'); - Filter::register('spec.matchers', function($chain) use (&$order) { $order[] = 'matchers';}); + Filter::register('spec.matchers', function ($chain) use (&$order) { + $order[] = 'matchers'; + }); Filter::apply($this->specs, 'matchers', 'spec.matchers'); - Filter::register('spec.run', function($chain) use (&$order) { $order[] = 'run';}); + Filter::register('spec.run', function ($chain) use (&$order) { + $order[] = 'run'; + }); Filter::apply($this->specs, 'run', 'spec.run'); - Filter::register('spec.reporting', function($chain) use (&$order) { $order[] = 'reporting';}); + Filter::register('spec.reporting', function ($chain) use (&$order) { + $order[] = 'reporting'; + }); Filter::apply($this->specs, 'reporting', 'spec.reporting'); - Filter::register('spec.stop', function($chain) use (&$order) { $order[] = 'stop';}); + Filter::register('spec.stop', function ($chain) use (&$order) { + $order[] = 'stop'; + }); Filter::apply($this->specs, 'stop', 'spec.stop'); - Filter::register('spec.quit', function($chain) use (&$order) { $order[] = 'quit';}); + Filter::register('spec.quit', function ($chain) use (&$order) { + $order[] = 'quit'; + }); Filter::apply($this->specs, 'quit', 'spec.quit'); $this->specs->run(); diff --git a/spec/Suite/Code/CodeSpec.php b/spec/Suite/Code/Code.spec.php similarity index 65% rename from spec/Suite/Code/CodeSpec.php rename to spec/Suite/Code/Code.spec.php index 21984b05..428dd1b6 100644 --- a/spec/Suite/Code/CodeSpec.php +++ b/spec/Suite/Code/Code.spec.php @@ -6,13 +6,13 @@ use Kahlan\Code\TimeoutException; use Kahlan\Code\Code; -describe("Code", function() { +describe("Code", function () { declare(ticks = 1) { - describe("::run()", function() { + describe("::run()", function () { - beforeEach(function() { + beforeEach(function () { if (!function_exists('pcntl_signal')) { skipIf(true); } @@ -22,16 +22,18 @@ $start = microtime(true); - expect(Code::run(function() {return true;}, 1))->toBe(true); + expect(Code::run(function () { + return true; + }, 1))->toBe(true); $end = microtime(true); expect($end - $start)->toBeLessThan(1); }); - it("throws an exception if an invalid closure is provided", function() { + it("throws an exception if an invalid closure is provided", function () { - $closure = function() { + $closure = function () { Code::run("invalid", 1); }; @@ -39,13 +41,15 @@ }); - it("throws an exception on timeout", function() { + it("throws an exception on timeout", function () { $start = microtime(true); - $closure = function() { - Code::run(function() { - while(true) sleep(1); + $closure = function () { + Code::run(function () { + while (true) { + sleep(1); + } }, 1); }; @@ -56,10 +60,10 @@ }); - it("throws all unexpected exceptions", function() { + it("throws all unexpected exceptions", function () { - $closure = function() { - Code::run(function() { + $closure = function () { + Code::run(function () { throw new Exception("Error Processing Request"); }, 1); }; @@ -72,22 +76,24 @@ } - describe("::spin()", function() { + describe("::spin()", function () { it("runs the passed closure", function () { $start = microtime(true); - expect(Code::spin(function() {return true;}, 1))->toBe(true); + expect(Code::spin(function () { + return true; + }, 1))->toBe(true); $end = microtime(true); expect($end - $start)->toBeLessThan(1); }); - it("throws an exception if an invalid closure is provided", function() { + it("throws an exception if an invalid closure is provided", function () { - $closure = function() { + $closure = function () { Code::spin("invalid", 1); }; @@ -95,12 +101,12 @@ }); - it("throws an exception on timeout", function() { + it("throws an exception on timeout", function () { $start = microtime(true); - $closure = function() { - Code::spin(function() {}, 1); + $closure = function () { + Code::spin(function () {}, 1); }; expect($closure)->toThrow(new TimeoutException('Timeout reached, execution aborted after 1 second(s).')); @@ -110,13 +116,15 @@ }); - it("respects the delay delay", function() { + it("respects the delay delay", function () { $start = microtime(true); $counter = 0; - $closure = function() use (&$counter) { - Code::spin(function() use (&$counter) { $counter++; }, 1, 250000); + $closure = function () use (&$counter) { + Code::spin(function () use (&$counter) { + $counter++; + }, 1, 250000); }; expect($closure)->toThrow(new TimeoutException('Timeout reached, execution aborted after 1 second(s).')); @@ -130,4 +138,4 @@ }); -}); \ No newline at end of file +}); diff --git a/spec/Suite/Dir/DirSpec.php b/spec/Suite/Dir/Dir.spec.php similarity index 84% rename from spec/Suite/Dir/DirSpec.php rename to spec/Suite/Dir/Dir.spec.php index bb3cb8b7..3c133268 100644 --- a/spec/Suite/Dir/DirSpec.php +++ b/spec/Suite/Dir/Dir.spec.php @@ -4,9 +4,9 @@ use Kahlan\Dir\Dir; use Exception; -describe("Dir", function() { +describe("Dir", function () { - $this->normalize = function($path) { + $this->normalize = function ($path) { if (!is_array($path)) { return str_replace('/', DIRECTORY_SEPARATOR, $path); } @@ -17,18 +17,18 @@ return $result; }; - describe("::scan()", function() { + describe("::scan()", function () { - $sort = function($files) { + $sort = function ($files) { sort($files); return $files; }; - beforeEach(function() { + beforeEach(function () { $this->path = 'spec/Fixture/Dir'; }); - it("scans files", function() { + it("scans files", function () { $files = Dir::scan($this->path, [ 'type' => 'file', @@ -38,7 +38,7 @@ }); - it("scans and show dots", function() use ($sort) { + it("scans and show dots", function () use ($sort) { $files = Dir::scan($this->path, [ 'skipDots' => false, @@ -55,7 +55,7 @@ }); - it("scans and follow symlinks", function() use ($sort) { + it("scans and follow symlinks", function () use ($sort) { $files = Dir::scan($this->path . DIRECTORY_SEPARATOR . 'Extensions', [ 'followSymlinks' => false, @@ -71,7 +71,7 @@ }); - it("scans files recursively", function() use ($sort) { + it("scans files recursively", function () use ($sort) { $files = Dir::scan($this->path . DIRECTORY_SEPARATOR . 'Nested', [ 'type' => 'file' @@ -85,7 +85,7 @@ }); - it("scans files & directores recursively", function() use ($sort) { + it("scans files & directores recursively", function () use ($sort) { $files = Dir::scan($this->path . DIRECTORY_SEPARATOR . 'Nested'); @@ -98,7 +98,7 @@ }); - it("scans only leaves recursively", function() use ($sort) { + it("scans only leaves recursively", function () use ($sort) { $files = Dir::scan($this->path. DIRECTORY_SEPARATOR . 'Nested', [ 'leavesOnly' => true @@ -112,7 +112,7 @@ }); - it("scans txt files recursively", function() use ($sort) { + it("scans txt files recursively", function () use ($sort) { $files = Dir::scan($this->path, [ 'include' => '*.txt', @@ -129,7 +129,7 @@ }); - it("scans non nested txt files recursively", function() use ($sort) { + it("scans non nested txt files recursively", function () use ($sort) { $files = Dir::scan($this->path, [ 'include' => '*.txt', @@ -144,9 +144,9 @@ }); - it("throws an exception if the path is invalid", function() { + it("throws an exception if the path is invalid", function () { - $closure = function() { + $closure = function () { Dir::scan('Non/Existing/Path', [ 'type' => 'file', 'recursive' => false @@ -156,7 +156,7 @@ }); - it("returns itself when the path is a file", function() { + it("returns itself when the path is a file", function () { $files = Dir::scan('spec/Fixture/Dir/file1.txt', [ 'include' => '*.txt', @@ -169,17 +169,17 @@ }); - describe("::copy()", function() { + describe("::copy()", function () { - beforeEach(function() { + beforeEach(function () { $this->tmpDir = Dir::tempnam(sys_get_temp_dir(), 'spec'); }); - afterEach(function() { + afterEach(function () { Dir::remove($this->tmpDir, ['recursive' => true]); }); - it("copies a directory recursively", function() { + it("copies a directory recursively", function () { Dir::copy('spec/Fixture/Dir', $this->tmpDir); @@ -192,7 +192,7 @@ }); - it("copies a directory recursively but not following symlinks", function() { + it("copies a directory recursively but not following symlinks", function () { Dir::copy('spec/Fixture/Dir', $this->tmpDir, ['followSymlinks' => false]); @@ -209,9 +209,9 @@ }); - it("throws an exception if the destination directory doesn't exists", function() { + it("throws an exception if the destination directory doesn't exists", function () { - $closure = function() { + $closure = function () { Dir::copy('spec/Fixture/Dir', 'Unexisting/Folder'); }; @@ -221,9 +221,9 @@ }); - describe("::remove()", function() { + describe("::remove()", function () { - it("removes a directory recursively", function() { + it("removes a directory recursively", function () { $this->tmpDir = Dir::tempnam(sys_get_temp_dir(), 'spec'); @@ -244,19 +244,19 @@ }); - describe("::make()", function() { + describe("::make()", function () { - beforeEach(function() { + beforeEach(function () { $this->umask = umask(0); $this->tmpDir = Dir::tempnam(sys_get_temp_dir(), 'spec'); }); - afterEach(function() { + afterEach(function () { Dir::remove($this->tmpDir, ['recursive' => true]); umask($this->umask); }); - it("creates a nested directory", function() { + it("creates a nested directory", function () { $path = $this->tmpDir . '/My/Nested/Directory'; $actual = Dir::make($path); @@ -270,7 +270,7 @@ }); - it("creates a nested directory with a specific mode", function() { + it("creates a nested directory with a specific mode", function () { $path = $this->tmpDir . '/My/Nested/Directory'; $actual = Dir::make($path, ['mode' => 0777]); @@ -284,7 +284,7 @@ }); - it("creates multiple nested directories in a single call", function() { + it("creates multiple nested directories in a single call", function () { $paths = [ $this->tmpDir . '/My/Nested/Directory', @@ -301,9 +301,9 @@ }); - describe("::tempnam()", function() { + describe("::tempnam()", function () { - it("uses the system temp directory by default", function() { + it("uses the system temp directory by default", function () { $dir = Dir::tempnam(null, 'spec'); diff --git a/spec/Suite/ExpectationSpec.php b/spec/Suite/Expectation.spec.php similarity index 59% rename from spec/Suite/ExpectationSpec.php rename to spec/Suite/Expectation.spec.php index 5818612d..2fd4a683 100644 --- a/spec/Suite/ExpectationSpec.php +++ b/spec/Suite/Expectation.spec.php @@ -8,15 +8,20 @@ use Kahlan\Specification; use Kahlan\Matcher; use Kahlan\Expectation; -use Kahlan\Plugin\Stub; +use Kahlan\Plugin\Double; -describe("Expectation", function() { +function expectation($actual, $timeout = -1) +{ + return new Expectation(compact('actual', 'timeout')); +} - beforeEach(function() { +describe("Expectation", function () { + + beforeEach(function () { $this->matchers = Matcher::get(); }); - afterEach(function() { + afterEach(function () { Matcher::reset(); foreach ($this->matchers as $name => $value) { foreach ($value as $for => $class) { @@ -25,24 +30,24 @@ } }); - describe("->__call()", function() { + describe("->__call()", function () { - it("throws an exception when using an undefined matcher name", function() { + it("throws an exception when using an undefined matcher name", function () { - $closure = function() { - $result = Expectation::expect(true)->toHelloWorld(true); + $closure = function () { + $result = expectation(true)->toHelloWorld(true); }; expect($closure)->toThrow(new Exception("Unexisting matcher attached to `'toHelloWorld'`.")); }); - it("throws an exception when a specific class matcher doesn't match", function() { + it("throws an exception when a specific class matcher doesn't match", function () { - Matcher::register('toEqualCustom', Stub::classname(['extends' => 'Kahlan\Matcher\ToEqual']), 'stdClass'); + Matcher::register('toEqualCustom', Double::classname(['extends' => 'Kahlan\Matcher\ToEqual']), 'stdClass'); - $closure = function() { - $result = Expectation::expect([])->toEqualCustom(new stdClass()); + $closure = function () { + $result = expectation([])->toEqualCustom(new stdClass()); }; expect($closure)->toThrow(new Exception("Unexisting matcher attached to `'toEqualCustom'` for `stdClass`.")); @@ -52,7 +57,7 @@ it("doesn't wait when the spec passes", function () { $start = microtime(true); - $result = Expectation::expect(true, 1)->toBe(true); + $result = expectation(true, 1)->toBe(true); $end = microtime(true); expect($end - $start)->toBeLessThan(1); @@ -61,7 +66,7 @@ it("loops until the timeout is reached on failure", function () { $start = microtime(true); - $result = Expectation::expect(true, 0.1)->toBe(false); + $result = expectation(true, 0.1)->toBe(false); $end = microtime(true); expect($end - $start)->toBeGreaterThan(0.1); expect($end - $start)->toBeLessThan(0.2); @@ -71,10 +76,10 @@ it("loops until the timeout is reached on failure using a sub spec with a return value", function () { $start = microtime(true); - $subspec = new Specification(['closure' => function() { + $subspec = new Specification(['closure' => function () { return true; }]); - $result = Expectation::expect($subspec, 0.1)->toBe(false); + $result = expectation($subspec, 0.1)->toBe(false); $end = microtime(true); expect($end - $start)->toBeGreaterThan(0.1); expect($end - $start)->toBeLessThan(0.2); @@ -84,7 +89,7 @@ it("doesn't wait on failure when a negative expectation is expected", function () { $start = microtime(true); - $result = Expectation::expect(true, 1)->not->toBe(false); + $result = expectation(true, 1)->not->toBe(false); $end = microtime(true); expect($end - $start)->toBeLessThan(1); @@ -92,33 +97,33 @@ }); - describe("->run()", function() { + describe("->passed()", function () { - it ("returns the matcher when called", function() { + it("verifies the expectation", function () { - $result = Expectation::expect(true)->run(); - expect($result)->toBeAnInstanceOf('Kahlan\Expectation'); + $actual = expectation(true)->toBe(true)->passed(); + expect($actual)->toBe(true); }); - it ("runs sub specs", function() { + it("verifies nested expectations inside a spec", function () { - $subspec = new Specification(['closure' => function() { + $spec = new Specification(['closure' => function () { return true; }]); - $result = Expectation::expect($subspec)->run(); - expect($result)->toBeAnInstanceOf('Kahlan\Expectation'); + $actual = expectation($spec)->toBe(true)->passed(); + expect($actual)->toBe(true); }); - it("loops until the timeout is reached on failure using a sub spec", function () { + it("loops until the timeout is reached on failure", function () { $start = microtime(true); - $subspec = new Specification(['closure' => function() { + $spec = new Specification(['closure' => function () { expect(true)->toBe(false); }]); - $result = Expectation::expect($subspec, 0.1)->run(); - expect($result)->toBeAnInstanceOf('Kahlan\Expectation'); + $actual = expectation($spec, 0.1)->passed(); + expect($actual)->toBe(false); $end = microtime(true); expect($end - $start)->toBeGreaterThan(0.1); expect($end - $start)->toBeLessThan(0.2); @@ -127,9 +132,9 @@ }); - describe("->__get()", function() { + describe("->__get()", function () { - it("sets the not value using `'not'`", function() { + it("sets the not value using `'not'`", function () { $expectation = new Expectation(); expect($expectation->not())->toBe(false); @@ -138,9 +143,9 @@ }); - it("throws an exception with unsupported attributes", function() { + it("throws an exception with unsupported attributes", function () { - $closure = function() { + $closure = function () { $expectation = new Expectation(); $expectation->abc; }; @@ -150,16 +155,25 @@ }); - describe("->clear()", function() { + describe("->clear()", function () { - it("clears an expectation", function() { + it("clears an expectation", function () { - $actual = new stdClass(); - $expectation = Expectation::expect($actual, 10); + $actual = Double::instance(); + $expectation = expectation($actual, 10); $matcher = $expectation->not->toReceive('helloWorld'); expect($expectation->actual())->toBe($actual); - expect($expectation->deferred())->toHaveLength(1); + expect($expectation->deferred())->toBe([ + 'matcherName' => 'toReceive', + 'matcher' => 'Kahlan\Matcher\ToReceive', + 'data' => [ + 'actual' => $actual, + 'expected' => 'helloWorld' + ], + 'instance' => $matcher, + 'not' => true + ]); expect($expectation->timeout())->toBe(10); expect($expectation->not())->toBe(true); expect($expectation->passed())->toBe(true); @@ -168,7 +182,7 @@ $expectation->clear(); expect($expectation->actual())->toBe(null); - expect($expectation->deferred())->toHaveLength(0); + expect($expectation->deferred())->toBe(null); expect($expectation->timeout())->toBe(-1); expect($expectation->not())->toBe(false); expect($expectation->passed())->toBe(true); diff --git a/spec/Suite/Filter/Behavior/Filterable.spec.php b/spec/Suite/Filter/Behavior/Filterable.spec.php new file mode 100644 index 00000000..8a20e5e1 --- /dev/null +++ b/spec/Suite/Filter/Behavior/Filterable.spec.php @@ -0,0 +1,37 @@ +mock = Double::instance(['uses' => ['Kahlan\Filter\Behavior\Filterable']]); + + allow($this->mock)->toReceive('filterable')->andRun(function () { + return Filter::on($this, 'filterable', func_get_args(), function ($chain, $message) { + return "Hello {$message}"; + }); + }); + }); + + describe("methodFilters", function () { + + it("gets the `MethodFilters` instance", function () { + + expect($this->mock->methodFilters())->toBeAnInstanceOf('Kahlan\Filter\MethodFilters'); + + }); + + it("sets a new `MethodFilters` instance", function () { + + $methodFilters = new MethodFilters(); + expect($this->mock->methodFilters($methodFilters))->toBeAnInstanceOf('Kahlan\Filter\MethodFilters'); + expect($this->mock->methodFilters())->toBe($methodFilters); + + }); + + }); + +}); diff --git a/spec/Suite/Filter/Behavior/FilterableSpec.php b/spec/Suite/Filter/Behavior/FilterableSpec.php deleted file mode 100644 index 6ccade3e..00000000 --- a/spec/Suite/Filter/Behavior/FilterableSpec.php +++ /dev/null @@ -1,37 +0,0 @@ -mock = Stub::create(['uses' => ['Kahlan\Filter\Behavior\Filterable']]); - - Stub::on($this->mock)->method('filterable', function() { - return Filter::on($this, 'filterable', func_get_args(), function($chain, $message) { - return "Hello {$message}"; - }); - }); - }); - - describe("methodFilters", function() { - - it("gets the `MethodFilters` instance", function() { - - expect($this->mock->methodFilters())->toBeAnInstanceOf('Kahlan\Filter\MethodFilters'); - - }); - - it("sets a new `MethodFilters` instance", function() { - - $methodFilters = new MethodFilters(); - expect($this->mock->methodFilters($methodFilters))->toBeAnInstanceOf('Kahlan\Filter\MethodFilters'); - expect($this->mock->methodFilters())->toBe($methodFilters); - - }); - - }); - -}); diff --git a/spec/Suite/Filter/Chain.spec.php b/spec/Suite/Filter/Chain.spec.php new file mode 100644 index 00000000..3128cec2 --- /dev/null +++ b/spec/Suite/Filter/Chain.spec.php @@ -0,0 +1,71 @@ + function ($chain, $message) { + $message = "My {$message}"; + return $chain->next($message); + }, + 'filter2' => function ($chain, $message) { + return $message; + } + ]; + $params = ['World!']; + $this->chain = new Chain(compact('filters', 'method', 'params')); + }); + + describe("->params()", function () { + + it("gets the params", function () { + + expect($this->chain->params())->toBe(['World!']); + + }); + + }); + + describe("->method()", function () { + + it("gets the methods", function () { + + expect($this->chain->method())->toBe('message'); + + }); + + }); + + describe("Iterator", function () { + + it("iterate throw the chain", function () { + + expect($this->chain->current())->toBeAnInstanceOf('Closure'); + expect($this->chain->key())->toBe('filter1'); + expect($this->chain->next())->toBe('World!'); + expect($this->chain->next())->toBe(false); + expect($this->chain->valid())->toBe(false); + + }); + + it("procceses a chain", function () { + + $closure = $this->chain->current(); + expect($closure($this->chain, 'Poney!'))->toBe('My Poney!'); + + }); + + it("counts the number of filters in the chain", function () { + + expect($this->chain)->toHaveLength(2); + + }); + + }); + +}); diff --git a/spec/Suite/Filter/ChainSpec.php b/spec/Suite/Filter/ChainSpec.php deleted file mode 100644 index 57115a80..00000000 --- a/spec/Suite/Filter/ChainSpec.php +++ /dev/null @@ -1,71 +0,0 @@ - function ($chain, $message) { - $message = "My {$message}"; - return $chain->next($message); - }, - 'filter2' => function ($chain, $message) { - return $message; - } - ]; - $params = ['World!']; - $this->chain = new Chain(compact('filters', 'method', 'params')); - }); - - describe("->params()", function() { - - it("gets the params", function() { - - expect($this->chain->params())->toBe(['World!']); - - }); - - }); - - describe("->method()", function() { - - it("gets the methods", function() { - - expect($this->chain->method())->toBe('message'); - - }); - - }); - - describe("Iterator", function() { - - it("iterate throw the chain", function() { - - expect($this->chain->current())->toBeAnInstanceOf('Closure'); - expect($this->chain->key())->toBe('filter1'); - expect($this->chain->next())->toBe('World!'); - expect($this->chain->next())->toBe(false); - expect($this->chain->valid())->toBe(false); - - }); - - it("procceses a chain", function() { - - $closure = $this->chain->current(); - expect($closure($this->chain, 'Poney!'))->toBe('My Poney!'); - - }); - - it("counts the number of filters in the chain", function() { - - expect($this->chain)->toHaveLength(2); - - }); - - }); - -}); diff --git a/spec/Suite/Filter/Filter.spec.php b/spec/Suite/Filter/Filter.spec.php new file mode 100644 index 00000000..ea1135a4 --- /dev/null +++ b/spec/Suite/Filter/Filter.spec.php @@ -0,0 +1,295 @@ +next($message); + }); + + Filter::register('spec.be_prefix', function ($chain, $message) { + $message = "Be {$message}"; + return $chain->next($message); + }); + + Filter::register('spec.no_chain', function ($chain, $message) { + return "No Man's {$message}"; + }); + + }); + + afterEach(function () { + Filter::reset(); + Filter::enable(); + }); + + context("with an instance context", function () { + + beforeEach(function () { + $this->mock = Double::instance(['uses' => ['Kahlan\Filter\Behavior\Filterable']]); + allow($this->mock)->toReceive('filterable')->andRun(function () { + return Filter::on($this, 'filterable', func_get_args(), function ($chain, $message) { + return "Hello {$message}"; + }); + }); + }); + + describe("::apply()", function () { + + it("applies a filter which override a parameter", function () { + + Filter::apply($this->mock, 'filterable', 'spec.my_prefix'); + expect($this->mock->filterable('World!'))->toBe('Hello My World!'); + + }); + + it("applies a filter which break the chain", function () { + + Filter::apply($this->mock, 'filterable', 'spec.no_chain'); + expect($this->mock->filterable('World!'))->toBe("No Man's World!"); + + }); + + it("applies a custom filter", function () { + + allow($this->mock)->toReceive('filterable')->andRun(function () { + $closure = function ($chain, $message) { + return "Hello {$message}"; + }; + $custom = function ($chain, $message) { + $message = "Custom {$message}"; + return $chain->next($message); + }; + return Filter::on($this, 'filterable', func_get_args(), $closure, [$custom]); + }); + expect($this->mock->filterable('World!'))->toBe("Hello Custom World!"); + + }); + + it("applies all filter set on the classname", function () { + + Filter::apply(get_class($this->mock), 'filterable', 'spec.my_prefix'); + expect($this->mock->filterable('World!'))->toBe('Hello My World!'); + + }); + + it("throws an Exception when trying to apply a filter using an unexisting closure", function () { + + $closure = function () { + Filter::apply($this->mock, 'filterable', 'spec.unexisting_closure'); + }; + expect($closure)->toThrow(new Exception('Undefined filter `spec.unexisting_closure`.')); + + }); + + }); + + describe("::detach()", function () { + + it("detaches a filters", function () { + + Filter::apply($this->mock, 'filterable', 'spec.my_prefix'); + Filter::detach($this->mock, 'filterable', 'spec.my_prefix'); + expect($this->mock->filterable('World!'))->toBe('Hello World!'); + + }); + + }); + + describe("::filters()", function () { + + it("gets filters of a context", function () { + + Filter::apply($this->mock, 'filterable', 'spec.my_prefix'); + $filters = Filter::filters($this->mock, 'filterable'); + expect($filters)->toBeAn('array')->toHaveLength(1); + expect(reset($filters))->toBeAnInstanceOf('Closure'); + + }); + + }); + + describe("::enable()", function () { + + it("disables the filter system", function () { + Filter::apply($this->mock, 'filterable', 'spec.my_prefix'); + Filter::enable(false); + expect($this->mock->filterable('World!'))->toBe('Hello World!'); + }); + + }); + + }); + + context("with a class context", function () { + + beforeEach(function () { + $this->class = Double::classname(); + allow($this->class)->toReceive('::filterable')->andRun(function () { + return Filter::on(get_called_class(), 'filterable', func_get_args(), function ($chain, $message) { + return "Hello {$message}"; + }); + }); + }); + + describe("::apply()", function () { + + it("applies a filter and override a parameter", function () { + $class = $this->class; + Filter::apply($class, 'filterable', 'spec.my_prefix'); + expect($class::filterable('World!'))->toBe('Hello My World!'); + }); + + it("applies a filter and break the chain", function () { + $class = $this->class; + Filter::apply($class, 'filterable', 'spec.no_chain'); + expect($class::filterable('World!'))->toBe("No Man's World!"); + }); + + it("applies parent classes's filters", function () { + $class = $this->class; + $subclass = Double::classname(['extends' => $class]); + allow($subclass)->toReceive('::filterable')->andRun(function () { + return Filter::on(get_called_class(), 'filterable', func_get_args(), function ($chain, $message) { + return "Hello {$message}"; + }); + }); + Filter::apply($class, 'filterable', 'spec.be_prefix'); + Filter::apply($subclass, 'filterable', 'spec.my_prefix'); + expect($subclass::filterable('World!'))->toBe('Hello Be My World!'); + }); + + it("applies parent classes's filters using cached filters", function () { + $class = $this->class; + $subclass = Double::classname(['extends' => $class]); + allow($subclass)->toReceive('::filterable')->andRun(function () { + return Filter::on(get_called_class(), 'filterable', func_get_args(), function ($chain, $message) { + return "Hello {$message}"; + }); + }); + Filter::apply($class, 'filterable', 'spec.be_prefix'); + Filter::apply($subclass, 'filterable', 'spec.my_prefix'); + expect($subclass::filterable('World!'))->toBe('Hello Be My World!'); + expect($subclass::filterable('World!'))->toBe('Hello Be My World!'); + }); + + it("invalidates parent cached filters", function () { + $class = $this->class; + $subclass = Double::classname(['extends' => $class]); + allow($subclass)->toReceive('::filterable')->andRun(function () { + return Filter::on(get_called_class(), 'filterable', func_get_args(), function ($chain, $message) { + return "Hello {$message}"; + }); + }); + Filter::apply($class, 'filterable', 'spec.be_prefix'); + Filter::apply($subclass, 'filterable', 'spec.my_prefix'); + expect($subclass::filterable('World!'))->toBe('Hello Be My World!'); + + Filter::apply($subclass, 'filterable', 'spec.no_chain'); + expect($subclass::filterable('World!'))->toBe("No Man's My World!"); + }); + + it("throws an Exception when trying to apply a filter using an unexisting closure", function () { + $class = $this->class; + $closure = function () use ($class) { + Filter::apply($class, 'filterable', 'spec.unexisting_closure'); + }; + expect($closure)->toThrow(new Exception('Undefined filter `spec.unexisting_closure`.')); + }); + + }); + + describe("::filters()", function () { + + it("exports filters setted as a class level", function () { + Filter::apply($this->class, 'filterable', 'spec.my_prefix'); + $filters = Filter::filters(); + expect($filters)->toHaveLength(1); + expect(isset($filters[$this->class]))->toBe(true); + }); + + it("imports class based filters", function () { + Filter::filters([$this->class => [Filter::registered('spec.my_prefix')]]); + $filters = Filter::filters(); + expect($filters)->toHaveLength(1); + expect(isset($filters[$this->class]))->toBe(true); + }); + + }); + }); + + describe("::apply()", function () { + + it("throws an Exception when trying to apply a filter on an unfilterable context", function () { + $closure = function () { + Filter::apply(null, 'filterable', 'spec.my_prefix'); + }; + expect($closure)->toThrow(new Exception("Error this context can't be filtered.")); + }); + + }); + + describe("::registered()", function () { + + it("exports the `Filter` class data", function () { + $registered = Filter::registered(); + expect($registered)->toHaveLength(3); + Filter::reset(); + Filter::register($registered); + $registered = Filter::registered(); + expect($registered)->toHaveLength(3); + }); + }); + + describe("::register()", function () { + + it("registers a closure", function () { + Filter::register('spec.newclosure', function ($chain, $message) { + $message = "My {$message}"; + return $chain->next($message); + }); + expect(Filter::registered('spec.newclosure'))->toBe(true); + }); + + it("registers a closure with no name", function () { + $name = Filter::register(function ($chain, $message) { + $message = "My {$message}"; + return $chain->next($message); + }); + expect(Filter::registered($name))->toBe(true); + }); + + }); + + describe("::unregister()", function () { + + it("unregisters a closure", function () { + Filter::register('spec.newclosure', function ($chain, $message) { + $message = "My {$message}"; + return $chain->next($message); + }); + Filter::unregister('spec.newclosure'); + expect(Filter::registered('spec.newclosure'))->toBe(false); + }); + + }); + + describe("::resets()", function () { + + it("clears all the filters", function () { + Filter::reset(); + expect(Filter::registered('spec.my_prefix'))->toBe(false); + expect(Filter::registered('spec.be_prefix'))->toBe(false); + expect(Filter::registered('spec.no_chain'))->toBe(false); + }); + + }); + +}); diff --git a/spec/Suite/Filter/FilterSpec.php b/spec/Suite/Filter/FilterSpec.php deleted file mode 100644 index 8cd2a0e2..00000000 --- a/spec/Suite/Filter/FilterSpec.php +++ /dev/null @@ -1,291 +0,0 @@ -next($message); - }); - - Filter::register('spec.be_prefix', function($chain, $message) { - $message = "Be {$message}"; - return $chain->next($message); - }); - - Filter::register('spec.no_chain', function($chain, $message) { - return "No Man's {$message}"; - }); - - }); - - afterEach(function() { - Filter::reset(); - Filter::enable(); - }); - - context("with an instance context", function() { - - beforeEach(function() { - $this->mock = Stub::create(['uses' => ['Kahlan\Filter\Behavior\Filterable']]); - Stub::on($this->mock)->method('filterable', function() { - return Filter::on($this, 'filterable', func_get_args(), function($chain, $message) { - return "Hello {$message}"; - }); - }); - }); - - describe("::apply()", function() { - - it("applies a filter which override a parameter", function() { - - Filter::apply($this->mock, 'filterable', 'spec.my_prefix'); - expect($this->mock->filterable('World!'))->toBe('Hello My World!'); - - }); - - it("applies a filter which break the chain", function() { - - Filter::apply($this->mock, 'filterable', 'spec.no_chain'); - expect($this->mock->filterable('World!'))->toBe("No Man's World!"); - - }); - - it("applies a custom filter", function() { - - Stub::on($this->mock)->method('filterable', function() { - $closure = function($chain, $message) { - return "Hello {$message}"; - }; - $custom = function($chain, $message) { - $message = "Custom {$message}"; - return $chain->next($message); - }; - return Filter::on($this, 'filterable', func_get_args(), $closure, [$custom]); - }); - expect($this->mock->filterable('World!'))->toBe("Hello Custom World!"); - - }); - - it("applies all filter set on the classname", function() { - - Filter::apply(get_class($this->mock), 'filterable', 'spec.my_prefix'); - expect($this->mock->filterable('World!'))->toBe('Hello My World!'); - - }); - - it("throws an Exception when trying to apply a filter using an unexisting closure", function() { - - $closure = function() { - Filter::apply($this->mock, 'filterable', 'spec.unexisting_closure'); - }; - expect($closure)->toThrow(new Exception('Undefined filter `spec.unexisting_closure`.')); - - }); - - }); - - describe("::detach()", function() { - - it("detaches a filters", function() { - - Filter::apply($this->mock, 'filterable', 'spec.my_prefix'); - Filter::detach($this->mock, 'filterable', 'spec.my_prefix'); - expect($this->mock->filterable('World!'))->toBe('Hello World!'); - - }); - - }); - - describe("::filters()", function() { - - it("gets filters of a context", function() { - - Filter::apply($this->mock, 'filterable', 'spec.my_prefix'); - $filters = Filter::filters($this->mock, 'filterable'); - expect($filters)->toBeAn('array')->toHaveLength(1); - expect(reset($filters))->toBeAnInstanceOf('Closure'); - - }); - - }); - - describe("::enable()", function() { - - it("disables the filter system", function() { - Filter::apply($this->mock, 'filterable', 'spec.my_prefix'); - Filter::enable(false); - expect($this->mock->filterable('World!'))->toBe('Hello World!'); - }); - - }); - - }); - - context("with a class context", function() { - - beforeEach(function() { - $this->class = Stub::classname(); - Stub::on($this->class)->method('::filterable', function() { - return Filter::on(get_called_class(), 'filterable', func_get_args(), function($chain, $message) { - return "Hello {$message}"; - }); - }); - }); - - describe("::apply()", function() { - - it("applies a filter and override a parameter", function() { - $class = $this->class; - Filter::apply($class, 'filterable', 'spec.my_prefix'); - expect($class::filterable('World!'))->toBe('Hello My World!'); - }); - - it("applies a filter and break the chain", function() { - $class = $this->class; - Filter::apply($class, 'filterable', 'spec.no_chain'); - expect($class::filterable('World!'))->toBe("No Man's World!"); - }); - - it("applies parent classes's filters", function() { - $class = $this->class; - $subclass = Stub::classname(['extends' => $class]); - Stub::on($subclass)->method('::filterable', function() { - return Filter::on(get_called_class(), 'filterable', func_get_args(), function($chain, $message) { - return "Hello {$message}"; - }); - }); - Filter::apply($class, 'filterable', 'spec.be_prefix'); - Filter::apply($subclass, 'filterable', 'spec.my_prefix'); - expect($subclass::filterable('World!'))->toBe('Hello Be My World!'); - }); - - it("applies parent classes's filters using cached filters", function() { - $class = $this->class; - $subclass = Stub::classname(['extends' => $class]); - Stub::on($subclass)->method('::filterable', function() { - return Filter::on(get_called_class(), 'filterable', func_get_args(), function($chain, $message) { - return "Hello {$message}"; - }); - }); - Filter::apply($class, 'filterable', 'spec.be_prefix'); - Filter::apply($subclass, 'filterable', 'spec.my_prefix'); - expect($subclass::filterable('World!'))->toBe('Hello Be My World!'); - expect($subclass::filterable('World!'))->toBe('Hello Be My World!'); - }); - - it("invalidates parent cached filters", function() { - $class = $this->class; - $subclass = Stub::classname(['extends' => $class]); - Stub::on($subclass)->method('::filterable', function() { - return Filter::on(get_called_class(), 'filterable', func_get_args(), function($chain, $message) { - return "Hello {$message}"; - }); - }); - Filter::apply($class, 'filterable', 'spec.be_prefix'); - Filter::apply($subclass, 'filterable', 'spec.my_prefix'); - expect($subclass::filterable('World!'))->toBe('Hello Be My World!'); - - Filter::apply($subclass, 'filterable', 'spec.no_chain'); - expect($subclass::filterable('World!'))->toBe("No Man's My World!"); - }); - - it("throws an Exception when trying to apply a filter using an unexisting closure", function() { - $class = $this->class; - $closure = function() use ($class) { Filter::apply($class, 'filterable', 'spec.unexisting_closure'); }; - expect($closure)->toThrow(new Exception('Undefined filter `spec.unexisting_closure`.')); - }); - - }); - - describe("::filters()", function() { - - it("exports filters setted as a class level", function() { - Filter::apply($this->class, 'filterable', 'spec.my_prefix'); - $filters = Filter::filters(); - expect($filters)->toHaveLength(1); - expect(isset($filters[$this->class]))->toBe(true); - }); - - it("imports class based filters", function() { - Filter::filters([$this->class => [Filter::registered('spec.my_prefix')]]); - $filters = Filter::filters(); - expect($filters)->toHaveLength(1); - expect(isset($filters[$this->class]))->toBe(true); - }); - - }); - }); - - describe("::apply()", function() { - - it("throws an Exception when trying to apply a filter on an unfilterable context", function() { - $closure = function() { Filter::apply(null, 'filterable', 'spec.my_prefix'); }; - expect($closure)->toThrow(new Exception("Error this context can't be filtered.")); - }); - - }); - - describe("::registered()", function() { - - it("exports the `Filter` class data", function() { - $registered = Filter::registered(); - expect($registered)->toHaveLength(3); - Filter::reset(); - Filter::register($registered); - $registered = Filter::registered(); - expect($registered)->toHaveLength(3); - }); - }); - - describe("::register()", function() { - - it("registers a closure", function() { - Filter::register('spec.newclosure', function($chain, $message) { - $message = "My {$message}"; - return $chain->next($message); - }); - expect(Filter::registered('spec.newclosure'))->toBe(true); - }); - - it("registers a closure with no name", function() { - $name = Filter::register(function($chain, $message) { - $message = "My {$message}"; - return $chain->next($message); - }); - expect(Filter::registered($name))->toBe(true); - }); - - }); - - describe("::unregister()", function() { - - it("unregisters a closure", function() { - Filter::register('spec.newclosure', function($chain, $message) { - $message = "My {$message}"; - return $chain->next($message); - }); - Filter::unregister('spec.newclosure'); - expect(Filter::registered('spec.newclosure'))->toBe(false); - }); - - }); - - describe("::resets()", function() { - - it("clears all the filters", function() { - Filter::reset(); - expect(Filter::registered('spec.my_prefix'))->toBe(false); - expect(Filter::registered('spec.be_prefix'))->toBe(false); - expect(Filter::registered('spec.no_chain'))->toBe(false); - }); - - }); - -}); diff --git a/spec/Suite/Filter/MethodFilters.spec.php b/spec/Suite/Filter/MethodFilters.spec.php new file mode 100644 index 00000000..e1cc48d0 --- /dev/null +++ b/spec/Suite/Filter/MethodFilters.spec.php @@ -0,0 +1,160 @@ +methodFilters = new MethodFilters(); + }); + + describe("->apply/filters()", function () { + + it("applies a filter to a method", function () { + + $this->methodFilters->apply('myMethod', 'spec.hello_world', function () { + return 'Hello World!'; + }); + + $filters = $this->methodFilters->filters('myMethod'); + expect($filters)->toBeAn('array')->toHaveLength(1); + + $closure = reset($filters); + expect($closure())->toBe('Hello World!'); + + }); + + it("applies multiple filters to a method", function () { + + $this->methodFilters->apply('myMethod', 'spec.hello_world', function () { + return 'Hello World!'; + }); + $this->methodFilters->apply('myMethod', 'spec.hello_beautiful_world', function () { + return 'Hello Beautiful World!'; + }); + + $filters = $this->methodFilters->filters('myMethod'); + expect($filters)->toBeAn('array')->toHaveLength(2); + + $closure = reset($filters); + expect($closure())->toBe('Hello World!'); + + $closure = next($filters); + expect($closure())->toBe('Hello Beautiful World!'); + + }); + + it("applies a filter to many methods once", function () { + + $this->methodFilters->apply(['myMethod1', 'myMethod2'], 'spec.hello_boy', function () { + return 'Hello Boy!'; + }); + + foreach (['myMethod1', 'myMethod2'] as $method) { + $filters = $this->methodFilters->filters($method); + expect($filters)->toBeAn('array')->toHaveLength(1); + + $closure = reset($filters); + expect($closure())->toBe('Hello Boy!'); + } + + }); + + }); + + describe("->detach()", function () { + + it("detaches a filter", function () { + + $this->methodFilters->apply('myMethod', 'spec.hello_world', function () { + return 'Hello World!'; + }); + $this->methodFilters->detach('myMethod', 'spec.hello_world'); + + $filters = $this->methodFilters->filters('myMethod'); + expect($filters)->toHaveLength(0); + + }); + + it("detaches filters by name", function () { + + $this->methodFilters->apply(['myMethod1', 'myMethod2'], 'spec.hello_boy', function () { + return 'Hello Boy!'; + }); + $this->methodFilters->detach(null, 'spec.hello_boy'); + + $filters = $this->methodFilters->filters('myMethod1'); + expect($filters)->toHaveLength(0); + + $filters = $this->methodFilters->filters('myMethod2'); + expect($filters)->toHaveLength(0); + + }); + + it("detaches all filters of a method", function () { + + $this->methodFilters->apply('myMethod', 'spec.hello_world', function () { + return 'Hello World!'; + }); + $this->methodFilters->apply('myMethod', 'spec.hello_beautiful_world', function () { + return 'Hello Beautiful World!'; + }); + $this->methodFilters->detach('myMethod'); + + $filters = $this->methodFilters->filters('myMethod'); + expect($filters)->toHaveLength(0); + + }); + + it("detaches all filters", function () { + + $this->methodFilters->apply('myMethod', 'spec.hello_world', function () { + return 'Hello World!'; + }); + $this->methodFilters->apply('myMethod', 'spec.hello_beautiful_world', function () { + return 'Hello Beautiful World!'; + }); + $this->methodFilters->apply(['myMethod1', 'myMethod2'], 'spec.hello_boy', function () { + return 'Hello Boy!'; + }); + $this->methodFilters->detach(); + + $filters = $this->methodFilters->filters('myMethod'); + expect($filters)->toHaveLength(0); + + $filters = $this->methodFilters->filters('myMethod1'); + expect($filters)->toHaveLength(0); + + $filters = $this->methodFilters->filters('myMethod2'); + expect($filters)->toHaveLength(0); + + }); + + it("clears all filters", function () { + + $this->methodFilters->apply('myMethod', 'spec.hello_world', function () { + return 'Hello World!'; + }); + $this->methodFilters->apply('myMethod', 'spec.hello_beautiful_world', function () { + return 'Hello Beautiful World!'; + }); + $this->methodFilters->apply(['myMethod1', 'myMethod2'], 'spec.hello_boy', function () { + return 'Hello Boy!'; + }); + $this->methodFilters->clear(); + + $filters = $this->methodFilters->filters('myMethod'); + expect($filters)->toHaveLength(0); + + $filters = $this->methodFilters->filters('myMethod1'); + expect($filters)->toHaveLength(0); + + $filters = $this->methodFilters->filters('myMethod2'); + expect($filters)->toHaveLength(0); + + }); + + }); + +}); diff --git a/spec/Suite/Filter/MethodFiltersSpec.php b/spec/Suite/Filter/MethodFiltersSpec.php deleted file mode 100644 index 6fcebbf9..00000000 --- a/spec/Suite/Filter/MethodFiltersSpec.php +++ /dev/null @@ -1,132 +0,0 @@ -methodFilters = new MethodFilters(); - }); - - describe("->apply/filters()", function() { - - it("applies a filter to a method", function() { - - $this->methodFilters->apply('myMethod', 'spec.hello_world', function() { return 'Hello World!'; }); - - $filters = $this->methodFilters->filters('myMethod'); - expect($filters)->toBeAn('array')->toHaveLength(1); - - $closure = reset($filters); - expect($closure())->toBe('Hello World!'); - - }); - - it("applies multiple filters to a method", function() { - - $this->methodFilters->apply('myMethod', 'spec.hello_world', function() { return 'Hello World!'; }); - $this->methodFilters->apply('myMethod', 'spec.hello_beautiful_world', function() { return 'Hello Beautiful World!'; }); - - $filters = $this->methodFilters->filters('myMethod'); - expect($filters)->toBeAn('array')->toHaveLength(2); - - $closure = reset($filters); - expect($closure())->toBe('Hello World!'); - - $closure = next($filters); - expect($closure())->toBe('Hello Beautiful World!'); - - }); - - it("applies a filter to many methods once", function() { - - $this->methodFilters->apply(['myMethod1', 'myMethod2'] , 'spec.hello_boy', function() { return 'Hello Boy!'; }); - - foreach (['myMethod1', 'myMethod2'] as $method) { - $filters = $this->methodFilters->filters($method); - expect($filters)->toBeAn('array')->toHaveLength(1); - - $closure = reset($filters); - expect($closure())->toBe('Hello Boy!'); - } - - }); - - }); - - describe("->detach()", function() { - - it("detaches a filter", function() { - - $this->methodFilters->apply('myMethod', 'spec.hello_world', function() { return 'Hello World!'; }); - $this->methodFilters->detach('myMethod', 'spec.hello_world'); - - $filters = $this->methodFilters->filters('myMethod'); - expect($filters)->toHaveLength(0); - - }); - - it("detaches filters by name", function() { - - $this->methodFilters->apply(['myMethod1', 'myMethod2'], 'spec.hello_boy', function() { return 'Hello Boy!'; }); - $this->methodFilters->detach(null, 'spec.hello_boy'); - - $filters = $this->methodFilters->filters('myMethod1'); - expect($filters)->toHaveLength(0); - - $filters = $this->methodFilters->filters('myMethod2'); - expect($filters)->toHaveLength(0); - - }); - - it("detaches all filters of a method", function() { - - $this->methodFilters->apply('myMethod', 'spec.hello_world', function() { return 'Hello World!'; }); - $this->methodFilters->apply('myMethod', 'spec.hello_beautiful_world', function() { return 'Hello Beautiful World!'; }); - $this->methodFilters->detach('myMethod'); - - $filters = $this->methodFilters->filters('myMethod'); - expect($filters)->toHaveLength(0); - - }); - - it("detaches all filters", function() { - - $this->methodFilters->apply('myMethod', 'spec.hello_world', function() { return 'Hello World!'; }); - $this->methodFilters->apply('myMethod', 'spec.hello_beautiful_world', function() { return 'Hello Beautiful World!'; }); - $this->methodFilters->apply(['myMethod1', 'myMethod2'], 'spec.hello_boy', function() { return 'Hello Boy!'; }); - $this->methodFilters->detach(); - - $filters = $this->methodFilters->filters('myMethod'); - expect($filters)->toHaveLength(0); - - $filters = $this->methodFilters->filters('myMethod1'); - expect($filters)->toHaveLength(0); - - $filters = $this->methodFilters->filters('myMethod2'); - expect($filters)->toHaveLength(0); - - }); - - it("clears all filters", function() { - - $this->methodFilters->apply('myMethod', 'spec.hello_world', function() { return 'Hello World!'; }); - $this->methodFilters->apply('myMethod', 'spec.hello_beautiful_world', function() { return 'Hello Beautiful World!'; }); - $this->methodFilters->apply(['myMethod1', 'myMethod2'], 'spec.hello_boy', function() { return 'Hello Boy!'; }); - $this->methodFilters->clear(); - - $filters = $this->methodFilters->filters('myMethod'); - expect($filters)->toHaveLength(0); - - $filters = $this->methodFilters->filters('myMethod1'); - expect($filters)->toHaveLength(0); - - $filters = $this->methodFilters->filters('myMethod2'); - expect($filters)->toHaveLength(0); - - }); - - }); - -}); diff --git a/spec/Suite/GivenSpec.php b/spec/Suite/Given.spec.php similarity index 54% rename from spec/Suite/GivenSpec.php rename to spec/Suite/Given.spec.php index bfcc51b2..8f89563b 100644 --- a/spec/Suite/GivenSpec.php +++ b/spec/Suite/Given.spec.php @@ -5,38 +5,48 @@ use Kahlan\Scope; use Kahlan\Given; -describe("Given", function() { +describe("Given", function () { - given('scope', function() { return 'root'; }); + given('scope', function () { + return 'root'; + }); - context("using the global `given()` function", function() { + context("using the global `given()` function", function () { - it("gets a lazy loadable variable", function() { + it("gets a lazy loadable variable", function () { - given('firstname', function() { return 'Willy'; }); + given('firstname', function () { + return 'Willy'; + }); expect($this->firstname)->toBe('Willy'); }); - it("lazy loads variables in cascades", function() { + it("lazy loads variables in cascades", function () { - given('firstname', function() { return 'Johnny'; }); - given('fullname', function() { + given('firstname', function () { + return 'Johnny'; + }); + given('fullname', function () { return "{$this->firstname} {$this->lastname}"; }); - given('lastname', function() { return 'Boy'; }); + given('lastname', function () { + return 'Boy'; + }); expect($this->fullname)->toBe('Johnny Boy'); }); - it("allows to reference a lazy loadable variable which get overrided", function() { + it("allows to reference a lazy loadable variable which get overrided", function () { - given('variable', function() { return []; }); - given('variable', function() { + given('variable', function () { + return []; + }); + given('variable', function () { $this->variable[] = 1; return $this->variable; }); - given('variable', function() { + given('variable', function () { $this->variable[] = 2; return $this->variable; }); @@ -44,14 +54,14 @@ }); - context("with a nested scope", function() { + context("with a nested scope", function () { $count = 0; - given('count', function() use (&$count) { + given('count', function () use (&$count) { return ++$count; }); - it("caches lazy loaded variables", function() { + it("caches lazy loaded variables", function () { expect($this->count)->toBe(1); expect($this->count)->toBe(1); @@ -59,7 +69,7 @@ }); - it("doesn't cache across specifications", function() { + it("doesn't cache across specifications", function () { expect($this->count)->toBe(2); expect($this->count)->toBe(2); @@ -68,32 +78,36 @@ }); }); - context('using a nested context', function() { + context('using a nested context', function () { - it("gets a lazy loadable variable defined in a parent context", function() { + it("gets a lazy loadable variable defined in a parent context", function () { expect($this->scope)->toBe('root'); }); - it("can override a lazy loadable variable defined in a parent context", function() { + it("can override a lazy loadable variable defined in a parent context", function () { - given('scope', function() { return 'nested'; }); + given('scope', function () { + return 'nested'; + }); expect($this->scope)->toBe('nested'); }); }); - context("using lazy loadable variables through `beforeEach()`", function() { + context("using lazy loadable variables through `beforeEach()`", function () { - beforeEach(function() { + beforeEach(function () { $this->value = $this->state; }); - given('state', function() { return 'some_state'; }); + given('state', function () { + return 'some_state'; + }); - it("makes lazy loadable variables loaded", function() { + it("makes lazy loadable variables loaded", function () { expect($this->value)->toBe('some_state'); @@ -101,20 +115,22 @@ }); - it("throw an exception when the second parameter is not a closure", function() { + it("throw an exception when the second parameter is not a closure", function () { - $closure = function() { + $closure = function () { given('some_name', 'some value'); }; expect($closure)->toThrow(new Exception("A closure is required by `Given` constructor.")); }); - it("throw an exception for reserved keywords", function() { + it("throw an exception for reserved keywords", function () { foreach (Scope::$blacklist as $keyword => $bool) { - $closure = function() use ($keyword) { - given($keyword, function() { return 'some value'; }); + $closure = function () use ($keyword) { + given($keyword, function () { + return 'some value'; + }); }; expect($closure)->toThrow(new Exception("Sorry `{$keyword}` is a reserved keyword, it can't be used as a scope variable.")); } @@ -123,12 +139,12 @@ }); - describe("->__get()", function() { + describe("->__get()", function () { - it("throw an new exception when trying to access an undefined variable through a given definition", function() { + it("throw an new exception when trying to access an undefined variable through a given definition", function () { - $closure = function() { - $given = new Given(function() { + $closure = function () { + $given = new Given(function () { return $this->undefinedVariable; }); $given(); @@ -139,4 +155,4 @@ }); -}); \ No newline at end of file +}); diff --git a/spec/Suite/Jit/InterceptorSpec.php b/spec/Suite/Jit/Interceptor.spec.php similarity index 86% rename from spec/Suite/Jit/InterceptorSpec.php rename to spec/Suite/Jit/Interceptor.spec.php index 0c60085b..71e78efd 100644 --- a/spec/Suite/Jit/InterceptorSpec.php +++ b/spec/Suite/Jit/Interceptor.spec.php @@ -6,14 +6,12 @@ use Kahlan\Jit\Patchers; use Kahlan\Jit\Interceptor; -use Kahlan\Plugin\Stub; - use Kahlan\Spec\Proxy\Autoloader; use Kahlan\Spec\Mock\Patcher; -describe("Interceptor", function() { +describe("Interceptor", function () { - before(function() { + beforeAll(function () { $this->previous = Interceptor::instance(); Interceptor::unpatch(); @@ -28,29 +26,29 @@ $this->cachePath = Dir::tempnam(null, 'cache'); }); - afterEach(function() { + afterEach(function () { Interceptor::unpatch(); }); - after(function() { + afterAll(function () { spl_autoload_register($this->composer); spl_autoload_unregister([$this->autoloader, 'loadClass']); Dir::remove($this->cachePath); Interceptor::load($this->previous); }); - describe("::patch()", function() { + describe("::patch()", function () { - it("patches the composer autoloader by default", function() { + it("patches the composer autoloader by default", function () { $interceptor = Interceptor::patch(['cachePath' => $this->cachePath]); expect($interceptor->originalInstance())->toBeAnInstanceOf("Composer\Autoload\ClassLoader"); }); - it("throws an exception if the autoloader has already been patched", function() { + it("throws an exception if the autoloader has already been patched", function () { - $closure = function() { + $closure = function () { Interceptor::patch(['cachePath' => $this->cachePath]); Interceptor::patch(['cachePath' => $this->cachePath]); }; @@ -58,7 +56,7 @@ }); - it("throws an exception if the autoloader has already been patched", function() { + it("throws an exception if the autoloader has already been patched", function () { spl_autoload_unregister([$this->autoloader, 'loadClass']); @@ -74,7 +72,7 @@ expect($message)->toBe("The loader option need to be a valid autoloader."); }); - it("allows to configure the autoloader method", function() { + it("allows to configure the autoloader method", function () { $interceptor = Interceptor::patch([ 'cachePath' => $this->cachePath, @@ -85,7 +83,7 @@ }); - it("throws an exception if the autoloader has already been patched", function() { + it("throws an exception if the autoloader has already been patched", function () { $interceptor = Interceptor::patch([ 'cachePath' => $this->cachePath, @@ -109,9 +107,9 @@ }); - describe("::unpatch()", function() { + describe("::unpatch()", function () { - it("detaches the patched autoloader", function() { + it("detaches the patched autoloader", function () { Interceptor::patch(['cachePath' => $this->cachePath]); @@ -123,7 +121,7 @@ }); - it("returns `false` if there's no patched autoloader", function() { + it("returns `false` if there's no patched autoloader", function () { Interceptor::patch(['cachePath' => $this->cachePath]); Interceptor::unpatch(); @@ -135,9 +133,9 @@ }); - describe("::load()", function() { + describe("::load()", function () { - it("auto unpatch when loading an interceptor autoloader", function() { + it("auto unpatch when loading an interceptor autoloader", function () { $interceptor = Interceptor::patch(['cachePath' => $this->cachePath]); @@ -155,9 +153,9 @@ }); - describe("::instance()", function() { + describe("::instance()", function () { - it("returns the interceptor autoloader", function() { + it("returns the interceptor autoloader", function () { $interceptor = Interceptor::patch(['cachePath' => $this->cachePath]); expect($interceptor)->toBeAnInstanceOf("Kahlan\Jit\Interceptor"); @@ -166,9 +164,9 @@ }); - describe("::composer()", function() { + describe("::composer()", function () { - it("returns the composer autoloader", function() { + it("returns the composer autoloader", function () { $composer = Interceptor::composer()[0]; expect($composer)->toBeAnInstanceOf("Composer\Autoload\ClassLoader"); @@ -177,9 +175,9 @@ }); - describe("->__construct()", function() { + describe("->__construct()", function () { - it("clear caches if `'clearCache'` is `true`", function() { + it("clear caches if `'clearCache'` is `true`", function () { touch($this->cachePath . DS . 'CachedFile.php'); @@ -192,7 +190,7 @@ }); - it("initializes watched files if passed to the constructor", function() { + it("initializes watched files if passed to the constructor", function () { $this->temp = Dir::tempnam(null, 'cache'); touch($this->temp . DS . 'watched1.php'); @@ -215,9 +213,9 @@ }); - describe("->findFile()", function() { + describe("->findFile()", function () { - it("deletages finds to patched autoloader", function() { + it("deletages finds to patched autoloader", function () { Interceptor::patch(['cachePath' => $this->cachePath]); @@ -228,7 +226,7 @@ }); - it("still finds path even with no patchers defined", function() { + it("still finds path even with no patchers defined", function () { $interceptor = Interceptor::patch(['cachePath' => $this->cachePath]); @@ -239,9 +237,9 @@ }); - context("with some patchers defined", function() { + context("with some patchers defined", function () { - beforeEach(function() { + beforeEach(function () { $this->interceptor = Interceptor::patch(['cachePath' => $this->cachePath]); @@ -254,12 +252,12 @@ }); - it("delegates find to patchers", function() { + it("delegates find to patchers", function () { - Stub::on($this->patcher1)->method('findFile', function($interceptor, $class, $file) { + allow($this->patcher1)->toReceive('findFile')->andRun(function ($interceptor, $class, $file) { return $file . '1'; }); - Stub::on($this->patcher2)->method('findFile', function($interceptor, $class, $file) { + allow($this->patcher2)->toReceive('findFile')->andRun(function ($interceptor, $class, $file) { return $file . '2'; }); @@ -273,24 +271,24 @@ }); - describe("->loadFile()", function() { + describe("->loadFile()", function () { - beforeEach(function() { + beforeEach(function () { $this->interceptor = Interceptor::patch(['cachePath' => $this->cachePath]); $this->loadFileNamespacePath = Dir::tempnam(null, 'loadFileNamespace'); $this->interceptor->addPsr4('loadFileNamespace\\', $this->loadFileNamespacePath); - $this->classBuilder = function($name) { + $this->classBuilder = function ($name) { return ""; }; }); - afterEach(function() { + afterEach(function () { Dir::remove($this->loadFileNamespacePath); }); - context("when interceptor doesn't watch additional files", function() { + context("when interceptor doesn't watch additional files", function () { - it("loads a file", function() { + it("loads a file", function () { $sourcePath = $this->loadFileNamespacePath . DS . 'ClassA.php'; file_put_contents($sourcePath, $this->classBuilder('ClassA')); @@ -300,7 +298,7 @@ }); - it("loads cached files", function() { + it("loads cached files", function () { $sourcePath = $this->loadFileNamespacePath . DS . 'ClassCached.php'; $body = $this->classBuilder('ClassCached'); @@ -313,11 +311,11 @@ }); - it("throws an exception for unexisting files", function() { + it("throws an exception for unexisting files", function () { $path = $this->loadFileNamespacePath . DS . 'ClassUnexisting.php'; - $closure= function() use ($path) { + $closure= function () use ($path) { $interceptor = Interceptor::instance(); $interceptor->loadFile($path); }; @@ -325,7 +323,7 @@ }); - it("caches a loaded files and set the cached file motification time to be the same as the source file", function() { + it("caches a loaded files and set the cached file motification time to be the same as the source file", function () { $sourcePath = $this->loadFileNamespacePath . DS . 'ClassB.php'; file_put_contents($sourcePath, $this->classBuilder('ClassB')); @@ -345,9 +343,9 @@ }); - context("when the interceptor watch some additional files", function() { + context("when the interceptor watch some additional files", function () { - beforeEach(function() { + beforeEach(function () { $this->currentTimestamp = time(); $this->watched1Timestamp = $this->currentTimestamp - 1 * 60; $this->watched2Timestamp = $this->currentTimestamp - 2 * 60; @@ -361,7 +359,7 @@ ]); }); - it("caches a file and set the cached file motification time to be the max timestamp between the watched and the source file", function() { + it("caches a file and set the cached file motification time to be the max timestamp between the watched and the source file", function () { file_put_contents($this->loadFileNamespacePath . DS . 'ClassC.php', $this->classBuilder('ClassC')); @@ -381,9 +379,9 @@ }); - describe("->loadFiles()", function() { + describe("->loadFiles()", function () { - it("loads a file", function() { + it("loads a file", function () { $interceptor = Interceptor::patch(['cachePath' => $this->cachePath]); @@ -398,9 +396,9 @@ }); - describe("->loadClass()", function() { + describe("->loadClass()", function () { - it("loads a class", function() { + it("loads a class", function () { $interceptor = Interceptor::patch(['cachePath' => $this->cachePath]); @@ -409,7 +407,7 @@ }); - it("bails out the patching process if the class has been excluded from being patched", function() { + it("bails out the patching process if the class has been excluded from being patched", function () { $interceptor = Interceptor::patch([ 'include' => ['allowed\\'], @@ -422,7 +420,7 @@ }); - it("loads and proccess patchable class", function() { + it("loads and proccess patchable class", function () { $interceptor = Interceptor::patch(['cachePath' => $this->cachePath]); $patcher = new Patcher(); @@ -433,7 +431,7 @@ }); - it("returns null when the class can't be loaded", function() { + it("returns null when the class can't be loaded", function () { $interceptor = Interceptor::patch(['cachePath' => $this->cachePath]); @@ -443,9 +441,9 @@ }); - describe("->findPath()", function() { + describe("->findPath()", function () { - it("finds a namespace path", function() { + it("finds a namespace path", function () { $interceptor = Interceptor::patch(['cachePath' => $this->cachePath]); @@ -454,7 +452,7 @@ }); - it("finds a PHP class path", function() { + it("finds a PHP class path", function () { $interceptor = Interceptor::patch(['cachePath' => $this->cachePath]); @@ -463,7 +461,7 @@ }); - it("finds a HH class path", function() { + it("finds a HH class path", function () { $interceptor = Interceptor::patch(['cachePath' => $this->cachePath]); @@ -472,7 +470,7 @@ }); - it("gives precedence to files", function() { + it("gives precedence to files", function () { $interceptor = Interceptor::patch(['cachePath' => $this->cachePath]); @@ -481,7 +479,7 @@ }); - it("forces the returned path to be a directory", function() { + it("forces the returned path to be a directory", function () { $interceptor = Interceptor::patch(['cachePath' => $this->cachePath]); @@ -492,9 +490,9 @@ }); - describe("->__call()", function() { + describe("->__call()", function () { - it("deletages calls to patched autoloader", function() { + it("deletages calls to patched autoloader", function () { $interceptor = Interceptor::patch(['cachePath' => $this->cachePath]); @@ -506,9 +504,9 @@ }); - describe("->allowed()", function() { + describe("->allowed()", function () { - it("returns true by default", function() { + it("returns true by default", function () { $interceptor = Interceptor::patch([ 'include' => ['*'], @@ -520,7 +518,7 @@ }); - it("returns true if the class match the include", function() { + it("returns true if the class match the include", function () { $interceptor = Interceptor::patch([ 'include' => ['allowed\\'], @@ -535,7 +533,7 @@ }); - it("processes exclude first", function() { + it("processes exclude first", function () { $interceptor = Interceptor::patch([ 'exclude' => ['namespace\\notallowed\\'], @@ -553,9 +551,9 @@ }); - describe("->cachePath()", function() { + describe("->cachePath()", function () { - it("returns the cache path", function() { + it("returns the cache path", function () { $interceptor = Interceptor::patch(); @@ -567,13 +565,13 @@ }); - describe("->cache()", function() { + describe("->cache()", function () { - it("throws an exception if no cache has been disabled", function() { + it("throws an exception if no cache has been disabled", function () { $this->temp = Dir::tempnam(null, 'cache'); - $closure = function() { + $closure = function () { $interceptor = Interceptor::patch(['cachePath' => false]); $interceptor->cache($this->temp . DS . 'ClassToCache.php', ''); }; @@ -583,19 +581,19 @@ Dir::remove($this->temp); }); - context("with a valid cache path", function() { + context("with a valid cache path", function () { - beforeEach(function() { + beforeEach(function () { $this->interceptor = Interceptor::patch(['cachePath' => $this->cachePath]); $this->temp = Dir::tempnam(null, 'cache'); }); - afterEach(function() { + afterEach(function () { Dir::remove($this->temp); }); - it("caches a file and into a subtree similar to the source location", function() { + it("caches a file and into a subtree similar to the source location", function () { $path = $this->temp . DS . 'ClassToCache.php'; $cached = $this->interceptor->cache($path, ''); @@ -607,9 +605,9 @@ }); - describe("->cached()", function() { + describe("->cached()", function () { - it("returns false when trying to get an unexisting file", function() { + it("returns false when trying to get an unexisting file", function () { $interceptor = Interceptor::patch(['cachePath' => $this->cachePath]); @@ -619,7 +617,7 @@ }); - it("returns false when trying no cache path has been defined", function() { + it("returns false when trying no cache path has been defined", function () { $interceptor = Interceptor::patch(['cachePath' => false]); @@ -629,45 +627,45 @@ }); - context("when the interceptor doesn't watch some additional files", function() { + context("when the interceptor doesn't watch some additional files", function () { - beforeEach(function() { + beforeEach(function () { $this->interceptor = Interceptor::patch(['cachePath' => $this->cachePath]); $this->temp = Dir::tempnam(null, 'cache'); $this->cached = $this->interceptor->cache($this->temp . DS . 'CachedClass.php', ''); }); - afterEach(function() { + afterEach(function () { Dir::remove($this->temp); }); - it("returns the cached file path if the modified timestamp of the cached file is up to date", function() { + it("returns the cached file path if the modified timestamp of the cached file is up to date", function () { touch($this->temp . DS . "CachedClass.php", time() - 1); expect($this->interceptor->cached($this->temp . DS . 'CachedClass.php'))->not->toBe(false); }); - it("returns false if the modified timestamp of the cached file is outdated", function() { + it("returns false if the modified timestamp of the cached file is outdated", function () { touch($this->temp . DS . "CachedClass.php", time() + 1); expect($this->interceptor->cached($this->temp . DS . 'CachedClass.php'))->toBe(false); }); }); - context("when the interceptor watch some additional files", function() { + context("when the interceptor watch some additional files", function () { - beforeEach(function() { + beforeEach(function () { $this->interceptor = Interceptor::patch(['cachePath' => $this->cachePath]); $this->temp = Dir::tempnam(null, 'cache'); $this->cached = $this->interceptor->cache($this->temp . DS . 'CachedClass.php', ''); }); - afterEach(function() { + afterEach(function () { Dir::remove($this->temp); }); - it("returns the cached file path if the modified timestamp of the cached file is up to date", function() { + it("returns the cached file path if the modified timestamp of the cached file is up to date", function () { $time = time(); touch($this->temp . DS . 'watched1.php', $time - 1); @@ -679,7 +677,7 @@ expect($this->interceptor->cached($this->temp . DS . 'CachedClass.php'))->not->toBe(false); }); - it("returns false if the modified timestamp of the cached file is outdated", function() { + it("returns false if the modified timestamp of the cached file is outdated", function () { $time = time(); touch($this->temp . DS . 'watched1.php', $time - 1); @@ -691,7 +689,7 @@ expect($this->interceptor->cached($this->temp . DS . 'CachedClass.php'))->toBe(false); }); - it("returns false if the modified timestamp of a watched file is outdated", function() { + it("returns false if the modified timestamp of a watched file is outdated", function () { $time = time(); touch($this->temp . DS . 'watched1.php', $time - 1); @@ -715,9 +713,9 @@ }); - describe("->clearCache()", function() { + describe("->clearCache()", function () { - beforeEach(function() { + beforeEach(function () { $this->customCachePath = Dir::tempnam(null, 'cache'); $this->interceptor = Interceptor::patch(['cachePath' => $this->customCachePath]); @@ -726,18 +724,18 @@ $this->interceptor->cache($this->temp . DS . 'nestedDir/CachedClass2.php', ''); }); - afterEach(function() { + afterEach(function () { Dir::remove($this->temp); }); - it("clears the cache", function() { + it("clears the cache", function () { $this->interceptor->clearCache(); expect(file_exists($this->customCachePath))->toBe(false); }); - it("bails out if the cache has already been cleared", function() { + it("bails out if the cache has already been cleared", function () { $this->interceptor->clearCache(); $this->interceptor->clearCache(); @@ -747,9 +745,9 @@ }); - describe("->watch()/unwatch()", function() { + describe("->watch()/unwatch()", function () { - it("add some file to be watched", function() { + it("add some file to be watched", function () { $this->temp = Dir::tempnam(null, 'cache'); touch($this->temp . DS . 'watched1.php'); @@ -781,4 +779,4 @@ }); -}); \ No newline at end of file +}); diff --git a/spec/Suite/Jit/Node/FunctionDefSpec.php b/spec/Suite/Jit/Node/FunctionDef.spec.php similarity index 85% rename from spec/Suite/Jit/Node/FunctionDefSpec.php rename to spec/Suite/Jit/Node/FunctionDef.spec.php index c2557ac2..c7ad334b 100644 --- a/spec/Suite/Jit/Node/FunctionDefSpec.php +++ b/spec/Suite/Jit/Node/FunctionDef.spec.php @@ -3,11 +3,11 @@ use Kahlan\Jit\Node\FunctionDef; -describe("FunctionDef", function() { +describe("FunctionDef", function () { - describe("->argsToParams()", function() { + describe("->argsToParams()", function () { - it("builds a list of params from function arguments", function() { + it("builds a list of params from function arguments", function () { $node = new FunctionDef(); $node->args = [ '$required', diff --git a/spec/Suite/Jit/ParserSpec.php b/spec/Suite/Jit/Parser.spec.php similarity index 90% rename from spec/Suite/Jit/ParserSpec.php rename to spec/Suite/Jit/Parser.spec.php index 8e7f5c7d..6d7b0114 100644 --- a/spec/Suite/Jit/ParserSpec.php +++ b/spec/Suite/Jit/Parser.spec.php @@ -3,10 +3,10 @@ use Kahlan\Jit\Parser; -describe("Parser", function() { +describe("Parser", function () { - beforeEach(function() { - $this->flattenTree = function($nodes, $self) { + beforeEach(function () { + $this->flattenTree = function ($nodes, $self) { $result = [] ; foreach ($nodes as $node) { if (count($node->tree)) { @@ -19,16 +19,17 @@ }; }); - describe("->parse()", function() { + describe("->parse()", function () { + + it("parses consistently", function () { - it("parses consistently", function() { $sample = file_get_contents('spec/Fixture/Jit/Parser/Sample.php'); $parsed = Parser::parse($sample); expect(Parser::unparse($parsed))->toBe($sample); }); - it("parses syntaxically broken use statement and doesn't crash", function() { + it("parses syntaxically broken use statement and doesn't crash", function () { $code = ""; $parsed = Parser::parse($code); @@ -36,7 +37,7 @@ }); - it("parses functions", function() { + it("parses functions", function () { $sample = file_get_contents('spec/Fixture/Jit/Parser/Function.php'); $root = Parser::parse($sample); @@ -60,7 +61,7 @@ }); - it("parses PHP directly when the `'php'` option is set to true", function() { + it("parses PHP directly when the `'php'` option is set to true", function () { $code = "namespace MyNamespace;"; $root = Parser::parse($code, ['php' => true]); @@ -71,14 +72,14 @@ }); - it("correctly populates the `->inPhp` attribute", function() { + it("correctly populates the `->inPhp` attribute", function () { $sample = file_get_contents('spec/Fixture/Jit/Parser/Sample.php'); $root = Parser::parse($sample); $plain = []; foreach ($this->flattenTree($root->tree, $this) as $node) { - if(!$node->inPhp) { + if (!$node->inPhp) { $plain[] = (string) $node; } } @@ -93,7 +94,7 @@ ]); }); - it("correctly populates the `->isGenerator` attribute", function() { + it("correctly populates the `->isGenerator` attribute", function () { skipIf(version_compare(phpversion(), '5.5', '<')); @@ -113,9 +114,9 @@ }); - describe("->debug()", function() { + describe("->debug()", function () { - it("attaches the correct lines", function() { + it("attaches the correct lines", function () { $filename = 'spec/Fixture/Jit/Parser/Sample'; $content = file_get_contents($filename . '.php'); @@ -128,7 +129,7 @@ }); - it("parses files with no namespace", function() { + it("parses files with no namespace", function () { $filename = 'spec/Fixture/Jit/Parser/NoNamespace'; $content = file_get_contents($filename . '.php'); @@ -141,7 +142,7 @@ }); - it("parses heredoc", function() { + it("parses heredoc", function () { $filename = 'spec/Fixture/Jit/Parser/Heredoc'; $content = file_get_contents($filename . '.php'); @@ -154,7 +155,7 @@ }); - it("parses strings", function() { + it("parses strings", function () { $filename = 'spec/Fixture/Jit/Parser/String'; $content = file_get_contents($filename . '.php'); @@ -167,7 +168,7 @@ }); - it("parses char at syntax", function() { + it("parses char at syntax", function () { $filename = 'spec/Fixture/Jit/Parser/CharAtSyntax'; $content = file_get_contents($filename . '.php'); @@ -180,7 +181,7 @@ }); - it("parses closures", function() { + it("parses closures", function () { $filename = 'spec/Fixture/Jit/Parser/Closure'; $content = file_get_contents($filename . '.php'); @@ -193,7 +194,7 @@ }); - it("parses switch cases", function() { + it("parses switch cases", function () { $filename = 'spec/Fixture/Jit/Parser/Switch'; $content = file_get_contents($filename . '.php'); @@ -206,7 +207,7 @@ }); - it("parses uses", function() { + it("parses uses", function () { $filename = 'spec/Fixture/Jit/Parser/Uses'; $content = file_get_contents($filename . '.php'); @@ -228,7 +229,7 @@ }); - it("parses ::class syntax", function() { + it("parses ::class syntax", function () { $filename = 'spec/Fixture/Jit/Parser/StaticClassKeyword'; $content = file_get_contents($filename . '.php'); @@ -241,7 +242,7 @@ }); - it("parses anonymous class", function() { + it("parses anonymous class", function () { $filename = 'spec/Fixture/Jit/Parser/AnonymousClass'; $content = file_get_contents($filename . '.php'); @@ -254,12 +255,13 @@ }); - it("parses extends", function() { + it("parses extends", function () { $sample = file_get_contents('spec/Fixture/Jit/Parser/Extends.php'); $root = Parser::parse($sample); - $check = 0;; + $check = 0; + ; foreach ($root->tree as $node) { if ($node->type !== 'namespace') { @@ -296,12 +298,13 @@ expect($check)->toBe(5); }); - it("parses implements", function() { + it("parses implements", function () { $sample = file_get_contents('spec/Fixture/Jit/Parser/Implements.php'); $root = Parser::parse($sample); - $check = 0;; + $check = 0; + ; foreach ($root->tree as $node) { if ($node->type !== 'namespace') { diff --git a/spec/Suite/Jit/Patcher/LayerSpec.php b/spec/Suite/Jit/Patcher/Layer.spec.php similarity index 71% rename from spec/Suite/Jit/Patcher/LayerSpec.php rename to spec/Suite/Jit/Patcher/Layer.spec.php index a666f83b..0c3cff39 100644 --- a/spec/Suite/Jit/Patcher/LayerSpec.php +++ b/spec/Suite/Jit/Patcher/Layer.spec.php @@ -4,11 +4,20 @@ use Kahlan\Jit\Parser; use Kahlan\Jit\Patcher\Layer; -describe("Layer", function() { +describe("Layer", function () { + + beforeEach(function () { + $this->path = 'spec/Fixture/Jit/Patcher/Layer'; + $this->patcher = new Layer([ + 'override' => [ + 'Kahlan\Analysis\Inspector' + ] + ]); + }); - describe("->findFile()", function() { + describe("->findFile()", function () { - it("returns the file path to patch", function() { + it("returns the file path to patch", function () { $layer = new Layer(); expect($layer->findFile(null, null, '/some/file/path.php'))->toBe('/some/file/path.php'); @@ -17,18 +26,9 @@ }); - describe("->process()", function() { - - beforeEach(function() { - $this->path = 'spec/Fixture/Jit/Patcher/Layer'; - $this->patcher = new Layer([ - 'override' => [ - 'Kahlan\Analysis\Inspector' - ] - ]); - }); + describe("->process()", function () { - it("patches class's extends", function() { + it("patches class's extends", function () { $nodes = Parser::parse(file_get_contents($this->path . '/Layer.php')); $actual = Parser::unparse($this->patcher->process($nodes)); @@ -40,7 +40,7 @@ class Inspector extends InspectorKLAYER { -}class InspectorKLAYER extends \\Kahlan\\Analysis\\Inspector { public static function inspect(\$class) {\$__KPOINTCUT_ARGS__ = func_get_args(); \$__KPOINTCUT_SELF__ = isset(\$this) ? \$this : get_called_class(); if (\$__KPOINTCUT__ = \\Kahlan\\Plugin\\Pointcut::before(__METHOD__, \$__KPOINTCUT_SELF__, \$__KPOINTCUT_ARGS__)) { \$r = \$__KPOINTCUT__(\$__KPOINTCUT_SELF__, \$__KPOINTCUT_ARGS__); return \$r; }return parent::inspect(\$class);} public static function parameters(\$class, \$method, \$data = NULL) {\$__KPOINTCUT_ARGS__ = func_get_args(); \$__KPOINTCUT_SELF__ = isset(\$this) ? \$this : get_called_class(); if (\$__KPOINTCUT__ = \\Kahlan\\Plugin\\Pointcut::before(__METHOD__, \$__KPOINTCUT_SELF__, \$__KPOINTCUT_ARGS__)) { \$r = \$__KPOINTCUT__(\$__KPOINTCUT_SELF__, \$__KPOINTCUT_ARGS__); return \$r; }return parent::parameters(\$class, \$method, \$data);} public static function typehint(\$parameter) {\$__KPOINTCUT_ARGS__ = func_get_args(); \$__KPOINTCUT_SELF__ = isset(\$this) ? \$this : get_called_class(); if (\$__KPOINTCUT__ = \\Kahlan\\Plugin\\Pointcut::before(__METHOD__, \$__KPOINTCUT_SELF__, \$__KPOINTCUT_ARGS__)) { \$r = \$__KPOINTCUT__(\$__KPOINTCUT_SELF__, \$__KPOINTCUT_ARGS__); return \$r; }return parent::typehint(\$parameter);}} +}class InspectorKLAYER extends \\Kahlan\\Analysis\\Inspector { public static function inspect(\$class) {\$__KPOINTCUT_ARGS__ = func_get_args(); \$__KPOINTCUT_SELF__ = isset(\$this) ? \$this : get_called_class(); if (\$__KPOINTCUT__ = \\Kahlan\\Plugin\\Pointcut::before(__METHOD__, \$__KPOINTCUT_SELF__, \$__KPOINTCUT_ARGS__)) { \$r = \$__KPOINTCUT__(\$__KPOINTCUT_ARGS__, \$__KPOINTCUT_SELF__); return \$r; }return parent::inspect(\$class);} public static function parameters(\$class, \$method, \$data = NULL) {\$__KPOINTCUT_ARGS__ = func_get_args(); \$__KPOINTCUT_SELF__ = isset(\$this) ? \$this : get_called_class(); if (\$__KPOINTCUT__ = \\Kahlan\\Plugin\\Pointcut::before(__METHOD__, \$__KPOINTCUT_SELF__, \$__KPOINTCUT_ARGS__)) { \$r = \$__KPOINTCUT__(\$__KPOINTCUT_ARGS__, \$__KPOINTCUT_SELF__); return \$r; }return parent::parameters(\$class, \$method, \$data);} public static function typehint(\$parameter) {\$__KPOINTCUT_ARGS__ = func_get_args(); \$__KPOINTCUT_SELF__ = isset(\$this) ? \$this : get_called_class(); if (\$__KPOINTCUT__ = \\Kahlan\\Plugin\\Pointcut::before(__METHOD__, \$__KPOINTCUT_SELF__, \$__KPOINTCUT_ARGS__)) { \$r = \$__KPOINTCUT__(\$__KPOINTCUT_ARGS__, \$__KPOINTCUT_SELF__); return \$r; }return parent::typehint(\$parameter);}} EOD; @@ -48,7 +48,7 @@ class Inspector extends InspectorKLAYER }); - it("bails out when `'override'` is empty", function() { + it("bails out when `'override'` is empty", function () { $this->patcher = new Layer([]); $nodes = Parser::parse(file_get_contents($this->path . '/Layer.php')); @@ -58,7 +58,7 @@ class Inspector extends InspectorKLAYER }); - it("doesn't patch classes which are not present in the `'override'` option", function() { + it("doesn't patch classes which are not present in the `'override'` option", function () { $this->patcher = new Layer([ 'override' => [ @@ -84,4 +84,15 @@ class Inspector extends \\Kahlan\\Analysis\\Inspector }); + + describe("->patchable()", function () { + + it("return `true`", function () { + + expect($this->patcher->patchable('SomeClass'))->toBe(true); + + }); + + }); + }); diff --git a/spec/Suite/Jit/Patcher/Monkey.spec.php b/spec/Suite/Jit/Patcher/Monkey.spec.php new file mode 100644 index 00000000..b95ea48b --- /dev/null +++ b/spec/Suite/Jit/Patcher/Monkey.spec.php @@ -0,0 +1,75 @@ +path = 'spec/Fixture/Jit/Patcher/Monkey'; + $this->patcher = new Monkey(); + }); + + describe("->process()", function () { + + it("patches class's methods", function () { + + $nodes = Parser::parse(file_get_contents($this->path . '/Class.php')); + $expected = file_get_contents($this->path . '/ClassProcessed.php'); + $actual = Parser::unparse($this->patcher->process($nodes)); + expect($actual)->toBe($expected); + + }); + + it("patches trait's methods", function () { + + $nodes = Parser::parse(file_get_contents($this->path . '/Trait.php')); + $expected = file_get_contents($this->path . '/TraitProcessed.php'); + $actual = Parser::unparse($this->patcher->process($nodes)); + expect($actual)->toBe($expected); + + }); + + it("patches plain php file", function () { + + $nodes = Parser::parse(file_get_contents($this->path . '/Plain.php')); + $expected = file_get_contents($this->path . '/PlainProcessed.php'); + $actual = Parser::unparse($this->patcher->process($nodes)); + expect($actual)->toBe($expected); + + }); + + it("patches errored php file", function () { + + $nodes = Parser::parse(file_get_contents($this->path . '/Errored.php')); + $expected = file_get_contents($this->path . '/ErroredProcessed.php'); + $actual = Parser::unparse($this->patcher->process($nodes)); + expect($actual)->toBe($expected); + + }); + + }); + + describe("->patchable()", function () { + + it("return `true`", function () { + + expect($this->patcher->patchable('SomeClass'))->toBe(true); + + }); + + }); + + describe("::blacklisted()", function () { + + it("checks that blacklisted function returns `false`", function () { + + foreach (Monkey::blacklisted() as $name) { + expect(Monkey::blacklisted($name))->toBe(true); + } + + }); + + }); +}); diff --git a/spec/Suite/Jit/Patcher/MonkeySpec.php b/spec/Suite/Jit/Patcher/MonkeySpec.php deleted file mode 100644 index 6af88c0b..00000000 --- a/spec/Suite/Jit/Patcher/MonkeySpec.php +++ /dev/null @@ -1,45 +0,0 @@ -process()", function() { - - beforeEach(function() { - $this->path = 'spec/Fixture/Jit/Patcher/Monkey'; - $this->patcher = new Monkey(); - }); - - it("patches class's methods", function() { - - $nodes = Parser::parse(file_get_contents($this->path . '/Class.php')); - $expected = file_get_contents($this->path . '/ClassProcessed.php'); - $actual = Parser::unparse($this->patcher->process($nodes)); - expect($actual)->toBe($expected); - - }); - - it("patches trait's methods", function() { - - $nodes = Parser::parse(file_get_contents($this->path . '/Trait.php')); - $expected = file_get_contents($this->path . '/TraitProcessed.php'); - $actual = Parser::unparse($this->patcher->process($nodes)); - expect($actual)->toBe($expected); - - }); - - it("patches plain php file", function() { - - $nodes = Parser::parse(file_get_contents($this->path . '/Plain.php')); - $expected = file_get_contents($this->path . '/PlainProcessed.php'); - $actual = Parser::unparse($this->patcher->process($nodes)); - expect($actual)->toBe($expected); - - }); - - }); - -}); diff --git a/spec/Suite/Jit/Patcher/PointcutSpec.php b/spec/Suite/Jit/Patcher/Pointcut.spec.php similarity index 70% rename from spec/Suite/Jit/Patcher/PointcutSpec.php rename to spec/Suite/Jit/Patcher/Pointcut.spec.php index 31016bae..4438891b 100644 --- a/spec/Suite/Jit/Patcher/PointcutSpec.php +++ b/spec/Suite/Jit/Patcher/Pointcut.spec.php @@ -4,16 +4,16 @@ use Kahlan\Jit\Parser; use Kahlan\Jit\Patcher\Pointcut; -describe("Pointcut", function() { +describe("Pointcut", function () { - describe("->process()", function() { + beforeEach(function () { + $this->path = 'spec/Fixture/Jit/Patcher/Pointcut'; + $this->patcher = new Pointcut(); + }); - beforeEach(function() { - $this->path = 'spec/Fixture/Jit/Patcher/Pointcut'; - $this->patcher = new Pointcut(); - }); + describe("->process()", function () { - it("adds an entry point to methods and wrap function call for classes", function() { + it("adds an entry point to methods and wrap function call for classes", function () { $nodes = Parser::parse(file_get_contents($this->path . '/Simple.php')); if (version_compare(phpversion(), '5.5', '<')) { @@ -26,7 +26,7 @@ }); - it("adds an entry point to methods and wrap function call for traits", function() { + it("adds an entry point to methods and wrap function call for traits", function () { $nodes = Parser::parse(file_get_contents($this->path . '/SimpleTrait.php')); $expected = file_get_contents($this->path . '/SimpleTraitProcessed.php'); @@ -37,4 +37,14 @@ }); + describe("->patchable()", function () { + + it("return `true`", function () { + + expect($this->patcher->patchable('SomeClass'))->toBe(true); + + }); + + }); + }); diff --git a/spec/Suite/Jit/Patcher/Quit.spec.php b/spec/Suite/Jit/Patcher/Quit.spec.php new file mode 100644 index 00000000..b48e63fc --- /dev/null +++ b/spec/Suite/Jit/Patcher/Quit.spec.php @@ -0,0 +1,36 @@ +path = 'spec/Fixture/Jit/Patcher/Quit'; + $this->patcher = new Quit(); + }); + + describe("->process()", function () { + + it("patches class's methods", function () { + + $nodes = Parser::parse(file_get_contents($this->path . '/File.php')); + $expected = file_get_contents($this->path . '/FileProcessed.php'); + $actual = Parser::unparse($this->patcher->process($nodes)); + expect($actual)->toBe($expected); + + }); + + }); + + describe("->patchable()", function () { + + it("return `true`", function () { + + expect($this->patcher->patchable('SomeClass'))->toBe(true); + + }); + + }); +}); diff --git a/spec/Suite/Jit/Patcher/QuitSpec.php b/spec/Suite/Jit/Patcher/QuitSpec.php deleted file mode 100644 index beb807ef..00000000 --- a/spec/Suite/Jit/Patcher/QuitSpec.php +++ /dev/null @@ -1,27 +0,0 @@ -process()", function() { - - beforeEach(function() { - $this->path = 'spec/Fixture/Jit/Patcher/Quit'; - $this->patcher = new Quit(); - }); - - it("patches class's methods", function() { - - $nodes = Parser::parse(file_get_contents($this->path . '/File.php')); - $expected = file_get_contents($this->path . '/FileProcessed.php'); - $actual = Parser::unparse($this->patcher->process($nodes)); - expect($actual)->toBe($expected); - - }); - - }); - -}); diff --git a/spec/Suite/Jit/Patcher/RebaseSpec.php b/spec/Suite/Jit/Patcher/Rebase.spec.php similarity index 51% rename from spec/Suite/Jit/Patcher/RebaseSpec.php rename to spec/Suite/Jit/Patcher/Rebase.spec.php index 4545cb8a..018415d8 100644 --- a/spec/Suite/Jit/Patcher/RebaseSpec.php +++ b/spec/Suite/Jit/Patcher/Rebase.spec.php @@ -4,16 +4,16 @@ use Kahlan\Jit\Parser; use Kahlan\Jit\Patcher\Rebase; -describe("Rebase", function() { +describe("Rebase", function () { - describe("->process()", function() { + beforeEach(function () { + $this->path = 'spec/Fixture/Jit/Patcher/Rebase'; + $this->patcher = new Rebase(); + }); - beforeEach(function() { - $this->path = 'spec/Fixture/Jit/Patcher/Rebase'; - $this->patcher = new Rebase(); - }); + describe("->process()", function () { - it("patches class's methods", function() { + it("patches class's methods", function () { $nodes = Parser::parse(file_get_contents($this->path . '/Rebase.php')); $expected = file_get_contents($this->path . '/RebaseProcessed.php'); @@ -24,4 +24,13 @@ }); + describe("->patchable()", function () { + + it("return `true`", function () { + + expect($this->patcher->patchable('SomeClass'))->toBe(true); + + }); + + }); }); diff --git a/spec/Suite/Jit/PatchersSpec.php b/spec/Suite/Jit/Patchers.spec.php similarity index 65% rename from spec/Suite/Jit/PatchersSpec.php rename to spec/Suite/Jit/Patchers.spec.php index 80b2716c..0a5f0741 100644 --- a/spec/Suite/Jit/PatchersSpec.php +++ b/spec/Suite/Jit/Patchers.spec.php @@ -2,21 +2,21 @@ namespace Kahlan\Spec\Jit\Suite; use Kahlan\Arg; -use Kahlan\Plugin\Stub; +use Kahlan\Plugin\Double; use Kahlan\Jit\Patchers; -describe("Patchers", function() { +describe("Patchers", function () { - beforeEach(function(){ + beforeEach(function () { $this->patchers = new Patchers; }); - describe("->add/get()", function() { + describe("->add/get()", function () { - it("stores a patcher", function() { + it("stores a patcher", function () { - $stub = Stub::create(); + $stub = Double::instance(); $this->patchers->add('my_patcher', $stub); $actual = $this->patchers->get('my_patcher'); @@ -24,7 +24,7 @@ }); - it("returns `false` if patcher are not objects", function() { + it("returns `false` if patcher are not objects", function () { expect($this->patchers->add('my_patcher', "not an object"))->toBe(false); @@ -32,9 +32,9 @@ }); - describe("->get()", function() { + describe("->get()", function () { - it("returns `null` for an unexisting patcher", function() { + it("returns `null` for an unexisting patcher", function () { $actual = $this->patchers->get('my_patcher'); expect($actual)->toBe(null); @@ -43,11 +43,11 @@ }); - describe("->exists()", function() { + describe("->exists()", function () { - it("returns `true` for an existing patcher", function() { + it("returns `true` for an existing patcher", function () { - $stub = Stub::create(); + $stub = Double::instance(); $this->patchers->add('my_patcher', $stub); $actual = $this->patchers->exists('my_patcher'); @@ -55,7 +55,7 @@ }); - it("returns `false` for an unexisting patcher", function() { + it("returns `false` for an unexisting patcher", function () { $actual = $this->patchers->exists('my_patcher'); expect($actual)->toBe(false); @@ -64,11 +64,11 @@ }); - describe("->remove()", function() { + describe("->remove()", function () { - it("removes a patcher", function() { + it("removes a patcher", function () { - $stub = Stub::create(); + $stub = Double::instance(); $this->patchers->add('my_patcher', $stub); $actual = $this->patchers->exists('my_patcher'); @@ -83,11 +83,11 @@ }); - describe("->clear()", function() { + describe("->clear()", function () { - it("clears all patchers", function() { + it("clears all patchers", function () { - $stub = Stub::create(); + $stub = Double::instance(); $this->patchers->add('my_patcher', $stub); $actual = $this->patchers->exists('my_patcher'); @@ -102,17 +102,17 @@ }); - describe("->patchable()", function() { + describe("->patchable()", function () { - it("runs `true` when at least one patcher consider a class as patchable", function() { + it("runs `true` when at least one patcher consider a class as patchable", function () { - $stub1 = Stub::create(); - Stub::on($stub1)->method('patchable')->andReturn(false); + $stub1 = Double::instance(); + allow($stub1)->toReceive('patchable')->andReturn(false); $this->patchers->add('patcher1', $stub1); - $stub2 = Stub::create(); + $stub2 = Double::instance(); $this->patchers->add('patcher2', $stub2); - Stub::on($stub2)->method('patchable')->andReturn(true); + allow($stub2)->toReceive('patchable')->andReturn(true); expect($stub1)->toReceive('patchable')->with('ClassName'); expect($stub2)->toReceive('patchable')->with('ClassName'); @@ -121,15 +121,15 @@ }); - it("runs `false` when at no patcher consider a class as patchable", function() { + it("runs `false` when at no patcher consider a class as patchable", function () { - $stub1 = Stub::create(); - Stub::on($stub1)->method('patchable')->andReturn(false); + $stub1 = Double::instance(); + allow($stub1)->toReceive('patchable')->andReturn(false); $this->patchers->add('patcher1', $stub1); - $stub2 = Stub::create(); + $stub2 = Double::instance(); $this->patchers->add('patcher2', $stub2); - Stub::on($stub2)->method('patchable')->andReturn(false); + allow($stub2)->toReceive('patchable')->andReturn(false); expect($stub1)->toReceive('patchable')->with('ClassName'); expect($stub2)->toReceive('patchable')->with('ClassName'); @@ -140,20 +140,20 @@ }); - describe("->process()", function() { + describe("->process()", function () { - it("runs a method on all patchers", function() { + it("runs a method on all patchers", function () { - $stub1 = Stub::create(); + $stub1 = Double::instance(); $this->patchers->add('patcher1', $stub1); - $stub2 = Stub::create(); + $stub2 = Double::instance(); $this->patchers->add('patcher2', $stub2); $path = 'tmp/hello_world.php'; $code = "patchers->process(''))->toBe(''); @@ -173,31 +173,31 @@ }); - describe("->findFile()", function() { + describe("->findFile()", function () { - beforeEach(function() { - $this->loader = Stub::create(); - $this->class = Stub::classname(); + beforeEach(function () { + $this->loader = Double::instance(); + $this->class = Double::classname(); $this->file = 'some/path/file.php'; - $this->stub1 = Stub::create(); + $this->stub1 = Double::instance(); $this->patchers->add('patcher1', $this->stub1); - $this->stub2 = Stub::create(); + $this->stub2 = Double::instance(); $this->patchers->add('patcher2', $this->stub2); $file = $this->file; - Stub::on($this->stub1)->method('findFile', function() use ($file) { + allow($this->stub1)->toReceive('findFile')->andRun(function () use ($file) { return $file; }); - Stub::on($this->stub2)->method('findFile', function() use ($file) { + allow($this->stub2)->toReceive('findFile')->andRun(function () use ($file) { return $file; }); }); - it("runs findFile() on all patchers", function() { + it("runs findFile() on all patchers", function () { expect($this->stub1)->toReceive('findFile')->with($this->loader, $this->class, $this->file); expect($this->stub2)->toReceive('findFile')->with($this->loader, $this->class, $this->file); @@ -207,11 +207,11 @@ }); - it("returns patchers overriding if available", function() { + it("returns patchers overriding if available", function () { $path = 'new/path/file.php'; - Stub::on($this->stub2)->method('findFile', function() use ($path) { + allow($this->stub2)->toReceive('findFile')->andRun(function () use ($path) { return $path; }); @@ -222,4 +222,4 @@ }); -}); \ No newline at end of file +}); diff --git a/spec/Suite/Jit/TokenStreamSpec.php b/spec/Suite/Jit/TokenStream.spec.php similarity index 71% rename from spec/Suite/Jit/TokenStreamSpec.php rename to spec/Suite/Jit/TokenStream.spec.php index 923f7ac1..14e509eb 100644 --- a/spec/Suite/Jit/TokenStreamSpec.php +++ b/spec/Suite/Jit/TokenStream.spec.php @@ -4,9 +4,9 @@ use Exception; use Kahlan\Jit\TokenStream; -describe("TokenStream", function() { +describe("TokenStream", function () { - beforeEach(function() { + beforeEach(function () { $this->code = <<len = count(token_get_all($this->code)); }); - describe("->load", function() { + describe("->load", function () { - it("wraps passed code with PHP tags when the `'wrap'` option is used", function() { + it("wraps passed code with PHP tags when the `'wrap'` option is used", function () { $stream = new TokenStream([ 'source' => 'class HelloWorld {}', @@ -37,16 +37,16 @@ public function hello() { }); - describe("->current()", function() { + describe("->current()", function () { - it("gets the current token value", function() { + it("gets the current token value", function () { $actual = $this->stream->current(); expect($actual)->toBe("stream->current(true); expect($actual)->toBe([T_OPEN_TAG, "next()", function() { + describe("->next()", function () { - it("moves next", function() { + it("moves next", function () { $key = $this->stream->key(); $this->stream->next(); @@ -65,21 +65,21 @@ public function hello() { }); - it("gets the next token value", function() { + it("gets the next token value", function () { $actual = $this->stream->next(); expect($actual)->toBe("class"); }); - it("gets the next token", function() { + it("gets the next token", function () { $actual = $this->stream->next(true); expect($actual)->toBe([T_CLASS, "class", 2]); }); - it("iterates through all tokens", function() { + it("iterates through all tokens", function () { $i = 0; foreach ($this->stream as $value) { @@ -91,12 +91,12 @@ public function hello() { }); - it("returns the skipped content until the next correponding token", function() { + it("returns the skipped content until the next correponding token", function () { $actual = $this->stream->next(T_CLASS); expect($actual)->toBe("class"); }); - it("cancels the lookup if the token is not found", function() { + it("cancels the lookup if the token is not found", function () { $actual = $this->stream->next(999); expect($actual)->toBe(null); @@ -106,16 +106,16 @@ public function hello() { }); - describe("->nextSequence()", function() { + describe("->nextSequence()", function () { - it("moves to the next sequence", function() { + it("moves to the next sequence", function () { $actual = $this->stream->nextSequence('()'); expect($actual)->toBe("class HelloWorld\n{\n public function hello()"); }); - it("cancels the lookup if the token is not found", function() { + it("cancels the lookup if the token is not found", function () { $actual = $this->stream->nextSequence('()()'); expect($actual)->toBe(null); @@ -125,16 +125,16 @@ public function hello() { }); - describe("->nextMatchingBracket()", function() { + describe("->nextMatchingBracket()", function () { - it("extracts the body between two correponding bracket", function() { + it("extracts the body between two correponding bracket", function () { $stream = new TokenStream(['source' => '']); $stream->next('{'); $actual = $stream->nextMatchingBracket(); expect($actual)->toBe("{ rand(2,5); }"); }); - it("supports nested brackets", function() { + it("supports nested brackets", function () { $stream = new TokenStream(['source' => '']); $stream->next('{'); @@ -143,7 +143,7 @@ public function hello() { }); - it("bails out nicely if there's no further tags", function() { + it("bails out nicely if there's no further tags", function () { $stream = new TokenStream(['source' => '']); $actual = $stream->nextMatchingBracket(); @@ -151,7 +151,7 @@ public function hello() { }); - it("bails out nicely if the current tags is not an open tags", function() { + it("bails out nicely if the current tags is not an open tags", function () { $stream = new TokenStream(['source' => '']); $actual = $stream->nextMatchingBracket(); @@ -159,7 +159,7 @@ public function hello() { }); - it("cancels the lookup if there's no closing tags", function() { + it("cancels the lookup if there's no closing tags", function () { $stream = new TokenStream(['source' => '']); $stream->next('{'); @@ -171,9 +171,9 @@ public function hello() { }); - describe("->prev()", function() { + describe("->prev()", function () { - it("moves prev", function() { + it("moves prev", function () { $key = $this->stream->key(); $this->stream->next(); @@ -182,7 +182,7 @@ public function hello() { }); - it("gets the previous token value", function() { + it("gets the previous token value", function () { $this->stream->seek(1); $actual = $this->stream->prev(); @@ -190,7 +190,7 @@ public function hello() { }); - it("gets the previous token", function() { + it("gets the previous token", function () { $this->stream->seek(1); $actual = $this->stream->prev(true); @@ -200,9 +200,9 @@ public function hello() { }); - describe("->key()", function() { + describe("->key()", function () { - it("returns the current key", function() { + it("returns the current key", function () { expect($this->stream->key())->toBe(0); $this->stream->next(); @@ -212,9 +212,9 @@ public function hello() { }); - describe("->seek()", function() { + describe("->seek()", function () { - it("correctly seeks inside the stream", function() { + it("correctly seeks inside the stream", function () { $this->stream->seek($this->len - 1); expect('?>')->toBe($this->stream->current()); @@ -223,9 +223,9 @@ public function hello() { }); - describe("->rewind()", function() { + describe("->rewind()", function () { - it("resets the stream to the start", function() { + it("resets the stream to the start", function () { $key = $this->stream->key(); $this->stream->next(); @@ -236,15 +236,15 @@ public function hello() { }); - describe("->valid()", function() { + describe("->valid()", function () { - it("returns true if the the stream is iteratable", function() { + it("returns true if the the stream is iteratable", function () { expect($this->stream->valid())->toBe(true); }); - it("returns false if the the stream is no more iteratable", function() { + it("returns false if the the stream is no more iteratable", function () { $this->stream->seek($this->len - 1); expect($this->stream->valid())->toBe(true); @@ -255,9 +255,9 @@ public function hello() { }); - describe("->count()", function() { + describe("->count()", function () { - it("returns the correct number of tokens", function() { + it("returns the correct number of tokens", function () { expect($this->stream->count())->toBe($this->len); @@ -265,9 +265,9 @@ public function hello() { }); - describe("->offsetGet()", function() { + describe("->offsetGet()", function () { - it("accesses token by key", function() { + it("accesses token by key", function () { $key = $this->stream->key(); $value = $this->stream[$key][1]; @@ -277,16 +277,16 @@ public function hello() { }); - describe("->offsetExist()", function() { + describe("->offsetExist()", function () { - it("returns true for an existing offset", function() { + it("returns true for an existing offset", function () { expect(isset($this->stream[0]))->toBe(true); expect(isset($this->stream[$this->len - 1]))->toBe(true); }); - it("returns false for an unexisting offset", function() { + it("returns false for an unexisting offset", function () { expect(isset($this->stream[$this->len]))->toBe(false); @@ -294,9 +294,9 @@ public function hello() { }); - describe("->offsetSet()", function() { + describe("->offsetSet()", function () { - it("throws an exception", function() { + it("throws an exception", function () { expect(isset($this->stream[0]))->toBe(true); expect(isset($this->stream[$this->len - 1]))->toBe(true); @@ -305,11 +305,11 @@ public function hello() { }); - describe("->offsetUnset()", function() { + describe("->offsetUnset()", function () { - it("throws an exception", function() { + it("throws an exception", function () { - expect(function() { + expect(function () { unset($this->stream[0]); })->toThrow(new Exception); @@ -317,11 +317,11 @@ public function hello() { }); - describe("->offsetSet()", function() { + describe("->offsetSet()", function () { - it("throws an exception", function() { + it("throws an exception", function () { - expect(function() { + expect(function () { $this->stream[0] = []; })->toThrow(new Exception()); @@ -329,15 +329,15 @@ public function hello() { }); - describe("->getType()", function() { + describe("->getType()", function () { - it("returns the correct token type", function() { + it("returns the correct token type", function () { expect($this->stream->getType(0))->toBe(T_OPEN_TAG); }); - it("returns the current token type", function() { + it("returns the current token type", function () { expect($this->stream->getType())->toBe(T_OPEN_TAG); @@ -345,15 +345,15 @@ public function hello() { }); - describe("->getValue()", function() { + describe("->getValue()", function () { - it("returns the correct token value", function() { + it("returns the correct token value", function () { expect($this->stream->getValue(0))->toBe("stream->getValue())->toBe("getName()", function() { + describe("->getName()", function () { - it("returns the correct token name", function() { + it("returns the correct token name", function () { expect($this->stream->getName(0))->toBe("T_OPEN_TAG"); @@ -371,15 +371,15 @@ public function hello() { }); - describe("->is()", function() { + describe("->is()", function () { - it("returns true when type is correct", function() { + it("returns true when type is correct", function () { expect($this->stream->is(T_OPEN_TAG, 0))->toBe(true); }); - it("returns false when type is incorrect", function() { + it("returns false when type is incorrect", function () { expect($this->stream->is(T_OPEN_TAG, 1))->toBe(false); @@ -387,9 +387,9 @@ public function hello() { }); - describe("->__toString()", function() { + describe("->__toString()", function () { - it("generates a string representation of the stream", function() { + it("generates a string representation of the stream", function () { $actual = (string) $this->stream; expect($actual)->toBe($this->code); diff --git a/spec/Suite/Legacy/Specification.spec.php b/spec/Suite/Legacy/Specification.spec.php new file mode 100644 index 00000000..d55f7702 --- /dev/null +++ b/spec/Suite/Legacy/Specification.spec.php @@ -0,0 +1,365 @@ +spec = new Specification(['closure' => function () {}]); + + }); + + describe("->passed()", function () { + + it("returns the closure return value", function () { + + $this->spec = new Specification([ + 'closure' => function () { + return 'hello world'; + } + ]); + + $return = null; + $this->spec->passed($return); + expect($return)->toBe('hello world'); + + }); + + it("fails when an expectation is not verified", function () { + + $this->spec = new Specification([ + 'closure' => function () { + $this->expect(true)->toBe(true); + $this->expect(true); + } + ]); + + expect($this->spec->passed())->toBe(false); + + }); + + context("when the specs passed", function () { + + it("logs a pass", function () { + + $this->spec = new Specification([ + 'message' => 'runs a spec', + 'closure' => function () { + $this->expect(true)->toBe(true); + } + ]); + + expect($this->spec->passed())->toBe(true); + + $passed = $this->spec->summary()->logs('passed'); + expect($passed)->toHaveLength(1); + + $pass = reset($passed); + $expectation = $pass->children()[0]; + + expect($expectation->matcher())->toBe('Kahlan\Matcher\ToBe'); + expect($expectation->matcherName())->toBe('toBe'); + expect($expectation->not())->toBe(false); + expect($expectation->type())->toBe('passed'); + expect($expectation->data())->toBe([ + 'actual' => true, + 'expected' => true + ]); + expect($expectation->messages())->toBe(['it runs a spec']); + + }); + + it("logs a pass with a deferred matcher", function () { + + $this->spec = new Specification([ + 'message' => 'runs a spec', + 'closure' => function () { + $stub = Double::instance(); + $this->expect($stub)->toReceive('methodName'); + $stub->methodName(); + } + ]); + + expect($this->spec->passed())->toBe(true); + + $passes = $this->spec->summary()->logs('passed'); + expect($passes)->toHaveLength(1); + + $pass = reset($passes); + $expectation = $pass->children()[0]; + + expect($expectation->matcher())->toBe('Kahlan\Matcher\ToReceive'); + expect($expectation->matcherName())->toBe('toReceive'); + expect($expectation->not())->toBe(false); + expect($expectation->type())->toBe('passed'); + expect($expectation->data())->toBe([ + 'actual received' => 'methodName', + "actual received times" => 1, + 'expected to receive' => 'methodName' + ]); + expect($expectation->description())->toBe('receive the expected method.'); + expect($expectation->messages())->toBe(['it runs a spec']); + + }); + + it("logs the not attribute", function () { + + $this->spec = new Specification([ + 'closure' => function () { + $this->expect(true)->not->toBe(false); + } + ]); + + expect($this->spec->passed())->toBe(true); + + $passes = $this->spec->summary()->logs('passed'); + expect($passes)->toHaveLength(1); + + $pass = reset($passes); + $expectation = $pass->children()[0]; + + expect($expectation->not())->toBe(true); + + }); + + it("logs deferred matcher backtrace", function () { + + $this->spec = new Specification([ + 'closure' => function () { + $this->expect(Double::instance())->not->toReceive('helloWorld'); + } + ]); + + expect($this->spec->passed())->toBe(true); + + $passes = $this->spec->summary()->logs('passed'); + expect($passes)->toHaveLength(1); + + $pass = reset($passes); + $expectation = $pass->children()[0]; + + $backtrace = $expectation->backtrace(); + expect($backtrace[0]['file'])->toMatch('~ToReceive.php$~'); + + }); + + it("logs the not attribute with a deferred matcher", function () { + + $this->spec = new Specification([ + 'closure' => function () { + $stub = Double::instance(); + $this->expect($stub)->not->toReceive('methodName'); + } + ]); + + expect($this->spec->passed())->toBe(true); + + $passes = $this->spec->summary()->logs('passed'); + expect($passes)->toHaveLength(1); + + $pass = reset($passes); + $expectation = $pass->children()[0]; + + expect($expectation->not())->toBe(true); + + }); + + it("resets `not` to `false ` after any matcher call", function () { + + expect([]) + ->not->toBeNull() + ->toBeA('array') + ->toBeEmpty(); + + }); + + }); + + context("when the specs failed", function () { + + it("logs a fail", function () { + + $this->spec = new Specification([ + 'message' => 'runs a spec', + 'closure' => function () { + $this->expect(true)->toBe(false); + } + ]); + + expect($this->spec->passed())->toBe(false); + + $failed = $this->spec->summary()->logs('failed'); + expect($failed)->toHaveLength(1); + $failure = reset($failed); + + $expectation = $failure->children()[0]; + expect($expectation->matcher())->toBe('Kahlan\Matcher\ToBe'); + expect($expectation->matcherName())->toBe('toBe'); + expect($expectation->not())->toBe(false); + expect($expectation->type())->toBe('failed'); + expect($expectation->data())->toBe([ + 'actual' => true, + 'expected' => false + ]); + expect($expectation->messages())->toBe(['it runs a spec']); + expect($expectation->backtrace())->toBeAn('array'); + }); + + it("logs a fail with a deferred matcher", function () { + + $this->spec = new Specification([ + 'message' => 'runs a spec', + 'closure' => function () { + $stub = Double::instance(); + $this->expect($stub)->toReceive('methodName'); + } + ]); + + expect($this->spec->passed())->toBe(false); + + $failed = $this->spec->summary()->logs('failed'); + expect($failed)->toHaveLength(1); + + $failure = reset($failed); + + $expectation = $failure->children()[0]; + expect($expectation->matcher())->toBe('Kahlan\Matcher\ToReceive'); + expect($expectation->matcherName())->toBe('toReceive'); + expect($expectation->not())->toBe(false); + expect($expectation->type())->toBe('failed'); + expect($expectation->data())->toBe([ + 'actual received calls' => [], + 'expected to receive' => 'methodName' + ]); + expect($expectation->description())->toBe('receive the expected method.'); + expect($expectation->messages())->toBe(['it runs a spec']); + expect($expectation->backtrace())->toBeAn('array'); + + }); + + it("logs the not attribute", function () { + + $this->spec = new Specification([ + 'closure' => function () { + $this->expect(true)->not->toBe(true); + } + ]); + + expect($this->spec->passed())->toBe(false); + + $failures = $this->spec->summary()->logs('failed'); + expect($failures)->toHaveLength(1); + + $failure = reset($failures); + $expectation = $failure->children()[0]; + + expect($expectation->not())->toBe(true); + + }); + + it("logs the not attribute with a deferred matcher", function () { + + $this->spec = new Specification([ + 'closure' => function () { + $stub = Double::instance(); + $this->expect($stub)->not->toReceive('methodName'); + $stub->methodName(); + } + ]); + + expect($this->spec->passed())->toBe(false); + + $failures = $this->spec->summary()->logs('failed'); + expect($failures)->toHaveLength(1); + + $failure = reset($failures); + $expectation = $failure->children()[0]; + + expect($expectation->not())->toBe(true); + expect($expectation->not())->toBe(true); + + }); + + it("logs sub spec fails", function () { + + $this->spec = new Specification([ + 'message' => 'runs a spec', + 'closure' => function () { + $this->waitsFor(function () { + $this->expect(true)->toBe(false); + }); + } + ]); + + expect($this->spec->passed())->toBe(false); + + $failured = $this->spec->summary()->logs('failed'); + expect($failured)->toHaveLength(1); + + $failure = reset($failured); + $expectation = $failure->children()[0]; + + expect($expectation->matcher())->toBe('Kahlan\Matcher\ToBe'); + expect($expectation->matcherName())->toBe('toBe'); + expect($expectation->not())->toBe(false); + expect($expectation->type())->toBe('failed'); + expect($expectation->data())->toBe([ + 'actual' => true, + 'expected' => false + ]); + expect($expectation->messages())->toBe(['it runs a spec']); + expect($expectation->backtrace())->toBeAn('array'); + }); + + it("logs the first failing spec only", function () { + + $this->spec = new Specification([ + 'message' => 'runs a spec', + 'closure' => function () { + $this->waitsFor(function () { + $this->expect(true)->toBe(false); + return true; + })->toBe(false); + } + ]); + + expect($this->spec->passed())->toBe(false); + + $failured = $this->spec->summary()->logs('failed'); + expect($failured)->toHaveLength(1); + + $failure = reset($failured); + $expectation = $failure->children()[0]; + + expect($expectation->matcher())->toBe('Kahlan\Matcher\ToBe'); + expect($expectation->matcherName())->toBe('toBe'); + expect($expectation->not())->toBe(false); + expect($expectation->type())->toBe('failed'); + expect($expectation->data())->toBe([ + 'actual' => true, + 'expected' => false + ]); + expect($expectation->messages())->toBe(['it runs a spec']); + expect($expectation->backtrace())->toBeAn('array'); + }); + + }); + + }); + +}); diff --git a/spec/Suite/Legacy/Suite.spec.php b/spec/Suite/Legacy/Suite.spec.php new file mode 100644 index 00000000..29709a1d --- /dev/null +++ b/spec/Suite/Legacy/Suite.spec.php @@ -0,0 +1,267 @@ +suite = new Suite(['matcher' => new Matcher()]); + }); + + describe("->run()", function () { + + it("run the suite", function () { + + $describe = $this->suite->describe("", function () { + + $this->it("runs a spec", function () { + $this->expect(true)->toBe(true); + }); + + }); + + $this->suite->run(); + expect($this->suite->status())->toBe(0); + expect($this->suite->passed())->toBe(true); + + }); + + it("calls `afterX` callbacks if an exception occurs during callbacks", function () { + + $describe = $this->suite->describe("", function () { + + $this->inAfterEach = 0; + + $this->beforeEach(function () { + throw new Exception('Breaking the flow should execute afterEach anyway.'); + }); + + $this->it("does nothing", function () { + }); + + $this->afterEach(function () { + $this->inAfterEach++; + }); + + }); + + $this->suite->run(); + + expect($describe->inAfterEach)->toBe(1); + + $results = $this->suite->summary()->logs('errored'); + expect($results)->toHaveLength(1); + + $report = reset($results); + $actual = $report->exception()->getMessage(); + expect($actual)->toBe('Breaking the flow should execute afterEach anyway.'); + + expect($this->suite->status())->toBe(-1); + expect($this->suite->passed())->toBe(false); + + }); + + it("logs `MissingImplementationException` when thrown", function () { + + $missing = new MissingImplementationException(); + + $describe = $this->suite->describe("", function () use ($missing) { + + $this->it("throws an `MissingImplementationException`", function () use ($missing) { + throw $missing; + }); + + }); + + $this->suite->run(); + + $results = $this->suite->summary()->logs('errored'); + expect($results)->toHaveLength(1); + + $report = reset($results); + expect($report->exception())->toBe($missing); + expect($report->type())->toBe('errored'); + expect($report->messages())->toBe(['', '', 'it throws an `MissingImplementationException`']); + + expect($this->suite->status())->toBe(-1); + expect($this->suite->passed())->toBe(false); + + }); + + it("throws and exception if attempts to call the `run()` function inside a scope", function () { + + $closure = function () { + $describe = $this->suite->describe("", function () { + $this->run(); + }); + $this->suite->run(); + }; + + expect($closure)->toThrow(new Exception('Method not allowed in this context.')); + + expect($this->suite->status())->toBe(-1); + expect($this->suite->passed())->toBe(false); + + }); + + it("fails fast", function () { + + $describe = $this->suite->describe("", function () { + + $this->it("fails1", function () { + $this->expect(true)->toBe(false); + }); + + $this->it("fails2", function () { + $this->expect(true)->toBe(false); + }); + + $this->it("fails3", function () { + $this->expect(true)->toBe(false); + }); + + }); + + $this->suite->run(['ff' => 1]); + + $failed = $this->suite->summary()->logs('failed'); + + expect($failed)->toHaveLength(1); + expect($this->suite->focused())->toBe(false); + expect($this->suite->status())->toBe(-1); + expect($this->suite->passed())->toBe(false); + + }); + + it("fails after two failures", function () { + + $describe = $this->suite->describe("", function () { + + $this->it("fails1", function () { + $this->expect(true)->toBe(false); + }); + + $this->it("fails2", function () { + $this->expect(true)->toBe(false); + }); + + $this->it("fails3", function () { + $this->expect(true)->toBe(false); + }); + + }); + + $this->suite->run(['ff' => 2]); + + $failed = $this->suite->summary()->logs('failed'); + + expect($failed)->toHaveLength(2); + expect($this->suite->focused())->toBe(false); + expect($this->suite->status())->toBe(-1); + expect($this->suite->passed())->toBe(false); + + }); + + }); + + describe("skipIf", function () { + + it("skips specs in a before", function () { + + $describe = $this->suite->describe("skip suite", function () { + + $this->exectuted = ['it' => 0]; + + beforeAll(function () { + skipIf(true); + }); + + $this->it("an it", function () { + $this->exectuted['it']++; + }); + + $this->it("an it", function () { + $this->exectuted['it']++; + }); + + }); + $reporters = Double::instance(); + + expect($reporters)->toReceive('dispatch')->with('start', ['total' => 2])->ordered; + expect($reporters)->toReceive('dispatch')->with('suiteStart', $describe)->ordered; + expect($reporters)->toReceive('dispatch')->with('specStart', Arg::toBeAnInstanceOf('Kahlan\Specification'))->ordered; + expect($reporters)->toReceive('dispatch')->with('specEnd', Arg::toBeAnInstanceOf('Kahlan\Log'))->ordered; + expect($reporters)->toReceive('dispatch')->with('specStart', Arg::toBeAnInstanceOf('Kahlan\Specification'))->ordered; + expect($reporters)->toReceive('dispatch')->with('specEnd', Arg::toBeAnInstanceOf('Kahlan\Log'))->ordered; + expect($reporters)->toReceive('dispatch')->with('suiteEnd', $describe)->ordered; + expect($reporters)->toReceive('dispatch')->with('end', Arg::toBeAnInstanceOf('Kahlan\Summary'))->ordered; + + $this->suite->run(['reporters' => $reporters]); + + expect($describe->exectuted)->toEqual(['it' => 0]); + expect($this->suite->focused())->toBe(false); + expect($this->suite->status())->toBe(0); + expect($this->suite->passed())->toBe(true); + + }); + + it("skips specs in a beforeEach", function () { + + $describe = $this->suite->describe("skip suite", function () { + + $this->exectuted = ['it' => 0]; + + beforeEach(function () { + skipIf(true); + }); + + $this->it("an it", function () { + $this->exectuted['it']++; + }); + + $this->it("an it", function () { + $this->exectuted['it']++; + }); + + }); + + $reporters = Double::instance(); + + expect($reporters)->toReceive('dispatch')->with('start', ['total' => 2])->ordered; + expect($reporters)->toReceive('dispatch')->with('suiteStart', $describe)->ordered; + expect($reporters)->toReceive('dispatch')->with('specStart', Arg::toBeAnInstanceOf('Kahlan\Specification'))->ordered; + expect($reporters)->toReceive('dispatch')->with('specEnd', Arg::toBeAnInstanceOf('Kahlan\Log'))->ordered; + expect($reporters)->toReceive('dispatch')->with('specStart', Arg::toBeAnInstanceOf('Kahlan\Specification'))->ordered; + expect($reporters)->toReceive('dispatch')->with('suiteEnd', $describe)->ordered; + expect($reporters)->toReceive('dispatch')->with('end', Arg::toBeAnInstanceOf('Kahlan\Summary'))->ordered; + + $this->suite->run(['reporters' => $reporters]); + + expect($describe->exectuted)->toEqual(['it' => 0]); + expect($this->suite->focused())->toBe(false); + expect($this->suite->status())->toBe(0); + expect($this->suite->passed())->toBe(true); + + }); + + }); + +}); diff --git a/spec/Suite/Log.spec.php b/spec/Suite/Log.spec.php new file mode 100644 index 00000000..a340788d --- /dev/null +++ b/spec/Suite/Log.spec.php @@ -0,0 +1,125 @@ +__construct()", function () { + + it("correctly sets default values", function () { + + $log = new Log(); + expect($log->scope())->toBe(null); + expect($log->type())->toBe('passed'); + expect($log->not())->toBe(false); + expect($log->description())->toBe(null); + expect($log->matcher())->toBe(null); + expect($log->matcherName())->toBe(null); + expect($log->data())->toBe([]); + expect($log->backtrace())->toBe([]); + expect($log->exception())->toBe(null); + expect($log->file())->toBe(null); + expect($log->line())->toBe(null); + expect($log->children())->toBe([]); + + }); + + }); + + describe("->add()", function () { + + beforeEach(function () { + $this->scope = new Scope(); + $this->pattern = '*Suite.php'; + $this->regExp = strtr(preg_quote($this->pattern, '~'), ['\*' => '.*', '\?' => '.']); + $this->scope->backtraceFocus($this->pattern); + $this->reports = new Log([ + "scope" => $this->scope + ]); + }); + + it("rebases backtrace on fail report", function () { + + $this->reports->add('fail', [ + 'backtrace' => debug_backtrace() + ]); + + $logs = $this->reports->children(); + $log = $logs[0]; + expect($log->backtrace()[0]['file'])->toMatch("~^{$this->regExp}$~"); + + }); + + it("doesn't rebase backtrace on an exception report", function () { + + $this->reports->exception(new Exception()); + expect($this->reports->backtrace()[0]['file'])->not->toMatch("~^{$this->regExp}$~"); + + }); + + }); + + describe("->passed()", function () { + + it("returns `true` type is `'passed'`", function () { + + $log = new Log(['type' => 'passed']); + expect($log->passed())->toBe(true); + + }); + + it("returns `true` type is `'skipped'`", function () { + + $log = new Log(['type' => 'skipped']); + expect($log->passed())->toBe(true); + + }); + + it("returns `true` type is `'excluded'`", function () { + + $log = new Log(['type' => 'excluded']); + expect($log->passed())->toBe(true); + + }); + + it("returns `false` type is `'failed'`", function () { + + $log = new Log(['type' => 'failed']); + expect($log->passed())->toBe(false); + + }); + + it("returns `false` type is `'errored'`", function () { + + $log = new Log(['type' => 'errored']); + expect($log->passed())->toBe(false); + + }); + + it("returns `true` when logged exceptions passed", function () { + + $log = new Log(); + $log->add('passed', []); + $log->add('passed', []); + + expect($log->passed())->toBe(true); + + }); + + it("returns `false` when some logged exceptions failed", function () { + + $log = new Log(); + $log->add('passed', []); + $log->add('passed', []); + $log->add('failed', []); + + expect($log->passed())->toBe(false); + + }); + + }); + +}); diff --git a/spec/Suite/MatcherSpec.php b/spec/Suite/Matcher.spec.php similarity index 71% rename from spec/Suite/MatcherSpec.php rename to spec/Suite/Matcher.spec.php index 5c423fbf..596efdda 100644 --- a/spec/Suite/MatcherSpec.php +++ b/spec/Suite/Matcher.spec.php @@ -7,15 +7,15 @@ use SplMaxHeap; use Kahlan\Specification; use Kahlan\Matcher; -use Kahlan\Plugin\Stub; +use Kahlan\Plugin\Double; -describe("Matcher", function() { +describe("Matcher", function () { - beforeEach(function() { + beforeEach(function () { $this->matchers = Matcher::get(); }); - afterEach(function() { + afterEach(function () { Matcher::reset(); foreach ($this->matchers as $name => $value) { foreach ($value as $for => $class) { @@ -24,11 +24,11 @@ } }); - describe("::register()", function() { + describe("::register()", function () { - it("registers a matcher", function() { + it("registers a matcher", function () { - Matcher::register('toBeOrNotToBe', Stub::classname(['extends' => 'Kahlan\Matcher\ToBe'])); + Matcher::register('toBeOrNotToBe', Double::classname(['extends' => 'Kahlan\Matcher\ToBe'])); expect(Matcher::exists('toBeOrNotToBe'))->toBe(true); expect(Matcher::exists('toBeOrNot'))->toBe(false); @@ -36,9 +36,9 @@ }); - it("registers a matcher for a specific class", function() { + it("registers a matcher for a specific class", function () { - Matcher::register('toEqualCustom', Stub::classname(['extends' => 'Kahlan\Matcher\ToEqual']), 'stdClass'); + Matcher::register('toEqualCustom', Double::classname(['extends' => 'Kahlan\Matcher\ToEqual']), 'stdClass'); expect(Matcher::exists('toEqualCustom', 'stdClass'))->toBe(true); expect(Matcher::exists('toEqualCustom'))->toBe(false); @@ -47,9 +47,9 @@ }); - it("makes registered matchers for a specific class available for sub classes", function() { + it("makes registered matchers for a specific class available for sub classes", function () { - Matcher::register('toEqualCustom', Stub::classname(['extends' => 'Kahlan\Matcher\ToEqual']), 'SplHeap'); + Matcher::register('toEqualCustom', Double::classname(['extends' => 'Kahlan\Matcher\ToEqual']), 'SplHeap'); expect(Matcher::exists('toEqualCustom', 'SplHeap'))->toBe(true); expect(Matcher::exists('toEqualCustom'))->toBe(false); @@ -59,9 +59,9 @@ }); - describe("::get()", function() { + describe("::get()", function () { - it("returns all registered matchers", function() { + it("returns all registered matchers", function () { Matcher::reset(); Matcher::register('toBe', 'Kahlan\Matcher\ToBe'); @@ -72,13 +72,13 @@ }); - it("returns a registered matcher", function() { + it("returns a registered matcher", function () { expect(Matcher::get('toBe'))->toBe('Kahlan\Matcher\ToBe'); }); - it("returns all registered matchers for a specific matcher", function() { + it("returns all registered matchers for a specific matcher", function () { Matcher::register('toBe', 'Kahlan\Matcher\ToEqual', 'stdClass'); @@ -89,13 +89,13 @@ }); - it("returns the default registered matcher", function() { + it("returns the default registered matcher", function () { expect(Matcher::get('toBe', 'stdClass'))->toBe('Kahlan\Matcher\ToBe'); }); - it("returns a custom matcher when defined for a specific class", function() { + it("returns a custom matcher when defined for a specific class", function () { Matcher::register('toBe', 'Kahlan\Matcher\ToEqual', 'stdClass'); @@ -104,9 +104,9 @@ }); - it("throws an exception when using an undefined matcher name", function() { + it("throws an exception when using an undefined matcher name", function () { - $closure = function() { + $closure = function () { Matcher::get('toHelloWorld'); }; @@ -114,9 +114,9 @@ }); - it("throws an exception when using an undefined matcher name for a specific class", function() { + it("throws an exception when using an undefined matcher name for a specific class", function () { - $closure = function() { + $closure = function () { Matcher::get('toHelloWorld', 'stdClass'); }; @@ -126,11 +126,11 @@ }); - describe("::unregister()", function() { + describe("::unregister()", function () { - it("unregisters a matcher", function() { + it("unregisters a matcher", function () { - Matcher::register('toBeOrNotToBe', Stub::classname(['extends' => 'Kahlan\Matcher\ToBe'])); + Matcher::register('toBeOrNotToBe', Double::classname(['extends' => 'Kahlan\Matcher\ToBe'])); expect(Matcher::exists('toBeOrNotToBe'))->toBe(true); Matcher::unregister('toBeOrNotToBe'); @@ -138,7 +138,7 @@ }); - it("unregisters all matchers", function() { + it("unregisters all matchers", function () { expect(Matcher::get())->toBeGreaterThan(1); Matcher::unregister(true); @@ -149,17 +149,17 @@ }); - describe("::reset()", function() { + describe("::reset()", function () { - it("unregisters all matchers", function() { + it("unregisters all matchers", function () { expect(Matcher::get())->toBeGreaterThan(1); Matcher::reset(); Matcher::register('toHaveLength', 'Kahlan\Matcher\ToHaveLength'); expect(Matcher::get())->toHaveLength(1); - }); + }); }); -}); \ No newline at end of file +}); diff --git a/spec/Suite/Matcher/ToBeSpec.php b/spec/Suite/Matcher/ToBe.spec.php similarity index 58% rename from spec/Suite/Matcher/ToBeSpec.php rename to spec/Suite/Matcher/ToBe.spec.php index 378f4053..517137eb 100644 --- a/spec/Suite/Matcher/ToBeSpec.php +++ b/spec/Suite/Matcher/ToBe.spec.php @@ -3,107 +3,107 @@ use Kahlan\Matcher\ToBe; -describe("toBe", function() { +describe("toBe", function () { - describe("::match()", function() { + describe("::match()", function () { - it("passes if true === true", function() { + it("passes if true === true", function () { expect(true)->toBe(true); }); - it("passes if false === false", function() { + it("passes if false === false", function () { expect(false)->toBe(false); }); - it("passes if 1 === 1", function() { + it("passes if 1 === 1", function () { expect(1)->toBe(1); }); - it("passes if 'Hello World' === 'Hello World'", function() { + it("passes if 'Hello World' === 'Hello World'", function () { expect('Hello World')->toBe('Hello World'); }); - it("passes if [1, 3, 7] === [1, 3, 7]", function() { + it("passes if [1, 3, 7] === [1, 3, 7]", function () { expect([1, 3, 7])->toBe([1, 3, 7]); }); - it("passes if true is not === false", function() { + it("passes if true is not === false", function () { expect(true)->not->toBe(false); }); - it("passes if false is not === true", function() { + it("passes if false is not === true", function () { expect(false)->not->toBe(true); }); - it("passes if 2 is not === 1", function() { + it("passes if 2 is not === 1", function () { expect(2)->not->toBe(1); }); - it("passes if 1 is not === true", function() { + it("passes if 1 is not === true", function () { expect(1)->not->toBe(true); }); - it("passes if 0 is not === false", function() { + it("passes if 0 is not === false", function () { expect(0)->not->toBe(false); }); - it("passes if [] is not === true", function() { + it("passes if [] is not === true", function () { expect([])->not->toBe(true); }); - it("passes if [] is not === false", function() { + it("passes if [] is not === false", function () { expect([])->not->toBe(false); }); - it("passes if 'Hello World' is not === true", function() { + it("passes if 'Hello World' is not === true", function () { expect('Hello World')->not->toBe(true); }); - it("passes if 'Hello World' is not === false", function() { + it("passes if 'Hello World' is not === false", function () { expect('Hello World')->not->toBe(false); }); - it("passes if 'Hello World' is not === 'World Hello'", function() { + it("passes if 'Hello World' is not === 'World Hello'", function () { expect('Hello World')->not->toBe('World Hello'); }); - it("passes if [1, 3, 7] is not === [1, 7, 3]", function() { + it("passes if [1, 3, 7] is not === [1, 7, 3]", function () { expect([1, 3, 7])->not->toBe([1, 7, 3]); }); - it("passes if ['a' => 1, 'b' => 3, 'c' => 7] is not === ['a' => 1, 'c' => 7, 'b' => 3]", function() { + it("passes if ['a' => 1, 'b' => 3, 'c' => 7] is not === ['a' => 1, 'c' => 7, 'b' => 3]", function () { expect(['a' => 1, 'b' => 3, 'c' => 7])->not->toBe(['a' => 1, 'c' => 7, 'b' => 3]); @@ -111,9 +111,9 @@ }); - describe("::description()", function() { + describe("::description()", function () { - it("returns the description message", function() { + it("returns the description message", function () { $actual = ToBe::description(); diff --git a/spec/Suite/Matcher/ToBeASpec.php b/spec/Suite/Matcher/ToBeA.spec.php similarity index 53% rename from spec/Suite/Matcher/ToBeASpec.php rename to spec/Suite/Matcher/ToBeA.spec.php index d0eb90e1..d474e9fe 100644 --- a/spec/Suite/Matcher/ToBeASpec.php +++ b/spec/Suite/Matcher/ToBeA.spec.php @@ -4,77 +4,77 @@ use stdClass; use Kahlan\Matcher\ToBeA; -describe("toBeA", function() { +describe("toBeA", function () { - describe("::match()", function() { + describe("::match()", function () { - it("passes if true is a boolean", function() { + it("passes if true is a boolean", function () { expect(true)->toBeA('boolean'); }); - it("passes if false is a boolean", function() { + it("passes if false is a boolean", function () { expect(false)->toBeA('boolean'); }); - it("passes if true is a bool", function() { + it("passes if true is a bool", function () { expect(true)->toBeA('bool'); }); - it("passes if false is a bool", function() { + it("passes if false is a bool", function () { expect(false)->toBeA('bool'); }); - it("passes if 1 is an integer", function() { + it("passes if 1 is an integer", function () { expect(1)->toBeA('integer'); }); - it("passes if 1 is an int", function() { + it("passes if 1 is an int", function () { expect(1)->toBeA('int'); }); - it("passes if 'Hello World' is a string", function() { + it("passes if 'Hello World' is a string", function () { expect('Hello World')->toBeA('string'); }); - it("passes if [1, 3, 7] is an array", function() { + it("passes if [1, 3, 7] is an array", function () { expect([1, 3, 7])->toBeA('array'); }); - it("passes if 1.5 is a float", function() { + it("passes if 1.5 is a float", function () { expect(1.5)->toBeA('float'); }); - it("passes if an instance of stdClass is an object", function() { + it("passes if an instance of stdClass is an object", function () { expect(new stdClass())->toBeA('object'); }); - it("passes if null is NULL", function() { + it("passes if null is NULL", function () { expect(null)->toBeA('null'); }); - it("passes if a resource is a resource", function() { + it("passes if a resource is a resource", function () { expect(opendir(sys_get_temp_dir()))->toBeA('resource'); @@ -82,16 +82,16 @@ }); - describe("::description()", function() { + describe("::description()", function () { - it("returns the description message", function() { + it("returns the description message", function () { ToBeA::match(1, 'boolean'); $actual = ToBeA::description(); expect($actual['description'])->toBe('have the expected type.'); - expect((string) $actual['params']['actual'])->toBe('integer'); - expect((string) $actual['params']['expected'])->toBe('boolean'); + expect((string) $actual['data']['actual'])->toBe('integer'); + expect((string) $actual['data']['expected'])->toBe('boolean'); }); diff --git a/spec/Suite/Matcher/ToBeAnInstanceOfSpec.php b/spec/Suite/Matcher/ToBeAnInstanceOf.spec.php similarity index 71% rename from spec/Suite/Matcher/ToBeAnInstanceOfSpec.php rename to spec/Suite/Matcher/ToBeAnInstanceOf.spec.php index f59b4aef..67ccaf1a 100644 --- a/spec/Suite/Matcher/ToBeAnInstanceOfSpec.php +++ b/spec/Suite/Matcher/ToBeAnInstanceOf.spec.php @@ -4,17 +4,17 @@ use stdClass; use Kahlan\Matcher\ToBeAnInstanceOf; -describe("toBeAnInstanceOf", function() { +describe("toBeAnInstanceOf", function () { - describe("::match()", function() { + describe("::match()", function () { - it("passes if an instance of stdClass is an object", function() { + it("passes if an instance of stdClass is an object", function () { expect(new stdClass())->toBeAnInstanceOf('stdClass'); }); - it("passes if an instance of stdClass is not a Exception", function() { + it("passes if an instance of stdClass is not a Exception", function () { expect(new stdClass())->not->toBeAnInstanceOf('Exception'); @@ -23,9 +23,9 @@ }); - describe("::description()", function() { + describe("::description()", function () { - it("returns the description message", function() { + it("returns the description message", function () { $actual = ToBeAnInstanceOf::description(); diff --git a/spec/Suite/Matcher/ToBeCalled.spec.php b/spec/Suite/Matcher/ToBeCalled.spec.php new file mode 100644 index 00000000..7d095ae9 --- /dev/null +++ b/spec/Suite/Matcher/ToBeCalled.spec.php @@ -0,0 +1,255 @@ +previous = Interceptor::instance(); + Interceptor::unpatch(); + + $cachePath = rtrim(sys_get_temp_dir(), DS) . DS . 'kahlan'; + $include = ['Kahlan\Spec\\']; + $interceptor = Interceptor::patch(compact('include', 'cachePath')); + $interceptor->patchers()->add('pointcut', new PointcutPatcher()); + $interceptor->patchers()->add('monkey', new MonkeyPatcher()); + }); + + /** + * Restore Interceptor class. + */ + afterAll(function () { + Interceptor::load($this->previous); + }); + + it("expects uncalled function to be uncalled", function () { + + $mon = new Mon(); + expect('time')->not->toBeCalled(); + + }); + + it("expects called function to be called", function () { + + $mon = new Mon(); + expect('time')->toBeCalled(); + $mon->time(); + + }); + + context("when using with()", function () { + + it("expects called function called with correct arguments to be called", function () { + + $mon = new Mon(); + expect('Kahlan\Spec\Fixture\Plugin\Monkey\rand')->toBeCalled()->with(5, 10); + $mon->rand(5, 10); + + }); + + it("expects called function called with correct arguments exactly a specified times to be called", function () { + + $mon = new Mon(); + expect('Kahlan\Spec\Fixture\Plugin\Monkey\rand')->toBeCalled()->with(5, 10)->times(2); + $mon->rand(5, 10); + $mon->rand(5, 10); + + }); + + it("expects called function called with correct arguments not exactly a specified times to be uncalled", function () { + + $mon = new Mon(); + expect('Kahlan\Spec\Fixture\Plugin\Monkey\rand')->not->toBeCalled()->with(5, 10)->times(2); + $mon->rand(5, 10); + $mon->rand(10, 10); + + }); + + }); + + context("when using times()", function () { + + it("expects called function to be called exactly once", function () { + + $mon = new Mon(); + expect('time')->toBeCalled()->once(); + $mon->time(); + + }); + + it("expects called function to be called exactly a specified times", function () { + + $mon = new Mon(); + expect('time')->toBeCalled()->times(3); + $mon->time(); + $mon->time(); + $mon->time(); + + }); + + it("expects called function not called exactly a specified times to be uncalled", function () { + + $mon = new Mon(); + expect('time')->not->toBeCalled()->times(1); + $mon->time(); + $mon->time(); + + }); + + }); + + context("with ordered enabled", function () { + + describe("::match()", function () { + + it("expects uncalled function to be uncalled in a defined order", function () { + + $mon = new Mon(); + expect('time')->toBeCalled()->ordered; + expect('Kahlan\Spec\Fixture\Plugin\Monkey\rand')->not->toBeCalled()->ordered; + $mon->time(); + + }); + + it("expects called function to be called in a defined order", function () { + + $mon = new Mon(); + expect('time')->toBeCalled()->ordered; + expect('Kahlan\Spec\Fixture\Plugin\Monkey\rand')->toBeCalled()->with(5, 10)->ordered; + expect('Kahlan\Spec\Fixture\Plugin\Monkey\rand')->toBeCalled()->with(10, 20)->ordered; + $mon->time(); + $mon->rand(5, 10); + $mon->rand(10, 20); + + }); + + it("expects called function called in a different order to be uncalled", function () { + + $mon = new Mon(); + expect('time')->toBeCalled()->ordered; + expect('Kahlan\Spec\Fixture\Plugin\Monkey\rand')->not->toBeCalled()->with(5, 10)->ordered; + $mon->rand(5, 10); + $mon->time(); + + }); + + }); + + }); + + }); + + describe("->description()", function () { + + it("returns the description message for not received call", function () { + + $mon = new Mon(); + $matcher = new ToBeCalled('time'); + + $matcher->resolve([ + 'instance' => $matcher, + 'data' => [ + 'actual' => 'time', + 'logs' => [] + ] + ]); + + $actual = $matcher->description(); + + expect($actual['description'])->toBe('be called.'); + expect($actual['data'])->toBe([ + 'actual' => 'time()', + 'actual called times' => 0, + 'expected to be called' => 'time()' + ]); + + }); + + it("returns the description message for not received call the specified number of times", function () { + + $mon = new Mon(); + $matcher = new ToBeCalled('time'); + $matcher->times(2); + + $matcher->resolve([ + 'instance' => $matcher, + 'data' => [ + 'actual' => 'time', + 'logs' => [] + ] + ]); + + $actual = $matcher->description(); + + expect($actual['description'])->toBe('be called the expected times.'); + expect($actual['data'])->toBe([ + 'actual' => 'time()', + 'actual called times' => 0, + 'expected to be called' => 'time()', + 'expected called times' => 2 + ]); + + }); + + it("returns the description message for wrong passed arguments", function () { + + $mon = new Mon(); + $matcher = new ToBeCalled('time'); + $matcher->with('Hello World!'); + + $mon->time(); + + $matcher->resolve([ + 'instance' => $matcher, + 'data' => [ + 'actual' => 'time', + 'logs' => [] + ] + ]); + + $actual = $matcher->description(); + + expect($actual['description'])->toBe('be called with expected parameters.'); + expect($actual['data'])->toBe([ + 'actual' => 'time()', + 'actual called times' => 1, + 'actual called parameters list' => [ + [] + ], + 'expected to be called' => 'time()', + 'expected parameters' => [ + 'Hello World!' + ] + ]); + + }); + + }); + + describe("->ordered()", function () { + + it("throw an exception when trying to play with core instance", function () { + + expect(function () { + $matcher = new ToBeCalled('a'); + $matcher->order; + })->toThrow(new Exception("Unsupported attribute `order` only `ordered` is available.")); + + }); + + }); + +}); diff --git a/spec/Suite/Matcher/ToBeCloseToSpec.php b/spec/Suite/Matcher/ToBeCloseTo.spec.php similarity index 67% rename from spec/Suite/Matcher/ToBeCloseToSpec.php rename to spec/Suite/Matcher/ToBeCloseTo.spec.php index 1df7b2ca..d5734c76 100644 --- a/spec/Suite/Matcher/ToBeCloseToSpec.php +++ b/spec/Suite/Matcher/ToBeCloseTo.spec.php @@ -3,48 +3,48 @@ use Kahlan\Matcher\ToBeCloseTo; -describe("toBeCloseTo", function() { +describe("toBeCloseTo", function () { - describe("::match()", function() { + describe("::match()", function () { - it("passes if the difference is lower than the default two decimal", function() { + it("passes if the difference is lower than the default two decimal", function () { expect(0)->toBeCloseTo(0.001); }); - it("fails if the difference is higher than the default two decimal", function() { + it("fails if the difference is higher than the default two decimal", function () { expect(0)->not->toBeCloseTo(0.01); }); - it("passes if the difference is lower than the precision", function() { + it("passes if the difference is lower than the precision", function () { expect(0)->toBeCloseTo(0.01, 1); }); - it("fails if the difference is higher than the precision", function() { + it("fails if the difference is higher than the precision", function () { expect(0)->not->toBeCloseTo(0.1, 1); }); - it("passes if the difference with the round is lower than the default two decimal", function() { + it("passes if the difference with the round is lower than the default two decimal", function () { expect(1.23)->toBeCloseTo(1.225); expect(1.23)->toBeCloseTo(1.234); }); - it("fails if the difference with the round is lower than the default two decimal", function() { + it("fails if the difference with the round is lower than the default two decimal", function () { expect(1.23)->not->toBeCloseTo(1.2249999); }); - it("return false if actual or expected are not a numeric value", function() { + it("return false if actual or expected are not a numeric value", function () { expect("string")->not->toBeCloseTo(1); expect(1)->not->toBeCloseTo("string"); @@ -53,17 +53,17 @@ }); - describe("::description()", function() { + describe("::description()", function () { - it("returns the description message", function() { + it("returns the description message", function () { ToBeCloseTo::match(1.23, 1.22499991, 2); $actual = ToBeCloseTo::description(); expect($actual['description'])->toBe('be close to expected relying to a precision of 2.'); - expect((string) $actual['params']['actual'])->toBe((string) 1.23); - expect((string) $actual['params']['expected'])->toBe((string) 1.22499991); - expect((string) $actual['params']['gap is >='])->toBe((string) 0.005); + expect((string) $actual['data']['actual'])->toBe((string) 1.23); + expect((string) $actual['data']['expected'])->toBe((string) 1.22499991); + expect((string) $actual['data']['gap is >='])->toBe((string) 0.005); }); diff --git a/spec/Suite/Matcher/ToBeFalsySpec.php b/spec/Suite/Matcher/ToBeFalsy.spec.php similarity index 52% rename from spec/Suite/Matcher/ToBeFalsySpec.php rename to spec/Suite/Matcher/ToBeFalsy.spec.php index cb46c04b..cbcd8662 100644 --- a/spec/Suite/Matcher/ToBeFalsySpec.php +++ b/spec/Suite/Matcher/ToBeFalsy.spec.php @@ -3,35 +3,35 @@ use Kahlan\Matcher\ToBeFalsy; -describe("toBeFalsy", function() { +describe("toBeFalsy", function () { - describe("::match()", function() { + describe("::match()", function () { - it("passes if false is fasly", function() { + it("passes if false is fasly", function () { expect(false)->toBeFalsy(); }); - it("passes if null is fasly", function() { + it("passes if null is fasly", function () { expect(null)->toBeFalsy(); }); - it("passes if [] is fasly", function() { + it("passes if [] is fasly", function () { expect([])->toBeFalsy(); }); - it("passes if 0 is fasly", function() { + it("passes if 0 is fasly", function () { expect(0)->toBeFalsy(); }); - it("passes if '' is fasly", function() { + it("passes if '' is fasly", function () { expect('')->toBeFalsy(); @@ -39,9 +39,9 @@ }); - describe("::description()", function() { + describe("::description()", function () { - it("returns the description message", function() { + it("returns the description message", function () { $actual = ToBeFalsy::description(); diff --git a/spec/Suite/Matcher/ToBeGreaterThanSpec.php b/spec/Suite/Matcher/ToBeGreaterThan.spec.php similarity index 55% rename from spec/Suite/Matcher/ToBeGreaterThanSpec.php rename to spec/Suite/Matcher/ToBeGreaterThan.spec.php index 91a49894..3dcddfb7 100644 --- a/spec/Suite/Matcher/ToBeGreaterThanSpec.php +++ b/spec/Suite/Matcher/ToBeGreaterThan.spec.php @@ -3,23 +3,23 @@ use Kahlan\Matcher\ToBeGreaterThan; -describe("toBeGreaterThan", function() { +describe("toBeGreaterThan", function () { - describe("::match()", function() { + describe("::match()", function () { - it("passes if 2 is > 1", function() { + it("passes if 2 is > 1", function () { expect(2)->toBeGreaterThan(1); }); - it("passes if 1 > 0.999", function() { + it("passes if 1 > 0.999", function () { expect(1)->toBeGreaterThan(0.999); }); - it("passes if 2 is not > 2", function() { + it("passes if 2 is not > 2", function () { expect(2)->not->toBeGreaterThan(2); @@ -27,9 +27,9 @@ }); - describe("::description()", function() { + describe("::description()", function () { - it("returns the description message", function() { + it("returns the description message", function () { $actual = ToBeGreaterThan::description(); diff --git a/spec/Suite/Matcher/ToBeLessThanSpec.php b/spec/Suite/Matcher/ToBeLessThan.spec.php similarity index 55% rename from spec/Suite/Matcher/ToBeLessThanSpec.php rename to spec/Suite/Matcher/ToBeLessThan.spec.php index b92bea53..52c8a2f6 100644 --- a/spec/Suite/Matcher/ToBeLessThanSpec.php +++ b/spec/Suite/Matcher/ToBeLessThan.spec.php @@ -3,23 +3,23 @@ use Kahlan\Matcher\ToBeLessThan; -describe("toBeLessThan", function() { +describe("toBeLessThan", function () { - describe("::match()", function() { + describe("::match()", function () { - it("passes if 1 is < 2", function() { + it("passes if 1 is < 2", function () { expect(1)->toBeLessThan(2); }); - it("passes if 0.999 < 1", function() { + it("passes if 0.999 < 1", function () { expect(0.999)->toBeLessThan(1); }); - it("passes if 2 is not < 2", function() { + it("passes if 2 is not < 2", function () { expect(2)->not->toBeLessThan(2); @@ -27,9 +27,9 @@ }); - describe("::description()", function() { + describe("::description()", function () { - it("returns the description message", function() { + it("returns the description message", function () { $actual = ToBeLessThan::description(); diff --git a/spec/Suite/Matcher/ToBeNullSpec.php b/spec/Suite/Matcher/ToBeNull.spec.php similarity index 53% rename from spec/Suite/Matcher/ToBeNullSpec.php rename to spec/Suite/Matcher/ToBeNull.spec.php index 809bd46d..cb77338f 100644 --- a/spec/Suite/Matcher/ToBeNullSpec.php +++ b/spec/Suite/Matcher/ToBeNull.spec.php @@ -3,35 +3,35 @@ use Kahlan\Matcher\ToBeNull; -describe("toBeNull", function() { +describe("toBeNull", function () { - describe("::match()", function() { + describe("::match()", function () { - it("passes if null is null", function() { + it("passes if null is null", function () { expect(null)->toBeNull(); }); - it("fails if false is null", function() { + it("fails if false is null", function () { expect(false)->not->toBeNull(); }); - it("fails if [] is null", function() { + it("fails if [] is null", function () { expect([])->not->toBeNull(); }); - it("fails if 0 is null", function() { + it("fails if 0 is null", function () { expect(0)->not->toBeNull(); }); - it("fails if '' is null", function() { + it("fails if '' is null", function () { expect('')->not->toBeNull(); @@ -39,9 +39,9 @@ }); - describe("::description()", function() { + describe("::description()", function () { - it("returns the description message", function() { + it("returns the description message", function () { $actual = ToBeNull::description(); diff --git a/spec/Suite/Matcher/ToBeTruthySpec.php b/spec/Suite/Matcher/ToBeTruthy.spec.php similarity index 52% rename from spec/Suite/Matcher/ToBeTruthySpec.php rename to spec/Suite/Matcher/ToBeTruthy.spec.php index 26d0e581..f05ec0d4 100644 --- a/spec/Suite/Matcher/ToBeTruthySpec.php +++ b/spec/Suite/Matcher/ToBeTruthy.spec.php @@ -3,29 +3,29 @@ use Kahlan\Matcher\ToBeTruthy; -describe("toBeTruthy", function() { +describe("toBeTruthy", function () { - describe("::match()", function() { + describe("::match()", function () { - it("passes if true is truthy", function() { + it("passes if true is truthy", function () { expect(true)->toBeTruthy(); }); - it("passes if 'Hello World' is truthy", function() { + it("passes if 'Hello World' is truthy", function () { expect('Hello World')->toBeTruthy(); }); - it("passes if 1 is truthy", function() { + it("passes if 1 is truthy", function () { expect(1)->toBeTruthy(); }); - it("passes if [1, 3, 7] is truthy", function() { + it("passes if [1, 3, 7] is truthy", function () { expect([1, 3, 7])->toBeTruthy(); @@ -33,9 +33,9 @@ }); - describe("::description()", function() { + describe("::description()", function () { - it("returns the description message", function() { + it("returns the description message", function () { $actual = ToBeTruthy::description(); diff --git a/spec/Suite/Matcher/ToContainSpec.php b/spec/Suite/Matcher/ToContain.spec.php similarity index 57% rename from spec/Suite/Matcher/ToContainSpec.php rename to spec/Suite/Matcher/ToContain.spec.php index eb7ab513..776e8477 100644 --- a/spec/Suite/Matcher/ToContainSpec.php +++ b/spec/Suite/Matcher/ToContain.spec.php @@ -5,25 +5,25 @@ use Kahlan\Spec\Mock\Collection; use Kahlan\Matcher\ToContain; -describe("toContain", function() { +describe("toContain", function () { - describe("::match()", function() { + describe("::match()", function () { - context("with an array", function() { + context("with an array", function () { - it("passes if 3 is in [1, 2, 3]", function() { + it("passes if 3 is in [1, 2, 3]", function () { expect([1, 2, 3])->toContain(3); }); - it("passes if 'a' is in ['a', 'b', 'c']", function() { + it("passes if 'a' is in ['a', 'b', 'c']", function () { expect(['a', 'b', 'c'])->toContain('a'); }); - it("passes if 'd' is in ['a', 'b', 'c']", function() { + it("passes if 'd' is in ['a', 'b', 'c']", function () { expect(['a', 'b', 'c'])->not->toContain('d'); @@ -31,21 +31,21 @@ }); - context("with a traversable instance", function() { + context("with a traversable instance", function () { - it("passes if 3 is in [1, 2, 3]", function() { + it("passes if 3 is in [1, 2, 3]", function () { expect(new Collection(['data' => [1, 2, 3]]))->toContain(3); }); - it("passes if 'a' is in ['a', 'b', 'c']", function() { + it("passes if 'a' is in ['a', 'b', 'c']", function () { expect(new Collection(['data' => ['a', 'b', 'c']]))->toContain('a'); }); - it("passes if 'd' is in ['a', 'b', 'c']", function() { + it("passes if 'd' is in ['a', 'b', 'c']", function () { expect(new Collection(['data' => ['a', 'b', 'c']]))->not->toContain('d'); @@ -53,16 +53,16 @@ }); - context("with a string", function() { + context("with a string", function () { - it("passes if contained in expected", function() { + it("passes if contained in expected", function () { expect('Hello World!')->toContain('World'); expect('World')->toContain('World'); }); - it("fails if not contained in expected", function() { + it("fails if not contained in expected", function () { expect('Hello World!')->not->toContain('world'); @@ -70,7 +70,7 @@ }); - it("fails with non string/array", function() { + it("fails with non string/array", function () { expect(new stdClass())->not->toContain('Hello World!'); expect(false)->not->toContain('0'); @@ -80,9 +80,9 @@ }); - describe("::description()", function() { + describe("::description()", function () { - it("returns the description message", function() { + it("returns the description message", function () { $actual = ToContain::description(); diff --git a/spec/Suite/Matcher/ToContainKeySpec.php b/spec/Suite/Matcher/ToContainKey.spec.php similarity index 80% rename from spec/Suite/Matcher/ToContainKeySpec.php rename to spec/Suite/Matcher/ToContainKey.spec.php index 3aeaa8cf..92b66976 100644 --- a/spec/Suite/Matcher/ToContainKeySpec.php +++ b/spec/Suite/Matcher/ToContainKey.spec.php @@ -6,13 +6,13 @@ use Kahlan\Spec\Mock\Traversable; use Kahlan\Matcher\ToContainKey; -describe("toContainKey", function() { +describe("toContainKey", function () { - describe("::match()", function() { + describe("::match()", function () { - context("with an array", function() { + context("with an array", function () { - it("passes when the key is contained", function() { + it("passes when the key is contained", function () { expect([1, 2, 3])->toContainKey(2); expect(['a' => 1, 'b' => 2, 'c' => 3])->toContainKey('a'); @@ -20,14 +20,14 @@ }); - it("passes when the keys are contained", function() { + it("passes when the keys are contained", function () { expect(['a' => 1, 'b' => 2, 'c' => 3])->toContainKeys('a', 'b'); expect(['a' => 1, 'b' => 2, 'c' => 3])->toContainKeys(['a', 'b']); }); - it("returns `false` when a key is missing", function() { + it("returns `false` when a key is missing", function () { expect(['a' => 1, 'b' => 2, 'c' => 3])->not->toContainKey('d'); expect(['a' => 1, 'b' => 2, 'c' => 3])->not->toContainKeys('a', 'b', 'd'); @@ -37,9 +37,9 @@ }); - context("with a collection instance", function() { + context("with a collection instance", function () { - it("passes when the key is contained", function() { + it("passes when the key is contained", function () { expect(new Collection(['data' => [1, 2, 3]]))->toContainKey(2); expect(new Collection(['data' => ['a' => 1, 'b' => 2, 'c' => 3]]))->toContainKey('a'); @@ -47,14 +47,14 @@ }); - it("passes when the keys are contained", function() { + it("passes when the keys are contained", function () { expect(new Collection(['data' => ['a' => 1, 'b' => 2, 'c' => 3]]))->toContainKeys('a', 'b'); expect(new Collection(['data' => ['a' => 1, 'b' => 2, 'c' => 3]]))->toContainKeys(['a', 'b']); }); - it("returns `false` when a key is missing", function() { + it("returns `false` when a key is missing", function () { expect(new Collection(['data' => ['a' => 1, 'b' => 2, 'c' => 3]]))->not->toContainKey('d'); expect(new Collection(['data' => ['a' => 1, 'b' => 2, 'c' => 3]]))->not->toContainKeys('a', 'b', 'd'); @@ -64,9 +64,9 @@ }); - context("with a traversable instance", function() { + context("with a traversable instance", function () { - it("passes when the key is contained", function() { + it("passes when the key is contained", function () { expect(new Traversable(['data' => [1, 2, 3]]))->toContainKey(2); expect(new Traversable(['data' => ['a' => 1, 'b' => 2, 'c' => 3]]))->toContainKey('a'); @@ -74,14 +74,14 @@ }); - it("passes when the keys are contained", function() { + it("passes when the keys are contained", function () { expect(new Traversable(['data' => ['a' => 1, 'b' => 2, 'c' => 3]]))->toContainKeys('a', 'b'); expect(new Traversable(['data' => ['a' => 1, 'b' => 2, 'c' => 3]]))->toContainKeys(['a', 'b']); }); - it("returns `false` when a key is missing", function() { + it("returns `false` when a key is missing", function () { expect(new Traversable(['data' => ['a' => 1, 'b' => 2, 'c' => 3]]))->not->toContainKey('d'); expect(new Traversable(['data' => ['a' => 1, 'b' => 2, 'c' => 3]]))->not->toContainKeys('a', 'd'); @@ -91,7 +91,7 @@ }); - it("fails with non array/collection/traversable", function() { + it("fails with non array/collection/traversable", function () { expect(new stdClass())->not->toContainKey('key'); expect(false)->not->toContainKey('0'); @@ -101,9 +101,9 @@ }); - describe("::description()", function() { + describe("::description()", function () { - it("returns the description message", function() { + it("returns the description message", function () { $actual = ToContainKey::description(); diff --git a/spec/Suite/Matcher/ToEcho.spec.php b/spec/Suite/Matcher/ToEcho.spec.php new file mode 100644 index 00000000..4c0b9da9 --- /dev/null +++ b/spec/Suite/Matcher/ToEcho.spec.php @@ -0,0 +1,49 @@ +toEcho('Hello World!'); + + }); + + it("passes if `'Hello World'` is not echoed", function () { + + expect(function () { + echo 'Good Bye!'; + })->not->toEcho('Hello World!'); + + }); + + }); + + describe("::description()", function () { + + it("returns the description message", function () { + + ToEcho::match(function () { + echo 'Hello'; + }, 'Good Bye!'); + $actual = ToEcho::description(); + + expect($actual)->toBe([ + 'description' => 'echo the expected string.', + 'data' => [ + "actual" => "Hello", + "expected" => "Good Bye!" + ] + ]); + + }); + + }); + +}); diff --git a/spec/Suite/Matcher/ToEchoSpec.php b/spec/Suite/Matcher/ToEchoSpec.php deleted file mode 100644 index ade732fb..00000000 --- a/spec/Suite/Matcher/ToEchoSpec.php +++ /dev/null @@ -1,43 +0,0 @@ -toEcho('Hello World!'); - - }); - - it("passes if `'Hello World'` is not echoed", function() { - - expect(function() { echo 'Good Bye!'; })->not->toEcho('Hello World!'); - - }); - - }); - - describe("::description()", function() { - - it("returns the description message", function() { - - ToEcho::match(function() {echo 'Hello';}, 'Good Bye!'); - $actual = ToEcho::description(); - - expect($actual)->toBe([ - 'description' => 'echo the expected string.', - 'params' => [ - "actual" => "Hello", - "expected" => "Good Bye!" - ] - ]); - - }); - - }); - -}); diff --git a/spec/Suite/Matcher/ToEqualSpec.php b/spec/Suite/Matcher/ToEqual.spec.php similarity index 57% rename from spec/Suite/Matcher/ToEqualSpec.php rename to spec/Suite/Matcher/ToEqual.spec.php index b3f0545f..0583f651 100644 --- a/spec/Suite/Matcher/ToEqualSpec.php +++ b/spec/Suite/Matcher/ToEqual.spec.php @@ -3,107 +3,107 @@ use Kahlan\Matcher\ToEqual; -describe("toEqual", function() { +describe("toEqual", function () { - describe("::match()", function() { + describe("::match()", function () { - it("passes if true == true", function() { + it("passes if true == true", function () { expect(true)->toEqual(true); }); - it("passes if false == false", function() { + it("passes if false == false", function () { expect(false)->toEqual(false); }); - it("passes if 1 == 1", function() { + it("passes if 1 == 1", function () { expect(1)->toEqual(1); }); - it("passes if [] == false", function() { + it("passes if [] == false", function () { expect([])->toEqual(false); }); - it("passes if 'Hello World' == true", function() { + it("passes if 'Hello World' == true", function () { expect('Hello World')->toEqual(true); }); - it("passes if 'Hello World' == 'Hello World'", function() { + it("passes if 'Hello World' == 'Hello World'", function () { expect('Hello World')->toEqual('Hello World'); }); - it("passes if 1 == true", function() { + it("passes if 1 == true", function () { expect(1)->toEqual(true); }); - it("passes if 0 == false", function() { + it("passes if 0 == false", function () { expect(0)->toEqual(false); }); - it("passes if [1, 3, 7] == [1, 3, 7]", function() { + it("passes if [1, 3, 7] == [1, 3, 7]", function () { expect([1, 3, 7])->toEqual([1, 3, 7]); }); - it("passes if ['a' => 1, 'b' => 3, 'c' => 7] == ['a' => 1, 'c' => 7, 'b' => 3]", function() { + it("passes if ['a' => 1, 'b' => 3, 'c' => 7] == ['a' => 1, 'c' => 7, 'b' => 3]", function () { expect(['a' => 1, 'b' => 3, 'c' => 7])->toEqual(['a' => 1, 'c' => 7, 'b' => 3]); }); - it("passes if [] is not == true", function() { + it("passes if [] is not == true", function () { expect([])->not->toEqual(true); }); - it("passes if true is not == false", function() { + it("passes if true is not == false", function () { expect(true)->not->toEqual(false); }); - it("passes if false is not == true", function() { + it("passes if false is not == true", function () { expect(false)->not->toEqual(true); }); - it("passes if 2 is not == 1", function() { + it("passes if 2 is not == 1", function () { expect(2)->not->toEqual(1); }); - it("passes if 'Hello World' is not == false", function() { + it("passes if 'Hello World' is not == false", function () { expect('Hello World')->not->toEqual(false); }); - it("passes if 'Hello World' is not == 'World Hello'", function() { + it("passes if 'Hello World' is not == 'World Hello'", function () { expect('Hello World')->not->toEqual('World Hello'); }); - it("passes if [1, 3, 7] is not == [1, 7, 3]", function() { + it("passes if [1, 3, 7] is not == [1, 7, 3]", function () { expect([1, 3, 7])->not->toEqual([1, 7, 3]); @@ -111,9 +111,9 @@ }); - describe("::description()", function() { + describe("::description()", function () { - it("returns the description message", function() { + it("returns the description message", function () { $actual = ToEqual::description(); diff --git a/spec/Suite/Matcher/ToHaveLengthSpec.php b/spec/Suite/Matcher/ToHaveLength.spec.php similarity index 51% rename from spec/Suite/Matcher/ToHaveLengthSpec.php rename to spec/Suite/Matcher/ToHaveLength.spec.php index bd6400fd..a60620f3 100644 --- a/spec/Suite/Matcher/ToHaveLengthSpec.php +++ b/spec/Suite/Matcher/ToHaveLength.spec.php @@ -3,23 +3,23 @@ use Kahlan\Matcher\ToHaveLength; -describe("toHaveLength", function() { +describe("toHaveLength", function () { - describe("::match()", function() { + describe("::match()", function () { - it("passes if 'Hello World' has a length of 11", function() { + it("passes if 'Hello World' has a length of 11", function () { expect('Hello World')->toHaveLength(11); }); - it("passes if [1, 3, 7] has a length of 3", function() { + it("passes if [1, 3, 7] has a length of 3", function () { expect([1, 3, 7])->toHaveLength(3); }); - it("passes if [] has a length of 0", function() { + it("passes if [] has a length of 0", function () { expect([])->toHaveLength(0); @@ -27,17 +27,17 @@ }); - describe("::description()", function() { + describe("::description()", function () { - it("returns the description message", function() { + it("returns the description message", function () { ToHaveLength::match([1, 2, 3], 5); $actual = ToHaveLength::description(); expect($actual['description'])->toBe('have the expected length.'); - expect($actual['params']['actual'])->toBe([1, 2, 3]); - expect($actual['params']['actual length'])->toBe(3); - expect($actual['params']['expected length'])->toBe(5); + expect($actual['data']['actual'])->toBe([1, 2, 3]); + expect($actual['data']['actual length'])->toBe(3); + expect($actual['data']['expected length'])->toBe(5); }); diff --git a/spec/Suite/Matcher/ToMatchSpec.php b/spec/Suite/Matcher/ToMatch.spec.php similarity index 57% rename from spec/Suite/Matcher/ToMatchSpec.php rename to spec/Suite/Matcher/ToMatch.spec.php index d01e9ed2..b8d915f4 100644 --- a/spec/Suite/Matcher/ToMatchSpec.php +++ b/spec/Suite/Matcher/ToMatch.spec.php @@ -3,23 +3,23 @@ use Kahlan\Matcher\ToMatch; -describe("toMatch", function() { +describe("toMatch", function () { - describe("::match()", function() { + describe("::match()", function () { - it("passes if 'Hello World!' match '/^H(?*)!$/'", function() { + it("passes if 'Hello World!' match '/^H(?*)!$/'", function () { expect('Hello World!')->toMatch('/^H(.*?)!$/'); }); - it("passes if actual match the closure", function() { + it("passes if actual match the closure", function () { - expect('Hello World!')->toMatch(function($actual) { + expect('Hello World!')->toMatch(function ($actual) { return $actual === 'Hello World!'; }); - expect('Hello')->not->toMatch(function($actual) { + expect('Hello')->not->toMatch(function ($actual) { return $actual === 'Hello World!'; }); @@ -27,9 +27,9 @@ }); - describe("::description()", function() { + describe("::description()", function () { - it("returns the description message", function() { + it("returns the description message", function () { $actual = ToMatch::description(); diff --git a/spec/Suite/Matcher/ToMatchEcho.spec.php b/spec/Suite/Matcher/ToMatchEcho.spec.php new file mode 100644 index 00000000..5e3558f4 --- /dev/null +++ b/spec/Suite/Matcher/ToMatchEcho.spec.php @@ -0,0 +1,63 @@ +toMatchEcho('/^H(.*?)!$/'); + + }); + + it("passes if `'Hello World'` is not echoed", function () { + + expect(function () { + echo 'Good Bye!'; + })->not->toMatchEcho('/^H(.*?)!$/'); + + }); + + it("passes if actual match the closure", function () { + + expect(function () { + echo 'Hello World!'; })->toMatchEcho(function ($actual) { + return $actual === 'Hello World!'; + }); + + expect(function () { + echo 'Hello'; })->not->toMatchEcho(function ($actual) { + return $actual === 'Hello World!'; + }); + + }); + + }); + + describe("::description()", function () { + + it("returns the description message", function () { + + ToMatchEcho::match(function () { + echo 'Hello'; + }, "/Bye/"); + $actual = ToMatchEcho::description(); + + expect($actual)->toBe([ + 'description' => 'matches expected regex in echoed string.', + 'data' => [ + "actual" => "Hello", + "expected" => "/Bye/" + ] + ]); + + }); + + }); + +}); diff --git a/spec/Suite/Matcher/ToMatchEchoSpec.php b/spec/Suite/Matcher/ToMatchEchoSpec.php deleted file mode 100644 index f070cd6a..00000000 --- a/spec/Suite/Matcher/ToMatchEchoSpec.php +++ /dev/null @@ -1,55 +0,0 @@ -toMatchEcho('/^H(.*?)!$/'); - - }); - - it("passes if `'Hello World'` is not echoed", function() { - - expect(function() { echo 'Good Bye!'; })->not->toMatchEcho('/^H(.*?)!$/'); - - }); - - it("passes if actual match the closure", function() { - - expect(function() { echo 'Hello World!'; })->toMatchEcho(function($actual) { - return $actual === 'Hello World!'; - }); - - expect(function() { echo 'Hello'; })->not->toMatchEcho(function($actual) { - return $actual === 'Hello World!'; - }); - - }); - - }); - - describe("::description()", function() { - - it("returns the description message", function() { - - ToMatchEcho::match(function() {echo 'Hello';}, "/Bye/"); - $actual = ToMatchEcho::description(); - - expect($actual)->toBe([ - 'description' => 'matches expected regex in echoed string.', - 'params' => [ - "actual" => "Hello", - "expected" => "/Bye/" - ] - ]); - - }); - - }); - -}); diff --git a/spec/Suite/Matcher/ToReceive.spec.php b/spec/Suite/Matcher/ToReceive.spec.php new file mode 100644 index 00000000..7abd83ed --- /dev/null +++ b/spec/Suite/Matcher/ToReceive.spec.php @@ -0,0 +1,761 @@ +previous = Interceptor::instance(); + Interceptor::unpatch(); + + $cachePath = rtrim(sys_get_temp_dir(), DS) . DS . 'kahlan'; + $include = ['Kahlan\Spec\\']; + $interceptor = Interceptor::patch(compact('include', 'cachePath')); + $interceptor->patchers()->add('pointcut', new PointcutPatcher()); + $interceptor->patchers()->add('monkey', new MonkeyPatcher()); + }); + + /** + * Restore Interceptor class. + */ + afterAll(function () { + Interceptor::load($this->previous); + }); + + context("with dynamic call", function () { + + it("expects called method to be called", function () { + + $foo = new Foo(); + expect($foo)->toReceive('message'); + $foo->message(); + + }); + + it("expects uncalled method to be uncalled", function () { + + $foo = new Foo(); + expect($foo)->not->toReceive('message'); + + }); + + it("expects method called in the past to be uncalled", function () { + + $foo = new Foo(); + $foo->message(); + expect($foo)->not->toReceive('message'); + + }); + + it("expects static method called using non-static way to still called (PHP behavior)", function () { + + $foo = new Foo(); + expect($foo)->toReceive('::version'); + $foo->version(); + + }); + + it("expects static method called using non-static way to be not called on instance", function () { + + $foo = new Foo(); + expect($foo)->not->toReceive('version'); + $foo->version(); + + }); + + it("throws an exception when trying to spy an invalid empty method", function () { + + expect(function () { + $foo = new Foo(); + expect($foo)->toReceive(); + })->toThrow(new InvalidArgumentException("Method name can't be empty.")); + + }); + + it("throws an exception when trying to play with core instance", function () { + + expect(function () { + $date = new DateTime(); + expect($date)->toReceive('getTimestamp'); + })->toThrow(new InvalidArgumentException("Can't Spy built-in PHP instances, create a test double using `Double::instance()`.")); + + }); + + context("when using with()", function () { + + it("expects called method to be called with correct arguments", function () { + + $foo = new Foo(); + expect($foo)->toReceive('message')->with('My Message', 'My Other Message'); + $foo->message('My Message', 'My Other Message'); + + }); + + it("expects called method with incorrect arguments to not be called", function () { + + $foo = new Foo(); + expect($foo)->not->toReceive('message')->with('My Message'); + $foo->message('Incorrect Message'); + + }); + + it("expects called method with missing arguments to not be called", function () { + + $foo = new Foo(); + expect($foo)->not->toReceive('message')->with('My Message'); + $foo->message(); + + }); + + it("expects arguments match the toContain argument matcher", function () { + + $foo = new Foo(); + expect($foo)->toReceive('message')->with(Arg::toContain('My Message')); + $foo->message(['My Message', 'My Other Message']); + + }); + + it("expects arguments match the argument matchers", function () { + + $foo = new Foo(); + expect($foo)->toReceive('message')->with(Arg::toBeA('boolean')); + expect($foo)->toReceive('message')->with(Arg::toBeA('string')); + $foo->message(true); + $foo->message('Hello World'); + + }); + + it("expects arguments to not match the toContain argument matcher", function () { + + $foo = new Foo(); + expect($foo)->not->toReceive('message')->with(Arg::toContain('Message')); + $foo->message(['My Message', 'My Other Message']); + + }); + + }); + + context("when using times()", function () { + + it("expects called method to be called exactly once", function () { + + $foo = new Foo(); + expect($foo)->toReceive('message')->once(); + $foo->message(); + + }); + + it("expects called method to be called exactly a specified times", function () { + + $foo = new Foo(); + expect($foo)->toReceive('message')->times(3); + $foo->message(); + $foo->message(); + $foo->message(); + + }); + + it("expects called method not called exactly a specified times to be uncalled", function () { + + $foo = new Foo(); + expect($foo)->not->toReceive('message')->times(1); + $foo->message(); + $foo->message(); + + }); + + }); + + context("when using classname", function () { + + it("expects called method to be called", function () { + + $foo = new Foo(); + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('message'); + $foo->message(); + + }); + + it("expects method called in the past to be called", function () { + + $foo = new Foo(); + $foo->message(); + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->not->toReceive('message'); + + }); + + it("expects uncalled method to be uncalled", function () { + + $foo = new Foo(); + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->not->toReceive('message'); + }); + + it("expects called method to be called exactly once", function () { + + $foo = new Foo(); + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('message')->once(); + $foo->message(); + + }); + + it("expects called method to be called exactly a specified times", function () { + + $foo = new Foo(); + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('message')->times(3); + $foo->message(); + $foo->message(); + $foo->message(); + + }); + + it("expects called method not called exactly a specified times to be uncalled", function () { + + $foo = new Foo(); + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->not->toReceive('message')->times(1); + $foo->message(); + $foo->message(); + + }); + + it("expects uncalled method to be uncalled", function () { + + $foo = new Foo(); + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->not->toReceive('message'); + + }); + + it("expects not overrided method to also be called on method's __CLASS__", function () { + + $bar = new SubBar(); + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Bar')->toReceive('send'); + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\SubBar')->toReceive('send'); + $bar->send(); + + }); + + it("expects overrided method to not be called on method's __CLASS__", function () { + + $bar = new SubBar(); + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Bar')->not->toReceive('overrided'); + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\SubBar')->toReceive('overrided'); + $bar->overrided(); + + }); + + }); + + context("with chain of methods", function () { + + it("expects called chain to be called", function () { + + $foo = new Foo(); + allow($foo)->toReceive('a', 'b', 'c')->andReturn('something'); + expect($foo)->toReceive('a', 'b', 'c')->once(); + $query = $foo->a(); + $select = $query->b(); + expect($select->c())->toBe('something'); + + }); + + it("expects not called chain to be uncalled", function () { + + $foo = new Foo(); + allow($foo)->toReceive('a', 'b', 'c')->andReturn('something'); + expect($foo)->not->toReceive('a', 'c', 'b')->once(); + $query = $foo->a(); + $select = $query->b(); + $select->c(); + + }); + + it('auto monkey patch core classes using a stub when possible', function () { + + allow('PDO')->toReceive('prepare', 'fetchAll')->andReturn([['name' => 'bob']]); + expect('PDO')->toReceive('prepare')->once(); + $user = new User(); + expect($user->all())->toBe([['name' => 'bob']]); + + }); + + it('allows to mix static/dynamic methods', function () { + + allow('Kahlan\Spec\Fixture\Plugin\Monkey\User')->toReceive('::create', 'all')->andReturn([['name' => 'bob']]); + expect('Kahlan\Spec\Fixture\Plugin\Monkey\User')->toReceive('::create', 'all')->once(); + $user = User::create(); + expect($user->all())->toBe([['name' => 'bob']]); + + }); + + }); + + context("with chain of methods and arguments requirements", function () { + + it("expects called method to be called with correct arguments", function () { + + $foo = new Foo(); + expect($foo)->toReceive('message')->where(['message' => ['My Message', 'My Other Message']]); + $foo->message('My Message', 'My Other Message'); + + }); + + it("expects stubbed chain called with matching arguments are called", function () { + + $foo = new Foo(); + allow($foo)->toReceive('a', 'b', 'c'); + expect($foo)->toReceive('a', 'b', 'c')->where([ + 'a' => [1], + 'b' => [2], + 'c' => [3] + ]); + + $query = $foo->a(1); + $select = $query->b(2); + $select->c(3); + + }); + + it("expects stubbed chain not called with matching arguments are uncalled", function () { + + $foo = new Foo(); + allow($foo)->toReceive('a', 'b', 'c'); + expect($foo)->not->toReceive('a', 'b', 'c')->where([ + 'a' => [1], + 'b' => [2], + 'c' => [3] + ]); + + $query = $foo->a(1); + $select = $query->b(2); + $select->c(0); + + }); + + it("expects stubbed chain to be called if one path exists", function () { + + $foo = new Foo(); + $double = Double::instance(); + allow($foo)->toReceive('a')->andReturn(null, $double); + allow($double)->toReceive('b')->andReturn('success'); + + expect($foo)->toReceive('a', 'b')->where([ + 'a' => ['arg1'], + 'b' => ['arg2'] + ]); + + expect($foo->a('arg1'))->toBe(null); + expect($instance = $foo->a('arg1'))->toBe($double); + + expect($instance->b('arg2'))->toBe('success'); + + }); + + + it("throws an exception when required arguments are applied on a method not present in the chain", function () { + + expect(function () { + $foo = new Foo(); + expect($foo)->not->toReceive('a')->where(['b' => [2]]); + })->toThrow(new InvalidArgumentException("Unexisting `b` as method as part of the chain definition.")); + + }); + + it("throws an exception when required arguments are not an array", function () { + + expect(function () { + $foo = new Foo(); + expect($foo)->not->toReceive('a')->where(['a' => 2]); + })->toThrow(new InvalidArgumentException("Argument requirements must be an arrays for `a` method.")); + + }); + + }); + + }); + + context("with static call", function () { + + it("expects called method to be called", function () { + + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('::version'); + Foo::version(); + + }); + + it("expects method called in the past to be uncalled", function () { + + Foo::version(); + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->not->toReceive('::version'); + + }); + + it("expects uncalled method to be uncalled", function () { + + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->not->toReceive('::version'); + + }); + + it("expects called method to be called exactly once", function () { + + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('::version')->once(); + Foo::version(); + + }); + + it("expects called method to be called exactly a specified times", function () { + + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('::version')->times(3); + Foo::version(); + Foo::version(); + Foo::version(); + + }); + + it("expects called method not called exactly a specified times to be uncalled", function () { + + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->not->toReceive('::version')->times(1); + Foo::version(); + Foo::version(); + + }); + + it("expects called method to not be dynamically called", function () { + + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->not->toReceive('version'); + Foo::version(); + + }); + + it("expects called method on instance to be called on classname", function () { + + $foo = new Foo(); + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('::version'); + $foo::version(); + + }); + + it("expects called method on instance to not be dynamically called", function () { + + $foo = new Foo(); + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->not->toReceive('version'); + $foo::version(); + + }); + + it("expects called method on instance to be called on classname (alternative syntax)", function () { + + $foo = new Foo(); + expect($foo)->toReceive('::version'); + $foo::version(); + + }); + + it("throws an exception when trying to spy an unexisting class", function () { + + $closure = function () { + expect('My\Unexisting\Classname\Foo')->toReceive('::test'); + }; + $message = "Can't Spy the unexisting class `My\\Unexisting\\Classname\\Foo`."; + expect($closure)->toThrow(new InvalidArgumentException($message)); + + }); + + context("with chain of methods", function () { + + it("expects called chain to be called", function () { + + allow('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('::getQuery', '::newQuery', '::from')->andReturn('something'); + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('::getQuery', '::newQuery', '::from'); + $query = Foo::getQuery(); + $select = $query::newQuery(); + expect($select::from())->toBe('something'); + + }); + + it("expects not called chain to be uncalled", function () { + + allow('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('::getQuery', '::from', '::newQuery')->andReturn('something'); + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->not->toReceive('::getQuery', '::from', '::newQuery'); + $query = Foo::getQuery(); + $select = $query::newQuery(); + $select::from(); + + }); + + }); + + context("with chain of methods and arguments requirements", function () { + + it("expects stubbed chain called with matching arguments are called", function () { + + $foo = new Foo(); + allow('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('::getQuery', '::newQuery', '::from'); + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('::getQuery', '::newQuery', '::from')->where([ + '::getQuery' => [1], + '::newQuery' => [2], + '::from' => [3] + ]); + + $query = Foo::getQuery(1); + $select = $query::newQuery(2); + $select::from(3); + + }); + + it("expects stubbed chain not called with matching arguments are uncalled", function () { + + $foo = new Foo(); + allow('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('::getQuery', '::newQuery', '::from'); + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->not->toReceive('::getQuery', '::newQuery', '::from')->where([ + '::getQuery' => [1], + '::newQuery' => [2], + '::from' => [3] + ]); + + $query = Foo::getQuery(1); + $select = $query::newQuery(2); + $select::from(0); + + }); + + }); + + }); + + context("with ordered enabled", function () { + + describe("::match()", function () { + + it("expects called methods to be called in a defined order", function () { + + $foo = new Foo(); + expect($foo)->toReceive('message')->ordered; + expect($foo)->toReceive('::version')->ordered; + expect($foo)->toReceive('bar')->ordered; + $foo->message(); + $foo::version(); + $foo->bar(); + + }); + + it("expects called methods to be called in a defined order only once", function () { + + $foo = new Foo(); + expect($foo)->toReceive('message')->ordered->once(); + expect($foo)->toReceive('::version')->ordered->once(); + expect($foo)->toReceive('bar')->ordered->once(); + $foo->message(); + $foo::version(); + $foo->bar(); + + }); + + it("expects called methods to be called in a defined order a specific number of times", function () { + + $foo = new Foo(); + expect($foo)->toReceive('message')->ordered->times(1); + expect($foo)->toReceive('::version')->ordered->times(2); + expect($foo)->toReceive('bar')->ordered->times(3); + $foo->message(); + $foo::version(); + $foo::version(); + $foo->bar(); + $foo->bar(); + $foo->bar(); + + }); + + it("expects called methods called in a different order to be uncalled", function () { + + $foo = new Foo(); + expect($foo)->toReceive('message')->ordered; + expect($foo)->not->toReceive('bar')->ordered; + $foo->bar(); + $foo->message(); + + }); + + it("expects called methods called a specific number of times but in a different order to be uncalled", function () { + + $foo = new Foo(); + expect($foo)->toReceive('message')->ordered->times(1); + expect($foo)->toReceive('::version')->ordered->times(2); + expect($foo)->not->toReceive('bar')->ordered->times(1); + $foo->message(); + $foo::version(); + $foo->bar(); + $foo::version(); + + }); + + it("expects to work as `toReceive` for the first call", function () { + + $foo = new Foo(); + expect($foo)->toReceive('message'); + $foo->message(); + + }); + + it("expects called methods are consumated", function () { + + $foo = new Foo(); + expect($foo)->toReceive('message')->ordered; + expect($foo)->not->toReceive('message')->ordered; + $foo->message(); + + }); + + it("expects called methods are consumated using classname", function () { + + $foo = new Foo(); + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('message')->ordered; + expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->not->toReceive('message')->ordered; + $foo->message(); + + }); + + }); + + }); + + }); + + describe("->description()", function () { + + it("returns the description message for not received call", function () { + + $stub = Double::instance(); + $matcher = new ToReceive($stub, 'method'); + + $matcher->resolve([ + 'instance' => $matcher, + 'data' => [ + 'actual' => $stub, + 'expected' => 'method', + 'logs' => [] + ] + ]); + + $actual = $matcher->description(); + + expect($actual['description'])->toBe('receive the expected method.'); + expect($actual['data'])->toBe([ + 'actual received calls' => [], + 'expected to receive' => 'method' + ]); + + }); + + it("returns the description message for not received call the specified number of times", function () { + + $stub = Double::instance(); + $matcher = new ToReceive($stub, 'method'); + $matcher->times(2); + + $matcher->resolve([ + 'instance' => $matcher, + 'data' => [ + 'actual' => $stub, + 'expected' => 'method', + 'logs' => [] + ] + ]); + + $actual = $matcher->description(); + + expect($actual['description'])->toBe('receive the expected method the expected times.'); + expect($actual['data'])->toBe([ + 'actual received calls' => [], + 'expected to receive' => 'method', + 'expected received times' => 2 + ]); + + }); + + it("returns the description message for wrong passed arguments", function () { + + $stub = Double::instance(); + $matcher = new ToReceive($stub, 'method'); + $matcher->with('Hello World!'); + + $stub->method('Good Bye!'); + + $matcher->resolve([ + 'instance' => $matcher, + 'data' => [ + 'actual' => $stub, + 'expected' => 'method', + 'logs' => [] + ] + ]); + + $actual = $matcher->description(); + + expect($actual['description'])->toBe('receive the expected method with expected parameters.'); + expect($actual['data'])->toBe([ + 'actual received' => 'method', + 'actual received times' => 1, + 'actual received parameters list' => [['Good Bye!']], + 'expected to receive' => 'method', + 'expected parameters' => ['Hello World!'] + ]); + + }); + + }); + + describe("->ordered()", function () { + + it("throw an exception when trying to play with core instance", function () { + + expect(function () { + $foo = new Foo(); + $matcher = new ToReceive($foo, 'a'); + $matcher->order; + })->toThrow(new Exception("Unsupported attribute `order` only `ordered` is available.")); + + }); + + }); + + describe("->resolve()", function () { + + it("throw an exception when not explicitly defining the stub value", function () { + + expect(function () { + $foo = new Foo(); + $matcher = new ToReceive($foo, ['a', 'b', 'c']); + $matcher->resolve([ + 'instance' => $matcher, + 'data' => [ + 'actual' => $foo, + 'expected' => ['a', 'b', 'c'], + 'logs' => [] + ] + ]); + })->toThrow(new InvalidArgumentException("Kahlan can't Spy chained methods on real PHP code, you need to Stub the chain first.")); + + }); + + }); + +}); diff --git a/spec/Suite/Matcher/ToReceiveNextSpec.php b/spec/Suite/Matcher/ToReceiveNextSpec.php deleted file mode 100644 index bb80a600..00000000 --- a/spec/Suite/Matcher/ToReceiveNextSpec.php +++ /dev/null @@ -1,124 +0,0 @@ -previous = Interceptor::instance(); - Interceptor::unpatch(); - - $cachePath = rtrim(sys_get_temp_dir(), DS) . DS . 'kahlan'; - $include = ['Kahlan\Spec\\']; - $interceptor = Interceptor::patch(compact('include', 'cachePath')); - $interceptor->patchers()->add('pointcut', new Pointcut()); - }); - - /** - * Restore Interceptor class. - */ - after(function() { - Interceptor::load($this->previous); - }); - - it("expects called methods to be called in a defined order", function() { - - $foo = new Foo(); - expect($foo)->toReceive('message'); - expect($foo)->toReceiveNext('::version'); - expect($foo)->toReceiveNext('bar'); - $foo->message(); - $foo::version(); - $foo->bar(); - - }); - - it("expects called methods to be called in a defined order only once", function() { - - $foo = new Foo(); - expect($foo)->toReceive('message')->once(); - expect($foo)->toReceiveNext('::version')->once(); - expect($foo)->toReceiveNext('bar')->once(); - $foo->message(); - $foo::version(); - $foo->bar(); - - }); - - it("expects called methods to be called in a defined order a specific number of times", function() { - - $foo = new Foo(); - expect($foo)->toReceive('message')->times(1); - expect($foo)->toReceiveNext('::version')->times(2); - expect($foo)->toReceiveNext('bar')->times(3); - $foo->message(); - $foo::version(); - $foo::version(); - $foo->bar(); - $foo->bar(); - $foo->bar(); - - }); - - it("expects called methods called in a different order to be uncalled", function() { - - $foo = new Foo(); - expect($foo)->toReceive('message'); - expect($foo)->not->toReceiveNext('bar'); - $foo->bar(); - $foo->message(); - - }); - - it("expects called methods called a specific number of times but in a different order to be uncalled", function() { - - $foo = new Foo(); - expect($foo)->toReceive('message')->times(1); - expect($foo)->toReceiveNext('::version')->times(2); - expect($foo)->not->toReceiveNext('bar')->times(1); - $foo->message(); - $foo::version(); - $foo->bar(); - $foo::version(); - - }); - - it("expects to work as `toReceive` for the first call", function() { - - $foo = new Foo(); - expect($foo)->toReceiveNext('message'); - $foo->message(); - - }); - - it("expects called methods are consumated", function() { - - $foo = new Foo(); - expect($foo)->toReceive('message'); - expect($foo)->not->toReceiveNext('message'); - $foo->message(); - - }); - - it("expects called methods are consumated using classname", function() { - - $foo = new Foo(); - expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('message'); - expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->not->toReceiveNext('message'); - $foo->message(); - - }); - - }); - -}); diff --git a/spec/Suite/Matcher/ToReceiveSpec.php b/spec/Suite/Matcher/ToReceiveSpec.php deleted file mode 100644 index e09023fd..00000000 --- a/spec/Suite/Matcher/ToReceiveSpec.php +++ /dev/null @@ -1,387 +0,0 @@ -previous = Interceptor::instance(); - Interceptor::unpatch(); - - $cachePath = rtrim(sys_get_temp_dir(), DS) . DS . 'kahlan'; - $include = ['Kahlan\Spec\\']; - $interceptor = Interceptor::patch(compact('include', 'cachePath')); - $interceptor->patchers()->add('pointcut', new Pointcut()); - }); - - /** - * Restore Interceptor class. - */ - after(function() { - Interceptor::load($this->previous); - }); - - context("with dynamic call", function() { - - it("expects called method to be called", function() { - - $foo = new Foo(); - expect($foo)->toReceive('message'); - $foo->message(); - - }); - - it("expects called method to be called exactly once", function() { - - $foo = new Foo(); - expect($foo)->toReceive('message')->once(); - $foo->message(); - - }); - - it("expects called method to be called exactly a specified times", function() { - - $foo = new Foo(); - expect($foo)->toReceive('message')->times(3); - $foo->message(); - $foo->message(); - $foo->message(); - - }); - - it("expects called method not called exactly a specified times to be uncalled", function() { - - $foo = new Foo(); - expect($foo)->not->toReceive('message')->times(1); - $foo->message(); - $foo->message(); - - }); - - it("expects static method called using non-static way to still called (PHP behavior)", function() { - - $foo = new Foo(); - expect($foo)->toReceive('::version'); - $foo->version(); - - }); - - it("expects static method called using non-static way to be not called on instance", function() { - - $foo = new Foo(); - expect($foo)->not->toReceive('version'); - $foo->version(); - - }); - - it("expects uncalled method to be uncalled", function() { - - $foo = new Foo(); - expect($foo)->not->toReceive('message'); - - }); - - context("when using with()", function() { - - it("expects called method to be called with correct params", function() { - - $foo = new Foo(); - expect($foo)->toReceive('message')->with('My Message', 'My Other Message'); - $foo->message('My Message', 'My Other Message'); - - }); - - it("expects called method with incorrect params to not be called", function() { - - $foo = new Foo(); - expect($foo)->not->toReceive('message')->with('My Message'); - $foo->message('Incorrect Message'); - - }); - - it("expects called method with missing params to not be called", function() { - - $foo = new Foo(); - expect($foo)->not->toReceive('message')->with('My Message'); - $foo->message(); - - }); - - }); - - context("when using with() and matchers", function() { - - it("expects params match the toContain argument matcher", function() { - - $foo = new Foo(); - expect($foo)->toReceive('message')->with(Arg::toContain('My Message')); - $foo->message(['My Message', 'My Other Message']); - - }); - - it("expects params match the argument matchers", function() { - - $foo = new Foo(); - expect($foo)->toReceive('message')->with(Arg::toBeA('boolean')); - expect($foo)->toReceiveNext('message')->with(Arg::toBeA('string')); - $foo->message(true); - $foo->message('Hello World'); - - }); - - it("expects params to not match the toContain argument matcher", function() { - - $foo = new Foo(); - expect($foo)->not->toReceive('message')->with(Arg::toContain('Message')); - $foo->message(['My Message', 'My Other Message']); - - }); - - }); - - context("when using classname", function() { - - it("expects called method to be called", function() { - - $foo = new Foo(); - expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('message'); - $foo->message(); - - }); - - it("expects called method to be called exactly once", function() { - - $foo = new Foo(); - expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('message')->once(); - $foo->message(); - - }); - - it("expects called method to be called exactly a specified times", function() { - - $foo = new Foo(); - expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('message')->times(3); - $foo->message(); - $foo->message(); - $foo->message(); - - }); - - it("expects called method not called exactly a specified times to be uncalled", function() { - - $foo = new Foo(); - expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->not->toReceive('message')->times(1); - $foo->message(); - $foo->message(); - - }); - - it("expects uncalled method to be uncalled", function() { - - $foo = new Foo(); - expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->not->toReceive('message'); - - }); - - it("expects called method to be uncalled using a wrong classname", function() { - - $foo = new Foo(); - expect('Kahlan\Spec\Fixture\Plugin\Pointcut\FooFoo')->not->toReceive('message'); - $foo->message(); - - }); - - it("expects not overrided method to also be called on method's __CLASS__", function() { - - $bar = new SubBar(); - expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Bar')->toReceive('send'); - expect('Kahlan\Spec\Fixture\Plugin\Pointcut\SubBar')->toReceive('send'); - $bar->send(); - - }); - - it("expects overrided method to not be called on method's __CLASS__", function() { - - $bar = new SubBar(); - expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Bar')->not->toReceive('overrided'); - expect('Kahlan\Spec\Fixture\Plugin\Pointcut\SubBar')->toReceive('overrided'); - $bar->overrided(); - - }); - - }); - }); - - context("with static call", function() { - - it("expects called method to be called", function() { - - expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('::version'); - Foo::version(); - - }); - - it("expects called method to be called exactly once", function() { - - expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('::version')->once(); - Foo::version(); - - }); - - it("expects called method to be called exactly a specified times", function() { - - expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('::version')->times(3); - Foo::version(); - Foo::version(); - Foo::version(); - - }); - - it("expects called method not called exactly a specified times to be uncalled", function() { - - expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->not->toReceive('::version')->times(1); - Foo::version(); - Foo::version(); - - }); - - it("expects called method to not be dynamically called", function() { - - expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->not->toReceive('version'); - Foo::version(); - - }); - - it("expects called method on instance to be called on classname", function() { - - $foo = new Foo(); - expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->toReceive('::version'); - $foo::version(); - - }); - - it("expects called method on instance to not be dynamically called", function() { - - $foo = new Foo(); - expect('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->not->toReceive('version'); - $foo::version(); - - }); - - it("expects called method on instance to be called on classname (alternative syntax)", function() { - - $foo = new Foo(); - expect($foo)->toReceive('::version'); - $foo::version(); - - }); - - }); - - }); - - describe("->description()", function() { - - it("returns the description message", function() { - - $stub = Stub::create(); - $matcher = new ToReceive($stub, 'method'); - $message = $matcher->message(); - - expect($message)->toBeAnInstanceOf('Kahlan\Plugin\Call\Message'); - - }); - - it("returns the description message for not received call", function() { - - $stub = Stub::create(); - $matcher = new ToReceive($stub, 'method'); - - $matcher->resolve([ - 'instance' => $matcher, - 'params' => [ - 'actual' => $stub, - 'expected' => 'method', - 'logs' => [] - ] - ]); - - $actual = $matcher->description(); - - expect($actual['description'])->toBe('receive the correct message.'); - expect($actual['params'])->toBe([ - 'actual received' => ['__construct'], - 'expected' => 'method' - ]); - - }); - - it("returns the description message for not received call the specified number of times", function() { - - $stub = Stub::create(); - $matcher = new ToReceive($stub, 'method'); - $matcher->times(2); - - $matcher->resolve([ - 'instance' => $matcher, - 'params' => [ - 'actual' => $stub, - 'expected' => 'method', - 'logs' => [] - ] - ]); - - $actual = $matcher->description(); - - expect($actual['description'])->toBe('receive the correct message.'); - expect($actual['params'])->toBe([ - 'actual received' => ['__construct'], - 'expected' => 'method', - 'called times' => 2 - ]); - - }); - - it("returns the description message for wrong passed arguments", function() { - - $stub = Stub::create(); - $matcher = new ToReceive($stub, 'method'); - $matcher->with('Hello World!'); - - $stub->method('Good Bye!'); - - $matcher->resolve([ - 'instance' => $matcher, - 'params' => [ - 'actual' => $stub, - 'expected' => 'method', - 'logs' => [] - ] - ]); - - $actual = $matcher->description(); - - expect($actual['description'])->toBe('receive correct parameters.'); - expect($actual['params'])->toBe([ - 'actual with' => ['Good Bye!'], - 'expected with' => ['Hello World!'] - ]); - - }); - - }); - -}); diff --git a/spec/Suite/Matcher/ToThrowSpec.php b/spec/Suite/Matcher/ToThrow.spec.php similarity index 67% rename from spec/Suite/Matcher/ToThrowSpec.php rename to spec/Suite/Matcher/ToThrow.spec.php index e471db0e..5b830cae 100644 --- a/spec/Suite/Matcher/ToThrowSpec.php +++ b/spec/Suite/Matcher/ToThrow.spec.php @@ -5,64 +5,64 @@ use RuntimeException; use Kahlan\Matcher\ToThrow; -describe("toThrow", function() { +describe("toThrow", function () { - describe("::match()", function() { + describe("::match()", function () { - it("catches any kind of exception", function() { + it("catches any kind of exception", function () { - $closure = function() { + $closure = function () { throw new RuntimeException(); }; expect($closure)->toThrow(); - $closure = function() { + $closure = function () { throw new Exception('exception message'); }; expect($closure)->toThrow(); }); - it("catches any kind of exception but with a specific code", function() { + it("catches any kind of exception but with a specific code", function () { - $closure = function() { + $closure = function () { throw new RuntimeException('runtime error', 500); }; expect($closure)->toThrow(null, 500); - $closure = function() { + $closure = function () { throw new Exception('exception message', 500); }; expect($closure)->toThrow(null, 500); }); - it("doesn't catches any kind of exception with a specific code", function() { + it("doesn't catches any kind of exception with a specific code", function () { - $closure = function() { + $closure = function () { throw new Exception('exception message'); }; expect($closure)->not->toThrow(null, 400); - $closure = function() { + $closure = function () { throw new Exception('exception message', 500); }; expect($closure)->not->toThrow(null, 400); }); - it("catches a detailed exception", function() { + it("catches a detailed exception", function () { - $closure = function() { + $closure = function () { throw new RuntimeException('exception message'); }; expect($closure)->toThrow(new RuntimeException('exception message')); }); - it("catches a detailed exception with some specific code", function() { + it("catches a detailed exception with some specific code", function () { - $closure = function() { + $closure = function () { throw new RuntimeException('exception message', 500); }; expect($closure)->not->toThrow(new RuntimeException('exception message')); @@ -70,69 +70,69 @@ }); - it("catches a detailed exception using the message name only", function() { + it("catches a detailed exception using the message name only", function () { - $closure = function() { + $closure = function () { throw new RuntimeException('exception message'); }; expect($closure)->toThrow('exception message'); }); - it("catches an exception message using a regular expression", function() { + it("catches an exception message using a regular expression", function () { - $closure = function() { + $closure = function () { throw new RuntimeException('exception stuff message'); }; expect($closure)->toThrow('/exception (.*?) message/'); - $closure = function() { + $closure = function () { throw new RuntimeException('exception stuff message'); }; expect($closure)->toThrow('~exception (.*?) message~'); - $closure = function() { + $closure = function () { throw new RuntimeException('exception stuff message'); }; expect($closure)->toThrow('#exception (.*?) message#'); - $closure = function() { + $closure = function () { throw new RuntimeException('exception stuff message'); }; expect($closure)->toThrow('@exception (.*?) message@'); - $closure = function() { + $closure = function () { throw new RuntimeException('exception stuff message'); }; expect($closure)->not->toThrow('@exception (.*?) message#'); }); - it("doesn't catch not an exception", function() { - $closure = function() { + it("doesn't catch not an exception", function () { + $closure = function () { return true; }; expect($closure)->not->toThrow(new Exception()); }); - it("doesn't catch whatever exception if a detailed one is expected", function() { + it("doesn't catch whatever exception if a detailed one is expected", function () { - $closure = function() { + $closure = function () { throw new RuntimeException(); }; expect($closure)->not->toThrow(new RuntimeException('exception message')); }); - it("doesn't catch the exception if the expected exception has a different class name", function() { + it("doesn't catch the exception if the expected exception has a different class name", function () { - $closure = function() { + $closure = function () { throw new Exception('exception message'); }; expect($closure)->not->toThrow(new RuntimeException('exception message')); - $closure = function() { + $closure = function () { throw new RuntimeException('exception message'); }; expect($closure)->not->toThrow(new Exception('exception message')); @@ -141,14 +141,14 @@ }); - describe("::description()", function() { + describe("::description()", function () { - it("returns the description message", function() { + it("returns the description message", function () { $actualException = new Exception(); $expectedException = new Exception(); - $actual = function() use ($actualException) { + $actual = function () use ($actualException) { throw $actualException; }; @@ -156,33 +156,33 @@ $actual = ToThrow::description(); expect($actual['description'])->toBe('throw a compatible exception.'); - expect($actual['params']['actual'])->toBe($actualException); - expect($actual['params']['expected'])->toBe($expectedException); + expect($actual['data']['actual'])->toBe($actualException); + expect($actual['data']['expected'])->toBe($expectedException); }); - it("returns the description message when actual doesn't throw any exception", function() { + it("returns the description message when actual doesn't throw any exception", function () { $exception = new Exception(); - ToThrow::match(function() {}, $exception, 0); + ToThrow::match(function () {}, $exception, 0); $actual = ToThrow::description(); expect($actual['description'])->toBe('throw a compatible exception.'); - expect($actual['params']['actual'])->toBe(null); - expect($actual['params']['expected'])->toBe($exception); + expect($actual['data']['actual'])->toBe(null); + expect($actual['data']['expected'])->toBe($exception); }); - it("returns the description message when the expected value is a string", function() { + it("returns the description message when the expected value is a string", function () { - ToThrow::match(function() {}, 'Expected exception message', 0); + ToThrow::match(function () {}, 'Expected exception message', 0); $actual = ToThrow::description(); expect($actual['description'])->toBe('throw a compatible exception.'); - expect($actual['params']['actual'])->toBe(null); - expect($actual['params']['expected'])->toBeAnInstanceOf('Kahlan\Matcher\AnyException'); - expect($actual['params']['expected']->getMessage())->toBe('Expected exception message'); + expect($actual['data']['actual'])->toBe(null); + expect($actual['data']['expected'])->toBeAnInstanceOf('Kahlan\Matcher\AnyException'); + expect($actual['data']['expected']->getMessage())->toBe('Expected exception message'); }); diff --git a/spec/Suite/Plugin/Call/Calls.spec.php b/spec/Suite/Plugin/Call/Calls.spec.php new file mode 100644 index 00000000..08a98858 --- /dev/null +++ b/spec/Suite/Plugin/Call/Calls.spec.php @@ -0,0 +1,66 @@ + 'methodName' + ]); + + $logs = Calls::logs(); + + expect($logs[0][0])->toEqual([ + 'class' => 'my\name\space\Class', + 'name' => 'methodName', + 'instance' => null, + 'static' => false, + 'method' => null + ]); + + }); + + it("logs a static call", function () { + + Calls::log('my\name\space\Class', [ + 'name' => '::methodName' + ]); + + $logs = Calls::logs(); + + expect($logs[0][0])->toEqual([ + 'class' => 'my\name\space\Class', + 'name' => 'methodName', + 'instance' => null, + 'static' => true, + 'method' => null + ]); + + }); + + }); + + describe("::lastFindIndex()", function () { + + it("gets/sets the last find index", function () { + + $index = Calls::lastFindIndex(100); + expect($index)->toBe(100); + + $index = Calls::lastFindIndex(); + expect($index)->toBe(100); + + }); + + }); + +}); diff --git a/spec/Suite/Plugin/Call/Message.spec.php b/spec/Suite/Plugin/Call/Message.spec.php new file mode 100644 index 00000000..031c9cde --- /dev/null +++ b/spec/Suite/Plugin/Call/Message.spec.php @@ -0,0 +1,78 @@ +parent()", function () { + + it("Gets the message parent", function () { + + $message = new Message([ + 'parent' => 'parent', + ]); + expect($message->parent())->toBe('parent'); + + }); + + }); + + describe("->reference()", function () { + + it("Gets the message reference", function () { + + $message = new Message([ + 'reference' => 'reference', + ]); + expect($message->reference())->toBe('reference'); + + }); + + }); + + describe("->name()", function () { + + it("Gets the message name", function () { + + $message = new Message([ + 'name' => 'message_name', + ]); + expect($message->name())->toBe('message_name'); + + }); + + }); + + describe("->args()", function () { + + it('Gets the message args', function () { + + $message = new Message([ + 'args' => ['a', 'b', 'c'], + ]); + expect($message->args())->toBe(['a', 'b', 'c']); + + }); + + }); + + describe("->isStatic()", function () { + + it('Checks if the message is static', function () { + + $message = new Message([ + 'static' => true + ]); + expect($message->isStatic())->toBe(true); + + $message = new Message([ + 'static' => false + ]); + expect($message->isStatic())->toBe(false); + + }); + + }); + +}); diff --git a/spec/Suite/Plugin/Call/MessageSpec.php b/spec/Suite/Plugin/Call/MessageSpec.php deleted file mode 100644 index 3dea8d9a..00000000 --- a/spec/Suite/Plugin/Call/MessageSpec.php +++ /dev/null @@ -1,52 +0,0 @@ -name()", function() { - - it("Gets the message name", function() { - - $message = new Message([ - 'name' => 'message_name', - ]); - expect($message->name())->toBe('message_name'); - - }); - - }); - - describe("->params()", function() { - - it('Gets the message params', function() { - - $message = new Message([ - 'params' => ['a', 'b', 'c'], - ]); - expect($message->params())->toBe(['a', 'b', 'c']); - - }); - - }); - - describe("->isStatic()", function() { - - it('Checks if the message is static', function() { - - $message = new Message([ - 'static' => true - ]); - expect($message->isStatic())->toBe(true); - - $message = new Message([ - 'static' => false - ]); - expect($message->isStatic())->toBe(false); - - }); - - }); - -}); diff --git a/spec/Suite/Plugin/CallSpec.php b/spec/Suite/Plugin/CallSpec.php deleted file mode 100644 index b5e035cd..00000000 --- a/spec/Suite/Plugin/CallSpec.php +++ /dev/null @@ -1,64 +0,0 @@ - 'methodName' - ]); - - $logs = Call::logs(); - - expect($logs[0][0])->toEqual([ - 'class' => 'my\name\space\Class', - 'name' => 'methodName', - 'instance' => null, - 'static' => false - ]); - - }); - - it("logs a static call", function() { - - Call::log('my\name\space\Class', [ - 'name' => '::methodName' - ]); - - $logs = Call::logs(); - - expect($logs[0][0])->toEqual([ - 'class' => 'my\name\space\Class', - 'name' => 'methodName', - 'instance' => null, - 'static' => true - ]); - - }); - - }); - - describe("::lastFindIndex()", function() { - - it("gets/sets the last find index", function() { - - $index = Call::lastFindIndex(100); - expect($index)->toBe(100); - - $index = Call::lastFindIndex(); - expect($index)->toBe(100); - - }); - - }); - -}); diff --git a/spec/Suite/Plugin/Double.spec.php b/spec/Suite/Plugin/Double.spec.php new file mode 100644 index 00000000..ab945984 --- /dev/null +++ b/spec/Suite/Plugin/Double.spec.php @@ -0,0 +1,696 @@ +previous = Interceptor::instance(); + Interceptor::unpatch(); + + $cachePath = rtrim(sys_get_temp_dir(), DS) . DS . 'kahlan'; + $include = ['Kahlan\Spec\\']; + $interceptor = Interceptor::patch(compact('include', 'cachePath')); + $interceptor->patchers()->add('pointcut', new PointcutPatcher()); + $interceptor->patchers()->add('monkey', new MonkeyPatcher()); + }); + + /** + * Restore Interceptor class. + */ + afterAll(function () { + Interceptor::load($this->previous); + }); + + describe("::_generateAbstractMethods()", function () { + + it("throws an exception when called with a non-existing class", function () { + + expect(function () { + $double = Double::classname([ + 'extends' => 'Kahlan\Plugin\Double', + 'methods' => ['::generateAbstractMethods'] + ]); + allow($double)->toReceive('::generateAbstractMethods')->andRun(function ($class) { + return static::_generateAbstractMethods($class); + }); + $double::generateAbstractMethods('some\unexisting\Class'); + })->toThrow(); + + }); + + }); + + describe("::create()", function () { + + beforeAll(function () { + $this->is_method_exists = function ($instance, $method, $type = "public") { + if (!method_exists($instance, $method)) { + return false; + } + $refl = new ReflectionMethod($instance, $method); + switch ($type) { + case "static": + return $refl->isStatic(); + break; + case "public": + return $refl->isPublic(); + break; + case "private": + return $refl->isPrivate(); + break; + } + return false; + }; + }); + + it("stubs an instance", function () { + + $double = Double::instance(); + expect(is_object($double))->toBe(true); + expect(get_class($double))->toMatch("/^Kahlan\\\Spec\\\Plugin\\\Double\\\Double\d+$/"); + + }); + + it("names a stub instance", function () { + + $double = Double::instance(['class' => 'Kahlan\Spec\Double\MyDouble']); + expect(is_object($double))->toBe(true); + expect(get_class($double))->toBe('Kahlan\Spec\Double\MyDouble'); + + }); + + it("stubs an instance with a parent class", function () { + + $double = Double::instance(['extends' => 'Kahlan\Util\Text']); + expect(is_object($double))->toBe(true); + expect(get_parent_class($double))->toBe('Kahlan\Util\Text'); + + }); + + it("stubs an instance using a trait", function () { + + $double = Double::instance(['uses' => 'Kahlan\Spec\Mock\Plugin\Double\HelloTrait']); + expect($double->hello())->toBe('Hello World From Trait!'); + + }); + + it("stubs an instance implementing some interface", function () { + + $double = Double::instance(['implements' => ['ArrayAccess', 'Iterator']]); + $interfaces = class_implements($double); + expect(isset($interfaces['ArrayAccess']))->toBe(true); + expect(isset($interfaces['Iterator']))->toBe(true); + expect(isset($interfaces['Traversable']))->toBe(true); + + }); + + it("stubs an instance with multiple stubbed methods", function () { + + $double = Double::instance(); + allow($double)->toReceive('message')->andReturn('Good Evening World!', 'Good Bye World!'); + allow($double)->toReceive('bar')->andReturn('Hello Bar!'); + + expect($double->message())->toBe('Good Evening World!'); + expect($double->message())->toBe('Good Bye World!'); + expect($double->bar())->toBe('Hello Bar!'); + + }); + + it("stubs static methods on a stub instance", function () { + + $double = Double::instance(); + allow($double)->toReceive('::magicCallStatic')->andReturn('Good Evening World!', 'Good Bye World!'); + + expect($double::magicCallStatic())->toBe('Good Evening World!'); + expect($double::magicCallStatic())->toBe('Good Bye World!'); + + }); + + it("produces unique instance", function () { + + $double = Double::instance(); + $double2 = Double::instance(); + + expect(get_class($double))->not->toBe(get_class($double2)); + + }); + + it("stubs instances with some magic methods if no parent defined", function () { + + $double = Double::instance(); + + expect($double)->toReceive('__get')->ordered; + expect($double)->toReceive('__set')->ordered; + expect($double)->toReceive('__isset')->ordered; + expect($double)->toReceive('__unset')->ordered; + expect($double)->toReceive('__sleep')->ordered; + expect($double)->toReceive('__toString')->ordered; + expect($double)->toReceive('__invoke')->ordered; + expect(get_class($double))->toReceive('__wakeup')->ordered; + expect(get_class($double))->toReceive('__clone')->ordered; + + $prop = $double->prop; + $double->prop = $prop; + expect(isset($double->prop))->toBe(true); + expect(isset($double->data))->toBe(false); + unset($double->data); + $serialized = serialize($double); + $string = (string) $double; + $double(); + unserialize($serialized); + $double2 = clone $double; + + }); + + it("defaults stub can be used as container", function () { + + $double = Double::instance(); + $double->data = 'hello'; + expect($double->data)->toBe('hello'); + + }); + + it("stubs an instance with an extra method", function () { + + $double = Double::instance([ + 'methods' => ['method1'] + ]); + + expect($this->is_method_exists($double, 'method1'))->toBe(true); + expect($this->is_method_exists($double, 'method2'))->toBe(false); + expect($this->is_method_exists($double, 'method1', 'static'))->toBe(false); + + }); + + it("stubs an instance with an extra static method", function () { + + $double = Double::instance([ + 'methods' => ['::method1'] + ]); + + expect($this->is_method_exists($double, 'method1'))->toBe(true); + expect($this->is_method_exists($double, 'method2'))->toBe(false); + expect($this->is_method_exists($double, 'method1', 'static'))->toBe(true); + + }); + + it("stubs an instance with an extra method returning by reference", function () { + + $double = Double::instance([ + 'methods' => ['&method1'] + ]); + + $double->method1(); + expect(method_exists($double, 'method1'))->toBe(true); + + $array = []; + allow($double)->toReceive('method1')->andRun(function () use (&$array) { + $array[] = 'in'; + }); + + $result = $double->method1(); + $result[] = 'out'; + expect($array)->toBe(['in'/*, 'out'*/]); //I guess that's the limit of the system. + + }); + + it("applies constructor parameters to the stub", function () { + + $double = Double::instance([ + 'extends' => 'Kahlan\Spec\Fixture\Plugin\Double\ConstrDoz', + 'args' => ['a', 'b'] + ]); + + expect($double->a)->toBe('a'); + expect($double->b)->toBe('b'); + + }); + + it("expects method called in the past to be uncalled", function () { + + $double = Double::instance(); + $double->message(); + expect($double)->not->toReceive('message'); + + }); + + }); + + describe("::classname()", function () { + + it("stubs class", function () { + + $double = Double::classname(); + expect($double)->toMatch("/^Kahlan\\\Spec\\\Plugin\\\Double\\\Double\d+$/"); + + }); + + it("names a stub class", function () { + + $double = Double::classname(['class' => 'Kahlan\Spec\Double\MyStaticDouble']); + expect(is_string($double))->toBe(true); + expect($double)->toBe('Kahlan\Spec\Double\MyStaticDouble'); + + }); + + it("stubs a stub class with multiple methods", function () { + + $classname = Double::classname(); + allow($classname)->toReceive('message')->andReturn('Good Evening World!', 'Good Bye World!'); + allow($classname)->toReceive('bar')->andReturn('Hello Bar!'); + + $double = new $classname(); + expect($double->message())->toBe('Good Evening World!'); + + $double2 = new $classname(); + expect($double->message())->toBe('Good Bye World!'); + + $double3 = new $classname(); + expect($double->bar())->toBe('Hello Bar!'); + + }); + + it("stubs static methods on a stub class", function () { + + $classname = Double::classname(); + allow($classname)->toReceive('::magicCallStatic')->andReturn('Good Evening World!', 'Good Bye World!'); + + expect($classname::magicCallStatic())->toBe('Good Evening World!'); + expect($classname::magicCallStatic())->toBe('Good Bye World!'); + + }); + + it("produces unique classname", function () { + + $double = Double::classname(); + $double2 = Double::classname(); + + expect($double)->not->toBe($double2); + + }); + + it("stubs classes with `construct()` if no parent defined", function () { + + $class = Double::classname(); + expect($class)->toReceive('__construct'); + $double = new $class(); + + }); + + it("expects method called in the past to be uncalled", function () { + + $class = Double::classname(); + $class::message(); + expect($class)->not->toReceive('::message'); + + }); + + }); + + describe("::generate()", function () { + + it("throws an exception with an unexisting trait", function () { + + expect(function () { + Double::generate(['uses' => ['an\unexisting\Trait']]); + })->toThrow(); + + }); + + it("throws an exception with an unexisting interface", function () { + + expect(function () { + Double::generate(['implements' => ['an\unexisting\Interface']]); + })->toThrow(); + + }); + + it("throws an exception with an unexisting parent class", function () { + + expect(function () { + Double::generate(['extends' => 'an\unexisting\ParentClass']); + })->toThrow(); + + }); + + it("overrides the construct method", function () { + + $result = Double::generate([ + 'class' => 'Kahlan\Spec\Plugin\Double\Double', + 'methods' => ['__construct'], + 'magicMethods' => false + ]); + + $expected = << +EOD; + expect($result)->toBe($expected); + + }); + + it("generates use statement", function () { + + $result = Double::generate([ + 'class' => 'Kahlan\Spec\Plugin\Double\Double', + 'uses' => ['Kahlan\Spec\Mock\Plugin\Double\HelloTrait'], + 'magicMethods' => false + ]); + + $expected = << +EOD; + expect($result)->toBe($expected); + + }); + + it("generates abstract parent class methods", function () { + + $result = Double::generate([ + 'class' => 'Kahlan\Spec\Plugin\Double\Double', + 'extends' => 'Kahlan\Spec\Fixture\Plugin\Double\AbstractDoz' + ]); + + $expected = << +EOD; + expect($result)->toBe($expected); + + }); + + it("generates interface methods", function () { + + $result = Double::generate([ + 'class' => 'Kahlan\Spec\Plugin\Double\Double', + 'implements' => 'Countable', + 'magicMethods' => false + ]); + + $expected = << +EOD; + expect($result)->toBe($expected); + + }); + + it("generates interface methods for multiple insterfaces", function () { + + $result = Double::generate([ + 'class' => 'Kahlan\Spec\Plugin\Double\Double', + 'implements' => ['Countable', 'SplObserver'], + 'magicMethods' => false + ]); + + $expected = << +EOD; + expect(str_replace('$subject', '$SplSubject', $result))->toBe($expected); + + }); + + it("generates interface methods", function () { + + $result = Double::generate([ + 'class' => 'Kahlan\Spec\Plugin\Double\Double', + 'implements' => null, + 'magicMethods' => false + ]); + + $expected = << +EOD; + expect($result)->toBe($expected); + + }); + + it("generates interface methods with return type", function () { + + skipIf(PHP_MAJOR_VERSION < 7); + + $result = Double::generate([ + 'class' => 'Kahlan\Spec\Plugin\Double\Double', + 'implements' => ['Kahlan\Spec\Fixture\Plugin\Double\ReturnTypesInterface'], + 'magicMethods' => false + ]); + + $expected = << +EOD; + expect($result)->toBe($expected); + + }); + + it("generates interface methods with variadic variable", function () { + + skipIf(defined('HHVM_VERSION') || PHP_MAJOR_VERSION < 7); + + $result = Double::generate([ + 'class' => 'Kahlan\Spec\Plugin\Double\Double', + 'implements' => ['Kahlan\Spec\Fixture\Plugin\Double\VariadicInterface'], + 'magicMethods' => false + ]); + + $expected = << +EOD; + expect($result)->toBe($expected); + + }); + + it("manages methods inheritence", function () { + + $result = Double::generate([ + 'class' => 'Kahlan\Spec\Plugin\Double\Double', + 'implements' => ['Kahlan\Spec\Fixture\Plugin\Double\DozInterface'], + 'magicMethods' => false + ]); + + $expected = << +EOD; + expect($result)->toBe($expected); + + $result = Double::generate([ + 'class' => 'Kahlan\Spec\Plugin\Double\Double', + 'extends' => 'Kahlan\Spec\Fixture\Plugin\Double\AbstractDoz', + 'implements' => ['Kahlan\Spec\Fixture\Plugin\Double\DozInterface'], + ]); + + $expected = << +EOD; + expect($result)->toBe($expected); + + $result = Double::generate([ + 'class' => 'Kahlan\Spec\Plugin\Double\Double', + 'extends' => 'Kahlan\Spec\Fixture\Plugin\Double\AbstractDoz', + 'implements' => ['Kahlan\Spec\Fixture\Plugin\Double\DozInterface'], + 'methods' => ['foo', 'bar'] + ]); + + $expected = << +EOD; + expect($result)->toBe($expected); + + }); + + it("overrides all parent class method and respect typehints using the layer option", function () { + + $result = Double::generate([ + 'class' => 'Kahlan\Spec\Plugin\Double\Double', + 'extends' => 'Kahlan\Spec\Fixture\Plugin\Double\Doz', + 'layer' => true + ]); + + $expected = << +EOD; + expect($result)->toBe($expected); + + }); + + it("overrides by default all parent class method of internal classes if the layer option is not defined", function () { + + $double = Double::instance(['extends' => 'DateTime']); + + allow($double)->toReceive('getTimestamp')->andReturn(12345678); + + expect($double->getTimestamp())->toBe(12345678); + + }); + + it("adds ` = NULL` to optional parameter in PHP core method", function () { + + skipIf(defined('HHVM_VERSION')); + + $result = Double::generate([ + 'class' => 'Kahlan\Spec\Plugin\Double\Double', + 'extends' => 'LogicException', + 'layer' => true + ]); + + $expected = <<toMatch('~' . $expected . '~i'); + + }); + + it("generates code without PHP tags", function () { + + $result = Double::generate([ + 'class' => 'Kahlan\Spec\Plugin\Double\Double', + 'magicMethods' => false, + 'openTag' => false, + 'closeTag' => false, + ]); + + $expected = <<toBe($expected); + + }); + + }); + +}); diff --git a/spec/Suite/Plugin/Monkey.spec.php b/spec/Suite/Plugin/Monkey.spec.php new file mode 100644 index 00000000..dc5a3762 --- /dev/null +++ b/spec/Suite/Plugin/Monkey.spec.php @@ -0,0 +1,123 @@ +previous = Interceptor::instance(); + Interceptor::unpatch(); + + $cachePath = rtrim(sys_get_temp_dir(), DS) . DS . 'kahlan'; + $include = ['Kahlan\Spec\\']; + $interceptor = Interceptor::patch(compact('include', 'cachePath')); + $interceptor->patchers()->add('monkey', new MonkeyPatcher()); + + }); + + /** + * Restore Interceptor class. + */ + afterAll(function () { + Interceptor::load($this->previous); + }); + + it("patches a core function", function () { + + $mon = new Mon(); + Monkey::patch('time', 'Kahlan\Spec\Suite\Plugin\mytime'); + expect($mon->time())->toBe(245026800); + + }); + + describe("::patch()", function () { + + it("patches a core function with a closure", function () { + + $mon = new Mon(); + Monkey::patch('time', function () { + return 123; + }); + expect($mon->time())->toBe(123); + + }); + + it("patches a core class", function () { + + $mon = new Mon(); + Monkey::patch('DateTime', 'Kahlan\Spec\Mock\Plugin\Monkey\MyDateTime'); + expect($mon->datetime()->getTimestamp())->toBe(245026800); + + }); + + it("patches a core class using substitutes", function () { + + $mon = new Mon(); + $patch = Monkey::patch('DateTime'); + $patch->toBe(new DateTime('@123'), new DateTime('@456')); + expect($mon->datetime()->getTimestamp())->toBe(123); + expect($mon->datetime()->getTimestamp())->toBe(456); + + }); + + it("patches a function", function () { + + $mon = new Mon(); + Monkey::patch('Kahlan\Spec\Fixture\Plugin\Monkey\rand', 'Kahlan\Spec\Suite\Plugin\myrand'); + expect($mon->rand(0, 100))->toBe(101); + + }); + + it("patches a class", function () { + + $mon = new Mon(); + Monkey::patch('Kahlan\Util\Text', 'Kahlan\Spec\Mock\Plugin\Monkey\MyString'); + expect($mon->dump((object)'hello'))->toBe('myhashvalue'); + + }); + + it("can unpatch a monkey patch", function () { + + $mon = new Mon(); + Monkey::patch('Kahlan\Spec\Fixture\Plugin\Monkey\rand', 'Kahlan\Spec\Suite\Plugin\myrand'); + expect($mon->rand(0, 100))->toBe(101); + + Monkey::reset('Kahlan\Spec\Fixture\Plugin\Monkey\rand'); + expect($mon->rand(0, 100))->toBe(50); + + }); + + it("throws an exception with trying to patch an unsupported functions or core langage statements", function () { + + $closure = function () { + Monkey::patch('func_get_args', function () { + return []; + }); + }; + + expect($closure)->toThrow(new Exception('Monkey patching `func_get_args()` is not supported by Kahlan.')); + }); + + }); + +}); diff --git a/spec/Suite/Plugin/MonkeySpec.php b/spec/Suite/Plugin/MonkeySpec.php deleted file mode 100644 index 4cc1a5b2..00000000 --- a/spec/Suite/Plugin/MonkeySpec.php +++ /dev/null @@ -1,119 +0,0 @@ -_datetime = new DateTime(); - $this->_datetime->setTimestamp(245026800); - } - - public function __call($name, $params) { - return call_user_func_array([$this->_datetime, $name], $params); - } -} - -class MyString { - - public static function dump($value) { - return 'myhashvalue'; - } - -} - -describe("Monkey", function() { - - /** - * Save current & reinitialize the Interceptor class. - */ - before(function() { - $this->previous = Interceptor::instance(); - Interceptor::unpatch(); - - $cachePath = rtrim(sys_get_temp_dir(), DS) . DS . 'kahlan'; - $include = ['Kahlan\Spec\\']; - $interceptor = Interceptor::patch(compact('include', 'cachePath')); - $interceptor->patchers()->add('monkey', new MonkeyPatcher()); - - }); - - /** - * Restore Interceptor class. - */ - after(function() { - Interceptor::load($this->previous); - }); - - it("patches a core function", function() { - - $foo = new Foo(); - Monkey::patch('time', 'Kahlan\Spec\Suite\Plugin\mytime'); - expect($foo->time())->toBe(245026800); - - }); - - describe("::patch()", function() { - - it("patches a core function with a closure", function() { - - $foo = new Foo(); - Monkey::patch('time', function(){return 123;}); - expect($foo->time())->toBe(123); - - }); - - it("patches a core class", function() { - - $foo = new Foo(); - Monkey::patch('DateTime', 'Kahlan\Spec\Suite\Plugin\MyDateTime'); - expect($foo->datetime()->getTimestamp())->toBe(245026800); - - }); - - it("patches a function", function() { - - $foo = new Foo(); - Monkey::patch('Kahlan\Spec\Fixture\Plugin\Monkey\rand', 'Kahlan\Spec\Suite\Plugin\myrand'); - expect($foo->rand(0, 100))->toBe(101); - - }); - - it("patches a class", function() { - - $foo = new Foo(); - Monkey::patch('Kahlan\Util\Text', 'Kahlan\Spec\Suite\Plugin\MyString'); - expect($foo->dump((object)'hello'))->toBe('myhashvalue'); - - }); - - it("can unpatch a monkey patch", function() { - - $foo = new Foo(); - Monkey::patch('Kahlan\Spec\Fixture\Plugin\Monkey\rand', 'Kahlan\Spec\Suite\Plugin\myrand'); - expect($foo->rand(0, 100))->toBe(101); - - Monkey::reset('Kahlan\Spec\Fixture\Plugin\Monkey\rand'); - expect($foo->rand(0, 100))->toBe(50); - - }); - - }); - -}); diff --git a/spec/Suite/Plugin/QuitSpec.php b/spec/Suite/Plugin/Quit.spec.php similarity index 78% rename from spec/Suite/Plugin/QuitSpec.php rename to spec/Suite/Plugin/Quit.spec.php index f0b72c8f..3122b2c2 100644 --- a/spec/Suite/Plugin/QuitSpec.php +++ b/spec/Suite/Plugin/Quit.spec.php @@ -8,12 +8,12 @@ use Kahlan\Spec\Fixture\Plugin\Quit\Foo; -describe("Quit", function() { +describe("Quit", function () { /** * Save current & reinitialize the Interceptor class. */ - before(function() { + beforeAll(function () { $this->previous = Interceptor::instance(); Interceptor::unpatch(); @@ -26,13 +26,13 @@ /** * Restore Interceptor class. */ - after(function() { + afterAll(function () { Interceptor::load($this->previous); }); - describe("::enable()", function() { + describe("::enable()", function () { - it("enables quit statements", function() { + it("enables quit statements", function () { Quit::disable(); expect(Quit::enabled())->toBe(false); @@ -44,9 +44,9 @@ }); - describe("::disable()", function() { + describe("::disable()", function () { - it("disables quit statements", function() { + it("disables quit statements", function () { Quit::enable(); expect(Quit::enabled())->toBe(true); @@ -58,13 +58,13 @@ }); - describe("::disable()", function() { + describe("::disable()", function () { - it("throws an exception when an exit statement occurs if not allowed", function() { + it("throws an exception when an exit statement occurs if not allowed", function () { Quit::disable(); - $closure = function() { + $closure = function () { $foo = new Foo(); $foo->exitStatement(-1); }; diff --git a/spec/Suite/Plugin/Stub.spec.php b/spec/Suite/Plugin/Stub.spec.php new file mode 100644 index 00000000..7cd8eeb1 --- /dev/null +++ b/spec/Suite/Plugin/Stub.spec.php @@ -0,0 +1,232 @@ +previous = Interceptor::instance(); + Interceptor::unpatch(); + + $cachePath = rtrim(sys_get_temp_dir(), DS) . DS . 'kahlan'; + $include = ['Kahlan\Spec\\']; + $interceptor = Interceptor::patch(compact('include', 'cachePath')); + $interceptor->patchers()->add('pointcut', new PointcutPatcher()); + $interceptor->patchers()->add('monkey', new MonkeyPatcher()); + }); + + /** + * Restore Interceptor class. + */ + afterAll(function () { + Interceptor::load($this->previous); + }); + + describe("__construct()", function () { + + it("throws an exception when trying to stub an unexisting class", function () { + + $closure = function () { + new Stub('My\Unexisting\Classname\Foo'); + }; + $message = "Can't Stub the unexisting class `My\\Unexisting\\Classname\\Foo`."; + expect($closure)->toThrow(new InvalidArgumentException($message)); + + }); + + }); + + describe("->methods()", function () { + + context("with an instance", function () { + + it("stubs methods using an array", function () { + + $foo = new Foo(); + Stub::on($foo)->methods([ + 'message' => function () { + return 'Good Evening World!'; + }, + 'bar' => function () { + return 'Hello Bar!'; + } + ]); + expect($foo->message())->toBe('Good Evening World!'); + expect($foo->bar())->toBe('Hello Bar!'); + + }); + + it("throw an exception with invalid definition", function () { + + $closure = function () { + $foo = new Foo(); + Stub::on($foo)->methods([ + 'bar' => 'Hello Bar!' + ]); + }; + $message = "Stubbed method definition for `bar` must be a closure or an array of returned value(s)."; + expect($closure)->toThrow(new InvalidArgumentException($message)); + + }); + + }); + + context("with an class", function () { + + it("stubs methods using return values as an array", function () { + + Stub::on('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->methods([ + 'message' => ['Good Evening World!', 'Good Bye World!'], + 'bar' => ['Hello Bar!'] + ]); + + $foo = new Foo(); + expect($foo->message())->toBe('Good Evening World!'); + + $foo2 = new Foo(); + expect($foo2->message())->toBe('Good Bye World!'); + + $foo3 = new Foo(); + expect($foo3->bar())->toBe('Hello Bar!'); + + }); + + it("stubs methods using closure", function () { + + Stub::on('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->methods([ + 'message' => function () { + return 'Good Evening World!'; + }, + 'bar' => function () { + return 'Hello Bar!'; + } + ]); + + $foo = new Foo(); + expect($foo->message())->toBe('Good Evening World!'); + + $foo2 = new Foo(); + expect($foo2->bar())->toBe('Hello Bar!'); + + }); + + it("throw an exception with invalid definition", function () { + + $closure = function () { + $foo = new Foo(); + Stub::on('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->methods([ + 'bar' => 'Hello Bar!' + ]); + }; + $message = "Stubbed method definition for `bar` must be a closure or an array of returned value(s)."; + expect($closure)->toThrow(new InvalidArgumentException($message)); + + }); + + }); + + }); + + describe("::registered()", function () { + + describe("without provided hash", function () { + + it("returns an empty array when no instance are registered", function () { + + expect(Stub::registered())->toBe([]); + + }); + + it("returns an array of registered instances", function () { + + Stub::on('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->method('foo', function () {}); + + expect(Stub::registered())->toBeA('array')->toBe([ + 'Kahlan\Spec\Fixture\Plugin\Pointcut\Foo' + ]); + + }); + + }); + + describe("with provided hash", function () { + + it("returns `false` for registered stub", function () { + + expect(Stub::registered('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo'))->toBe(false); + + }); + + it("returns `true` for registered stub", function () { + + Stub::on('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->method('foo', function () {}); + + expect(Stub::registered('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo'))->toBe(true); + + }); + + }); + + }); + + describe("::on()", function () { + + it("throw when stub a method using closure and using andReturn()", function () { + + expect(function () { + $foo = new Foo(); + Stub::on($foo)->method('message', function ($param) { + return $param; + })->andReturn(true); + })->toThrow(new Exception("Some closure(s) has already been set.")); + + }); + + }); + + describe("::reset()", function () { + + beforeEach(function () { + + Stub::on('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->method('foo', function () {}); + Stub::on('Kahlan\Spec\Fixture\Plugin\Pointcut\Bar')->method('bar', function () {}); + + }); + + it("clears all stubs", function () { + + Stub::reset(); + expect(Stub::registered())->toBe([]); + + }); + + it("clears one stub", function () { + + Stub::reset('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo'); + expect(Stub::registered())->toBe([ + 'Kahlan\Spec\Fixture\Plugin\Pointcut\Bar' + ]); + + }); + + }); + +}); diff --git a/spec/Suite/Plugin/Stub/Method.spec.php b/spec/Suite/Plugin/Stub/Method.spec.php new file mode 100644 index 00000000..b1382e1c --- /dev/null +++ b/spec/Suite/Plugin/Stub/Method.spec.php @@ -0,0 +1,141 @@ +previous = Interceptor::instance(); + Interceptor::unpatch(); + + $cachePath = rtrim(sys_get_temp_dir(), DS) . DS . 'kahlan'; + $include = ['Kahlan\Spec\\']; + $interceptor = Interceptor::patch(compact('include', 'cachePath')); + $interceptor->patchers()->add('pointcut', new PointcutPatcher()); + $interceptor->patchers()->add('monkey', new MonkeyPatcher()); + }); + + /** + * Restore Interceptor class. + */ + afterAll(function () { + Interceptor::load($this->previous); + }); + + describe("->andReturn()", function () { + + it("sets a return value", function () { + + $foo = new Foo(); + $stub = allow($foo)->toReceive('message'); + $stub->andReturn('Aloha!'); + + expect($foo->message())->toBe('Aloha!'); + expect($stub->actualReturn())->toBe('Aloha!'); + + }); + + it("sets return values", function () { + + $foo = new Foo(); + $stub = allow($foo)->toReceive('message'); + $stub->andReturn('Aloha!', 'Hello!'); + + expect($foo->message())->toBe('Aloha!'); + expect($stub->actualReturn())->toBe('Aloha!'); + + expect($foo->message())->toBe('Hello!'); + expect($foo->message())->toBe('Hello!'); + expect($stub->actualReturn())->toBe('Hello!'); + + }); + + it("throws when return is already set", function () { + + expect(function () { + $foo = new Foo(); + $stub = allow($foo)->toReceive('message'); + $stub->andRun(function ($param) { + return $param; + }); + + $stub->andReturn('Ahoy!'); + + })->toThrow(new Exception('Some closure(s) has already been set.')); + + }); + + }); + + describe("->andRun()", function () { + + it("sets a closure", function () { + + $foo = new Foo(); + $stub = allow($foo)->toReceive('message'); + $stub->andRun(function ($param) { + return $param; + }); + + expect($foo->message('Aloha!'))->toBe('Aloha!'); + expect($stub->actualReturn())->toBe('Aloha!'); + + }); + + it("sets closures", function () { + + $foo = new Foo(); + $stub = allow($foo)->toReceive('message'); + $stub->andRun(function () { + return 'Aloha!'; + }, function () { + return 'Hello!'; + }); + + expect($foo->message())->toBe('Aloha!'); + expect($stub->actualReturn())->toBe('Aloha!'); + + expect($foo->message())->toBe('Hello!'); + expect($foo->message())->toBe('Hello!'); + expect($stub->actualReturn())->toBe('Hello!'); + + }); + + it("throws when return is already set", function () { + + expect(function () { + $foo = new Foo(); + $stub = allow($foo)->toReceive('message'); + $stub->andReturn('Ahoy!'); + + $stub->andRun(function ($param) { + return $param; + }); + })->toThrow(new Exception('Some return value(s) has already been set.')); + + }); + + it("throws when trying to pass non callable", function () { + + expect(function () { + $foo = new Foo(); + $stub = allow($foo)->toReceive('message'); + + $stub->andRun('String'); + })->toThrow(new Exception('The passed parameter is not callable.')); + + }); + + }); + +}); diff --git a/spec/Suite/Plugin/Stub/MethodSpec.php b/spec/Suite/Plugin/Stub/MethodSpec.php deleted file mode 100644 index 2e3e1a27..00000000 --- a/spec/Suite/Plugin/Stub/MethodSpec.php +++ /dev/null @@ -1,74 +0,0 @@ -previous = Interceptor::instance(); - Interceptor::unpatch(); - - $cachePath = rtrim(sys_get_temp_dir(), DS) . DS . 'kahlan'; - $include = ['Kahlan\Spec\\']; - $interceptor = Interceptor::patch(compact('include', 'cachePath')); - $interceptor->patchers()->add('pointcut', new Pointcut()); - }); - - /** - * Restore Interceptor class. - */ - after(function() { - Interceptor::load($this->previous); - }); - - describe("->run()", function() { - - it("should set closure", function() { - - $foo = new Foo(); - $stub = Stub::on($foo)->method('message'); - $stub->run(function($param) { - return $param; - }); - - expect($foo->message('Aloha!'))->toBe('Aloha!'); - - }); - - it("should throw when return is already set", function() { - - expect(function() { - $foo = new Foo(); - $stub = Stub::on($foo)->method('message'); - $stub->andReturn('Ahoy!'); - - $stub->run(function($param) { - return $param; - }); - })->toThrow(new Exception('Some return values are already set.')); - - }); - - it("should throw when trying to pass non callable", function() { - - expect(function() { - $foo = new Foo(); - $stub = Stub::on($foo)->method('message'); - - $stub->run('String'); - })->toThrow(new Exception('The passed parameter is not callable.')); - - }); - - }); - -}); \ No newline at end of file diff --git a/spec/Suite/Plugin/StubSpec.php b/spec/Suite/Plugin/StubSpec.php deleted file mode 100644 index 2f128807..00000000 --- a/spec/Suite/Plugin/StubSpec.php +++ /dev/null @@ -1,1093 +0,0 @@ -previous = Interceptor::instance(); - Interceptor::unpatch(); - - $cachePath = rtrim(sys_get_temp_dir(), DS) . DS . 'kahlan'; - $include = ['Kahlan\Spec\\']; - $interceptor = Interceptor::patch(compact('include', 'cachePath')); - $interceptor->patchers()->add('pointcut', new Pointcut()); - }); - - /** - * Restore Interceptor class. - */ - after(function() { - Interceptor::load($this->previous); - }); - - describe("::on()", function() { - - context("with an instance", function() { - - it("stubs a method", function() { - - $foo = new Foo(); - Stub::on($foo)->method('message')->andReturn('Good Bye!'); - expect($foo->message())->toBe('Good Bye!'); - - }); - - it("stubs only on the stubbed instance", function() { - - $foo = new Foo(); - Stub::on($foo)->method('message')->andReturn('Good Bye!'); - expect($foo->message())->toBe('Good Bye!'); - - $foo2 = new Foo(); - expect($foo2->message())->toBe('Hello World!'); - - }); - - it("stubs a method using a closure", function() { - - $foo = new Foo(); - Stub::on($foo)->method('message', function($param) { return $param; }); - expect($foo->message('Good Bye!'))->toBe('Good Bye!'); - - }); - - it("throw when stub a method using closure and using andReturn()", function() { - - expect(function() { - $foo = new Foo(); - Stub::on($foo)->method('message', function($param) { return $param; })->andReturn(true); - })->toThrow(new \Exception("Closure already set.")); - - }); - - it("stubs a magic method", function() { - - $foo = new Foo(); - Stub::on($foo)->method('magicCall')->andReturn('Magic Call!'); - expect($foo->magicCall())->toBe('Magic Call!'); - - }); - - it("stubs a magic method using a closure", function() { - - $foo = new Foo(); - Stub::on($foo)->method('magicHello', function($message) { return $message; }); - expect($foo->magicHello('Hello World!'))->toBe('Hello World!'); - - }); - - it("stubs a static magic method", function() { - - $foo = new Foo(); - Stub::on($foo)->method('::magicCallStatic')->andReturn('Magic Call Static!'); - expect($foo::magicCallStatic())->toBe('Magic Call Static!'); - - }); - - it("stubs a static magic method using a closure", function() { - - $foo = new Foo(); - Stub::on($foo)->method('::magicHello', function($message) { return $message; }); - expect($foo::magicHello('Hello World!'))->toBe('Hello World!'); - - }); - - it("overrides previously applied stubs", function() { - - $foo = new Foo(); - Stub::on($foo)->method('magicHello')->andReturn('Hello World!'); - Stub::on($foo)->method('magicHello')->andReturn('Good Bye!'); - expect($foo->magicHello())->toBe('Good Bye!'); - - }); - - context("with several applied stubs on a same method", function() { - - it("stubs a magic method multiple times", function() { - - $foo = new Foo(); - Stub::on($foo)->method('magic')->with('hello')->andReturn('world'); - Stub::on($foo)->method('magic')->with('world')->andReturn('hello'); - expect($foo->magic('hello'))->toBe('world'); - expect($foo->magic('world'))->toBe('hello'); - - }); - - it("stubs a static magic method multiple times", function() { - - $foo = new Foo(); - Stub::on($foo)->method('::magic')->with('hello')->andReturn('world'); - Stub::on($foo)->method('::magic')->with('world')->andReturn('hello'); - expect($foo::magic('hello'))->toBe('world'); - expect($foo::magic('world'))->toBe('hello'); - - }); - - }); - - context("using the with() parameter", function() { - - it("stubs on matched parameter", function() { - - $foo = new Foo(); - Stub::on($foo)->method('message')->with('Hello World!')->andReturn('Good Bye!'); - expect($foo->message('Hello World!'))->toBe('Good Bye!'); - - }); - - it("doesn't stubs on unmatched parameter", function() { - - $foo = new Foo(); - Stub::on($foo)->method('message')->with('Hello World!')->andReturn('Good Bye!'); - expect($foo->message('Hello!'))->not->toBe('Good Bye!'); - - - }); - - }); - - context("using the with() parameter and the argument matchers", function() { - - it("stubs on matched parameter", function() { - - $foo = new Foo(); - Stub::on($foo)->method('message')->with(Arg::toBeA('string'))->andReturn('Good Bye!'); - expect($foo->message('Hello World!'))->toBe('Good Bye!'); - expect($foo->message('Hello'))->toBe('Good Bye!'); - - }); - - it("doesn't stubs on unmatched parameter", function() { - - $foo = new Foo(); - Stub::on($foo)->method('message')->with(Arg::toBeA('string'))->andReturn('Good Bye!'); - expect($foo->message(false))->not->toBe('Good Bye!'); - expect($foo->message(['Hello World!']))->not->toBe('Good Bye!'); - - }); - - }); - - context("with multiple return values", function() { - - it("stubs a method", function() { - - $foo = new Foo(); - Stub::on($foo)->method('message')->andReturn('Good Evening World!', 'Good Bye World!'); - expect($foo->message())->toBe('Good Evening World!'); - expect($foo->message())->toBe('Good Bye World!'); - expect($foo->message())->toBe('Good Bye World!'); - - }); - - }); - - - context("with ->methods()", function() { - - it("stubs methods using return values as an array", function() { - - $foo = new Foo(); - Stub::on($foo)->methods([ - 'message' => ['Good Evening World!', 'Good Bye World!'], - 'bar' => ['Hello Bar!'] - ]); - expect($foo->message())->toBe('Good Evening World!'); - expect($foo->message())->toBe('Good Bye World!'); - expect($foo->bar())->toBe('Hello Bar!'); - - }); - - it("stubs methods using closure", function() { - - $foo = new Foo(); - Stub::on($foo)->methods([ - 'message' => function() { - return 'Good Evening World!'; - }, - 'bar' => function() { - return 'Hello Bar!'; - } - ]); - expect($foo->message())->toBe('Good Evening World!'); - expect($foo->bar())->toBe('Hello Bar!'); - - }); - - it("throw an exception with invalid definition", function() { - - $closure = function() { - $foo = new Foo(); - Stub::on($foo)->methods([ - 'bar' => 'Hello Bar!' - ]); - }; - $message = "Stubbed method definition for `bar` must be a closure or an array of returned value(s)."; - expect($closure)->toThrow(new InvalidArgumentException($message)); - - }); - - }); - - }); - - context("with an class", function() { - - it("stubs a method", function() { - - Stub::on('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo') - ->method('message') - ->andReturn('Good Bye!'); - - $foo = new Foo(); - expect($foo->message())->toBe('Good Bye!'); - $foo2 = new Foo(); - expect($foo2->message())->toBe('Good Bye!'); - - }); - - it("stubs a static method", function() { - - Stub::on('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->method('::messageStatic')->andReturn('Good Bye!'); - expect(Foo::messageStatic())->toBe('Good Bye!'); - - }); - - it("stubs a method using a closure", function() { - - Stub::on('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->method('message', function($param) { return $param; }); - $foo = new Foo(); - expect($foo->message('Good Bye!'))->toBe('Good Bye!'); - - }); - - it("stubs a static method using a closure", function() { - - Stub::on('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->method('::messageStatic', function($param) { return $param; }); - expect(Foo::messageStatic('Good Bye!'))->toBe('Good Bye!'); - - }); - - it("stubs a magic method multiple times", function() { - - Stub::on('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->method('::magic')->with('hello')->andReturn('world'); - Stub::on('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->method('::magic')->with('world')->andReturn('hello'); - expect(Foo::magic('hello'))->toBe('world'); - expect(Foo::magic('world'))->toBe('hello'); - - }); - - context("with multiple return values", function(){ - - it("stubs a method", function() { - - Stub::on('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo') - ->method('message') - ->andReturn('Good Evening World!', 'Good Bye World!'); - - $foo = new Foo(); - expect($foo->message())->toBe('Good Evening World!'); - - $foo2 = new Foo(); - expect($foo2->message())->toBe('Good Bye World!'); - - }); - - }); - - context("with ->methods()", function() { - - it("stubs methods using return values as an array", function() { - - Stub::on('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->methods([ - 'message' => ['Good Evening World!', 'Good Bye World!'], - 'bar' => ['Hello Bar!'] - ]); - - $foo = new Foo(); - expect($foo->message())->toBe('Good Evening World!'); - - $foo2 = new Foo(); - expect($foo2->message())->toBe('Good Bye World!'); - - $foo3 = new Foo(); - expect($foo3->bar())->toBe('Hello Bar!'); - - }); - - it("stubs methods using closure", function() { - - Stub::on('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->methods([ - 'message' => function() { - return 'Good Evening World!'; - }, - 'bar' => function() { - return 'Hello Bar!'; - } - ]); - - $foo = new Foo(); - expect($foo->message())->toBe('Good Evening World!'); - - $foo2 = new Foo(); - expect($foo2->bar())->toBe('Hello Bar!'); - - }); - - it("throw an exception with invalid definition", function() { - - $closure = function() { - $foo = new Foo(); - Stub::on('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->methods([ - 'bar' => 'Hello Bar!' - ]); - }; - $message = "Stubbed method definition for `bar` must be a closure or an array of returned value(s)."; - expect($closure)->toThrow(new InvalidArgumentException($message)); - - }); - - }); - - }); - - context("with a trait", function() { - - it("stubs a method", function() { - - Stub::on('Kahlan\Spec\Fixture\Plugin\Pointcut\SubBar') - ->method('traitMethod') - ->andReturn('trait method stubbed !'); - - $subBar = new SubBar(); - expect($subBar->traitMethod())->toBe('trait method stubbed !'); - $subBar2 = new SubBar(); - expect($subBar2->traitMethod())->toBe('trait method stubbed !'); - - }); - - }); - - }); - - describe("::registered()", function() { - - describe("without provided hash", function() { - - it("returns an empty array when no instance are registered", function() { - - expect(Stub::registered())->toBe([]); - - }); - - it("returns an array of registered instances", function() { - - Stub::on('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->method('foo', function() {}); - - expect(Stub::registered())->toBeA('array')->toBe([ - 'Kahlan\Spec\Fixture\Plugin\Pointcut\Foo' - ]); - - }); - - }); - - describe("with provided hash", function() { - - it("returns `false` for registered stub", function() { - - expect(Stub::registered('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo'))->toBe(false); - - }); - - it("returns `true` for registered stub", function() { - - Stub::on('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->method('foo', function() {}); - - expect(Stub::registered('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo'))->toBe(true); - - }); - - }); - - }); - - describe("::reset()", function() { - - beforeEach(function() { - - Stub::on('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo')->method('foo', function() {}); - Stub::on('Kahlan\Spec\Fixture\Plugin\Pointcut\Bar')->method('bar', function() {}); - - }); - - it("clears all stubs", function() { - - Stub::reset(); - expect(Stub::registered())->toBe([]); - - }); - - it("clears one stub", function() { - - Stub::reset('Kahlan\Spec\Fixture\Plugin\Pointcut\Foo'); - expect(Stub::registered())->toBe([ - 'Kahlan\Spec\Fixture\Plugin\Pointcut\Bar' - ]); - - }); - - }); - - describe("::_generateAbstractMethods()", function() { - - it("throws an exception when called with a non-existing class", function() { - - expect(function() { - $stub = Stub::classname([ - 'extends' => 'Kahlan\Plugin\Stub', - 'methods' => ['::generateAbstractMethods'] - ]); - Stub::on($stub)->method('::generateAbstractMethods', function($class) { - return static::_generateAbstractMethods($class); - }); - $stub::generateAbstractMethods('some\unexisting\Class'); - })->toThrow(new IncompleteException('Unexisting parent class `some\unexisting\Class`')); - - }); - - }); - - describe("::create()", function() { - - before(function() { - $this->is_method_exists = function($instance, $method, $type = "public") { - if (!method_exists($instance, $method)) { - return false; - } - $refl = new ReflectionMethod($instance, $method); - switch($type) { - case "static": - return $refl->isStatic(); - break; - case "public": - return $refl->isPublic(); - break; - case "private": - return $refl->isPrivate(); - break; - } - return false; - }; - }); - - it("stubs an instance", function() { - - $stub = Stub::create(); - expect(is_object($stub))->toBe(true); - expect(get_class($stub))->toMatch("/^Kahlan\\\Spec\\\Plugin\\\Stub\\\Stub\d+$/"); - - }); - - it("names a stub instance", function() { - - $stub = Stub::create(['class' => 'Kahlan\Spec\Stub\MyStub']); - expect(is_object($stub))->toBe(true); - expect(get_class($stub))->toBe('Kahlan\Spec\Stub\MyStub'); - - }); - - it("stubs an instance with a parent class", function() { - - $stub = Stub::create(['extends' => 'Kahlan\Util\Text']); - expect(is_object($stub))->toBe(true); - expect(get_parent_class($stub))->toBe('Kahlan\Util\Text'); - - }); - - it("stubs an instance using a trait", function() { - - $stub = Stub::create(['uses' => 'Kahlan\Spec\Mock\Plugin\Stub\HelloTrait']); - expect($stub->hello())->toBe('Hello World From Trait!'); - - }); - - it("stubs an instance implementing some interface", function() { - - $stub = Stub::create(['implements' => ['ArrayAccess', 'Iterator']]); - $interfaces = class_implements($stub); - expect(isset($interfaces['ArrayAccess']))->toBe(true); - expect(isset($interfaces['Iterator']))->toBe(true); - expect(isset($interfaces['Traversable']))->toBe(true); - - }); - - it("stubs an instance with multiple stubbed methods", function() { - - $stub = Stub::create(); - Stub::on($stub)->methods([ - 'message' => ['Good Evening World!', 'Good Bye World!'], - 'bar' => ['Hello Bar!'] - ]); - - expect($stub->message())->toBe('Good Evening World!'); - expect($stub->message())->toBe('Good Bye World!'); - expect($stub->bar())->toBe('Hello Bar!'); - - }); - - it("stubs static methods on a stub instance", function() { - - $stub = Stub::create(); - Stub::on($stub)->methods([ - '::magicCallStatic' => ['Good Evening World!', 'Good Bye World!'] - ]); - - expect($stub::magicCallStatic())->toBe('Good Evening World!'); - expect($stub::magicCallStatic())->toBe('Good Bye World!'); - - }); - - it("produces unique instance", function() { - - $stub = Stub::create(); - $stub2 = Stub::create(); - - expect(get_class($stub))->not->toBe(get_class($stub2)); - - }); - - it("stubs instances with some magic methods if no parent defined", function() { - - $stub = Stub::create(); - - expect($stub)->toReceive('__get'); - expect($stub)->toReceiveNext('__set'); - expect($stub)->toReceiveNext('__isset'); - expect($stub)->toReceiveNext('__unset'); - expect($stub)->toReceiveNext('__sleep'); - expect($stub)->toReceiveNext('__toString'); - expect($stub)->toReceiveNext('__invoke'); - expect(get_class($stub))->toReceive('__wakeup'); - expect(get_class($stub))->toReceiveNext('__clone'); - - $prop = $stub->prop; - $stub->prop = $prop; - expect(isset($stub->prop))->toBe(true); - expect(isset($stub->data))->toBe(false); - unset($stub->data); - $serialized = serialize($stub); - unserialize($serialized); - $string = (string) $stub; - $stub(); - $stub2 = clone $stub; - - }); - - it("defaults stub can be used as container", function() { - - $stub = Stub::create(); - $stub->data = 'hello'; - expect($stub->data)->toBe('hello'); - - }); - - it("stubs an instance with an extra method", function() { - - $stub = Stub::create([ - 'methods' => ['method1'] - ]); - - expect($this->is_method_exists($stub, 'method1'))->toBe(true); - expect($this->is_method_exists($stub, 'method2'))->toBe(false); - expect($this->is_method_exists($stub, 'method1', 'static'))->toBe(false); - - }); - - it("stubs an instance with an extra static method", function() { - - $stub = Stub::create([ - 'methods' => ['::method1'] - ]); - - expect($this->is_method_exists($stub, 'method1'))->toBe(true); - expect($this->is_method_exists($stub, 'method2'))->toBe(false); - expect($this->is_method_exists($stub, 'method1', 'static'))->toBe(true); - - }); - - it("stubs an instance with an extra method returning by reference", function() { - - $stub = Stub::create([ - 'methods' => ['&method1'] - ]); - - $stub->method1(); - expect(method_exists($stub, 'method1'))->toBe(true); - - $array = []; - Stub::on($stub)->method('method1', function() use (&$array) { - $array[] = 'in'; - }); - - $result = $stub->method1(); - $result[] = 'out'; - expect($array)->toBe(['in'/*, 'out'*/]); //I guess that's the limit of the system. - - }); - - it("applies constructor parameters to the stub", function () { - - $stub = Stub::create([ - 'extends' => 'Kahlan\Spec\Fixture\Plugin\Stub\ConstrDoz', - 'params' => ['a', 'b'] - ]); - - expect($stub->a)->toBe('a'); - expect($stub->b)->toBe('b'); - - }); - - }); - - describe("::classname()", function() { - - it("stubs class", function() { - - $stub = Stub::classname(); - expect($stub)->toMatch("/^Kahlan\\\Spec\\\Plugin\\\Stub\\\Stub\d+$/"); - - }); - - it("names a stub class", function() { - - $stub = Stub::classname(['class' => 'Kahlan\Spec\Stub\MyStaticStub']); - expect(is_string($stub))->toBe(true); - expect($stub)->toBe('Kahlan\Spec\Stub\MyStaticStub'); - - }); - - it("stubs a stub class with multiple methods", function() { - - $classname = Stub::classname(); - Stub::on($classname)->methods([ - 'message' => ['Good Evening World!', 'Good Bye World!'], - 'bar' => ['Hello Bar!'] - ]); - - $stub = new $classname(); - expect($stub->message())->toBe('Good Evening World!'); - - $stub2 = new $classname(); - expect($stub->message())->toBe('Good Bye World!'); - - $stub3 = new $classname(); - expect($stub->bar())->toBe('Hello Bar!'); - - }); - - it("stubs static methods on a stub class", function() { - - $classname = Stub::classname(); - Stub::on($classname)->methods([ - '::magicCallStatic' => ['Good Evening World!', 'Good Bye World!'] - ]); - - expect($classname::magicCallStatic())->toBe('Good Evening World!'); - expect($classname::magicCallStatic())->toBe('Good Bye World!'); - - }); - - it("produces unique classname", function() { - - $stub = Stub::classname(); - $stub2 = Stub::classname(); - - expect($stub)->not->toBe($stub2); - - }); - - it("stubs classes with `construct()` if no parent defined", function() { - - $class = Stub::classname(); - expect($class)->toReceive('__construct'); - $stub = new $class(); - - }); - - }); - - describe("::generate()", function() { - - it("throws an exception with an unexisting trait", function () { - - expect(function() { - Stub::generate(['uses' => ['an\unexisting\Trait']]); - })->toThrow(new IncompleteException('Unexisting trait `an\unexisting\Trait`')); - - }); - - it("throws an exception with an unexisting interface", function() { - - expect(function() { - Stub::generate(['implements' => ['an\unexisting\Interface']]); - })->toThrow(new IncompleteException('Unexisting interface `an\unexisting\Interface`')); - - }); - - it("throws an exception with an unexisting parent class", function() { - - expect(function() { - Stub::generate(['extends' => 'an\unexisting\ParentClass']); - })->toThrow(new IncompleteException('Unexisting parent class `an\unexisting\ParentClass`')); - - }); - - it("overrides the construct method", function() { - - $result = Stub::generate([ - 'class' => 'Kahlan\Spec\Plugin\Stub\Stub', - 'methods' => ['__construct'], - 'magicMethods' => false - ]); - - $expected = << -EOD; - expect($result)->toBe($expected); - - }); - - it("generates use statement", function() { - - $result = Stub::generate([ - 'class' => 'Kahlan\Spec\Plugin\Stub\Stub', - 'uses' => ['Kahlan\Spec\Mock\Plugin\Stub\HelloTrait'], - 'magicMethods' => false - ]); - - $expected = << -EOD; - expect($result)->toBe($expected); - - }); - - it("generates abstract parent class methods", function() { - - $result = Stub::generate([ - 'class' => 'Kahlan\Spec\Plugin\Stub\Stub', - 'extends' => 'Kahlan\Spec\Fixture\Plugin\Stub\AbstractDoz' - ]); - - $expected = << -EOD; - expect($result)->toBe($expected); - - }); - - it("generates interface methods", function() { - - $result = Stub::generate([ - 'class' => 'Kahlan\Spec\Plugin\Stub\Stub', - 'implements' => 'Countable', - 'magicMethods' => false - ]); - - $expected = << -EOD; - expect($result)->toBe($expected); - - }); - - it("generates interface methods for multiple insterfaces", function() { - - $result = Stub::generate([ - 'class' => 'Kahlan\Spec\Plugin\Stub\Stub', - 'implements' => ['Countable', 'SplObserver'], - 'magicMethods' => false - ]); - - $expected = << -EOD; - expect(str_replace('$subject', '$SplSubject', $result))->toBe($expected); - - }); - - it("generates interface methods", function() { - - $result = Stub::generate([ - 'class' => 'Kahlan\Spec\Plugin\Stub\Stub', - 'implements' => null, - 'magicMethods' => false - ]); - - $expected = << -EOD; - expect($result)->toBe($expected); - - }); - - it("generates interface methods with return type", function() { - - skipIf(PHP_MAJOR_VERSION < 7); - - $result = Stub::generate([ - 'class' => 'Kahlan\Spec\Plugin\Stub\Stub', - 'implements' => ['Kahlan\Spec\Fixture\Plugin\Stub\ReturnTypesInterface'], - 'magicMethods' => false - ]); - - $expected = << -EOD; - expect($result)->toBe($expected); - - }); - - it("generates interface methods with variadic variable", function() { - - skipIf(PHP_MAJOR_VERSION < 7); - - $result = Stub::generate([ - 'class' => 'Kahlan\Spec\Plugin\Stub\Stub', - 'implements' => ['Kahlan\Spec\Fixture\Plugin\Stub\VariadicInterface'], - 'magicMethods' => false - ]); - - $expected = << -EOD; - expect($result)->toBe($expected); - - }); - - it("manages methods inheritence", function() { - - $result = Stub::generate([ - 'class' => 'Kahlan\Spec\Plugin\Stub\Stub', - 'implements' => ['Kahlan\Spec\Fixture\Plugin\Stub\DozInterface'], - 'magicMethods' => false - ]); - - $expected = << -EOD; - expect($result)->toBe($expected); - - $result = Stub::generate([ - 'class' => 'Kahlan\Spec\Plugin\Stub\Stub', - 'extends' => 'Kahlan\Spec\Fixture\Plugin\Stub\AbstractDoz', - 'implements' => ['Kahlan\Spec\Fixture\Plugin\Stub\DozInterface'], - ]); - - $expected = << -EOD; - expect($result)->toBe($expected); - - $result = Stub::generate([ - 'class' => 'Kahlan\Spec\Plugin\Stub\Stub', - 'extends' => 'Kahlan\Spec\Fixture\Plugin\Stub\AbstractDoz', - 'implements' => ['Kahlan\Spec\Fixture\Plugin\Stub\DozInterface'], - 'methods' => ['foo', 'bar'] - ]); - - $expected = << -EOD; - expect($result)->toBe($expected); - - }); - - it("overrides all parent class method and respect typehints using the layer option", function() { - - $result = Stub::generate([ - 'class' => 'Kahlan\Spec\Plugin\Stub\Stub', - 'extends' => 'Kahlan\Spec\Fixture\Plugin\Stub\Doz', - 'layer' => true - ]); - - $expected = << -EOD; - expect($result)->toBe($expected); - - }); - - it("adds ` = NULL` to optional parameter in PHP core method", function() { - - skipIf(defined('HHVM_VERSION')); - - $result = Stub::generate([ - 'class' => 'Kahlan\Spec\Plugin\Stub\Stub', - 'extends' => 'LogicException', - 'layer' => true - ]); - - $expected = <<toMatch('~' . $expected . '~i'); - - }); - - it("generates code without PHP tags", function() { - - $result = Stub::generate([ - 'class' => 'Kahlan\Spec\Plugin\Stub\Stub', - 'magicMethods' => false, - 'openTag' => false, - 'closeTag' => false, - ]); - - $expected = <<toBe($expected); - - }); - - }); - -}); diff --git a/spec/Suite/ReportSpec.php b/spec/Suite/ReportSpec.php deleted file mode 100644 index b17708de..00000000 --- a/spec/Suite/ReportSpec.php +++ /dev/null @@ -1,70 +0,0 @@ -__construct()", function() { - - it("correctly sets default values", function() { - - $report = new Report(); - expect($report->scope())->toBe(null); - expect($report->type())->toBe('pass'); - expect($report->not())->toBe(false); - expect($report->description())->toBe(null); - expect($report->matcher())->toBe(null); - expect($report->matcherName())->toBe(null); - expect($report->params())->toBe([]); - expect($report->backtrace())->toBe([]); - expect($report->exception())->toBe(null); - expect($report->file())->toBe(null); - expect($report->line())->toBe(null); - expect($report->childs())->toBe([]); - - }); - - }); - - describe("->add()", function() { - - beforeEach(function() { - $this->scope = new Scope(); - $this->pattern = '*Suite.php'; - $this->regExp = strtr(preg_quote($this->pattern, '~'), ['\*' => '.*', '\?' => '.']); - $this->scope->backtraceFocus($this->pattern); - $this->reports = new Report([ - "scope" => $this->scope - ]); - }); - - it("rebases backtrace on fail report", function() { - - $this->reports->add('fail', [ - 'backtrace' => debug_backtrace() - ]); - - $logs = $this->reports->childs(); - $report = $logs[0]; - expect($report->backtrace()[0]['file'])->toMatch("~^{$this->regExp}$~"); - - }); - - it("doesn't rebase backtrace on an exception report", function() { - - $this->reports->add('exception', [ - 'exception' => new Exception() - ]); - - $logs = $this->reports->childs(); - $report = $logs[0]; - expect($report->backtrace()[0]['file'])->not->toMatch("~^{$this->regExp}$~"); - - }); - - }); - -}); \ No newline at end of file diff --git a/spec/Suite/Reporter/Coverage/CollectorSpec.php b/spec/Suite/Reporter/Coverage/Collector.spec.php similarity index 86% rename from spec/Suite/Reporter/Coverage/CollectorSpec.php rename to spec/Suite/Reporter/Coverage/Collector.spec.php index 03b5f29e..1d01c24d 100644 --- a/spec/Suite/Reporter/Coverage/CollectorSpec.php +++ b/spec/Suite/Reporter/Coverage/Collector.spec.php @@ -6,16 +6,16 @@ use Kahlan\Reporter\Coverage\Driver\Phpdbg; use Kahlan\Spec\Fixture\Reporter\Coverage\CodeCoverage; -describe("Coverage", function() { +describe("Coverage", function () { - beforeEach(function() { + beforeEach(function () { if (!extension_loaded('xdebug') && PHP_SAPI !== 'phpdbg') { skipIf(true); } $this->driver = PHP_SAPI !== 'phpdbg' ? new Xdebug() : new Phpdbg(); }); - beforeEach(function() { + beforeEach(function () { $this->path = 'spec/Fixture/Reporter/Coverage/CodeCoverage.php'; $this->collector = new Collector([ @@ -32,9 +32,9 @@ }); - describe("->export()", function() { + describe("->export()", function () { - it("exports covered lines", function() { + it("exports covered lines", function () { $code = new CodeCoverage(); @@ -51,7 +51,7 @@ ]); }); - it("exports multiline array", function() { + it("exports multiline array", function () { $code = new CodeCoverage(); @@ -69,7 +69,7 @@ ]); }); - it("exports covered lines and append coverage to parent's coverage data", function() { + it("exports covered lines and append coverage to parent's coverage data", function () { $code = new CodeCoverage(); @@ -100,7 +100,7 @@ ]); }); - it("exports covered lines and doesn't append coverage to parent's coverage data", function() { + it("exports covered lines and doesn't append coverage to parent's coverage data", function () { $code = new CodeCoverage(); @@ -130,9 +130,9 @@ }); - describe("->start/stop()", function() { + describe("->start/stop()", function () { - it("return `true` on success", function() { + it("return `true` on success", function () { expect($this->collector->start())->toBe(true); expect($this->collector->stop())->toBe(true); @@ -141,15 +141,15 @@ }); - describe("->stop()", function() { + describe("->stop()", function () { - it("does nothing if not the collector has not been started", function() { + it("does nothing if not the collector has not been started", function () { expect($this->collector->stop())->toBe(false); }); - it("does nothing if not the collector has not been started", function() { + it("does nothing if not the collector has not been started", function () { $this->parent->start(); $this->child->start(); @@ -164,9 +164,9 @@ }); - describe("->metrics()", function() { + describe("->metrics()", function () { - it("returns the metrics", function() { + it("returns the metrics", function () { $code = new CodeCoverage(); @@ -180,9 +180,9 @@ }); - describe("->realpath()", function() { + describe("->realpath()", function () { - it("supports special chars", function() { + it("supports special chars", function () { $collector = new Collector([ 'driver' => $this->driver, diff --git a/spec/Suite/Reporter/Coverage/Exporter/CloverSpec.php b/spec/Suite/Reporter/Coverage/Exporter/Clover.spec.php similarity index 78% rename from spec/Suite/Reporter/Coverage/Exporter/CloverSpec.php rename to spec/Suite/Reporter/Coverage/Exporter/Clover.spec.php index 7ed59586..147db5c2 100644 --- a/spec/Suite/Reporter/Coverage/Exporter/CloverSpec.php +++ b/spec/Suite/Reporter/Coverage/Exporter/Clover.spec.php @@ -9,21 +9,21 @@ use Kahlan\Spec\Fixture\Reporter\Coverage\ExtraEmptyLine; use RuntimeException; -describe("Clover", function() { +describe("Clover", function () { - beforeEach(function() { + beforeEach(function () { if (!extension_loaded('xdebug') && PHP_SAPI !== 'phpdbg') { skipIf(true); } - if(!class_exists('DOMDocument', false)) { + if (!class_exists('DOMDocument', false)) { skipIf(true); } $this->driver = PHP_SAPI !== 'phpdbg' ? new Xdebug() : new Phpdbg(); }); - describe("::export()", function() { + describe("::export()", function () { - it("exports the coverage of a file with no extra end line", function() { + it("exports the coverage of a file with no extra end line", function () { $path = 'spec' . DS . 'Fixture' . DS . 'Reporter' . DS . 'Coverage' . DS . 'NoEmptyLine.php'; @@ -43,15 +43,15 @@ $xml = Clover::export([ 'collector' => $collector, 'time' => $time, - 'base_path' => DS . 'home' . DS . 'crysalead' . DS . 'kahlan' + 'base_path' => DS . 'home' . DS . 'kahlan' . DS . 'kahlan' ]); $ds = DS; -$expected = << - + @@ -66,7 +66,7 @@ expect($xml)->toBe($expected); }); - it("exports the coverage of a file with an extra line at the end", function() { + it("exports the coverage of a file with an extra line at the end", function () { $path = 'spec' . DS . 'Fixture' . DS . 'Reporter' . DS . 'Coverage' . DS . 'ExtraEmptyLine.php'; @@ -86,15 +86,15 @@ $xml = Clover::export([ 'collector' => $collector, 'time' => $time, - 'base_path' => DS . 'home' . DS . 'crysalead' . DS . 'kahlan' + 'base_path' => DS . 'home' . DS . 'kahlan' . DS . 'kahlan' ]); $ds = DS; -$expected = << - + @@ -112,17 +112,17 @@ }); - describe("::write()", function() { + describe("::write()", function () { - beforeEach(function() { + beforeEach(function () { $this->output = tempnam("/tmp", "KAHLAN"); }); - afterEach(function() { + afterEach(function () { unlink($this->output); }); - it("writes the coverage to a file", function() { + it("writes the coverage to a file", function () { $path = 'spec' . DS . 'Fixture' . DS . 'Reporter' . DS . 'Coverage' . DS . 'NoEmptyLine.php'; @@ -143,19 +143,19 @@ 'collector' => $collector, 'file' => $this->output, 'time' => $time, - 'base_path' => DS . 'home' . DS . 'crysalead' . DS . 'kahlan' + 'base_path' => DS . 'home' . DS . 'kahlan' . DS . 'kahlan' ]); - expect($success)->toBe(484); + expect($success)->toBe(481); $xml = file_get_contents($this->output); $ds = DS; -$expected = << - + @@ -171,9 +171,9 @@ }); - it("throws exception when no file is set", function() { + it("throws exception when no file is set", function () { - expect(function() { + expect(function () { Clover::write([]); })->toThrow(new RuntimeException('Missing file name')); diff --git a/spec/Suite/Reporter/Coverage/Exporter/CodeClimateSpec.php b/spec/Suite/Reporter/Coverage/Exporter/CodeClimate.spec.php similarity index 86% rename from spec/Suite/Reporter/Coverage/Exporter/CodeClimateSpec.php rename to spec/Suite/Reporter/Coverage/Exporter/CodeClimate.spec.php index 5a30e387..d9b03fd2 100644 --- a/spec/Suite/Reporter/Coverage/Exporter/CodeClimateSpec.php +++ b/spec/Suite/Reporter/Coverage/Exporter/CodeClimate.spec.php @@ -9,18 +9,18 @@ use Kahlan\Spec\Fixture\Reporter\Coverage\ExtraEmptyLine; use RuntimeException; -describe("CodeClimate", function() { +describe("CodeClimate", function () { - beforeEach(function() { + beforeEach(function () { if (!extension_loaded('xdebug') && PHP_SAPI !== 'phpdbg') { skipIf(true); } $this->driver = PHP_SAPI !== 'phpdbg' ? new Xdebug() : new Phpdbg(); }); - describe("::export()", function() { + describe("::export()", function () { - it("exports custom parameters", function() { + it("exports custom parameters", function () { $collector = new Collector([ 'driver' => $this->driver @@ -54,7 +54,7 @@ ]); }); - it("exports the coverage of a file with no extra end line", function() { + it("exports the coverage of a file with no extra end line", function () { $path = 'spec' . DS . 'Fixture' . DS . 'Reporter' . DS . 'Coverage' . DS . 'NoEmptyLine.php'; @@ -84,17 +84,17 @@ expect(array_filter($coverage))->toHaveLength(2); - expect(array_filter($coverage, function($value){ + expect(array_filter($coverage, function ($value) { return $value === 0; }))->toHaveLength(2); - expect(array_filter($coverage, function($value){ + expect(array_filter($coverage, function ($value) { return $value === null; }))->toHaveLength(11); }); - it("exports the coverage of a file with an extra line at the end", function() { + it("exports the coverage of a file with an extra line at the end", function () { $path = 'spec' . DS . 'Fixture' . DS . 'Reporter' . DS . 'Coverage' . DS . 'ExtraEmptyLine.php'; @@ -126,11 +126,11 @@ $coverage = json_decode($coverage['coverage']); expect($coverage)->toHaveLength(16); - expect(array_filter($coverage, function($value){ + expect(array_filter($coverage, function ($value) { return $value === 0; }))->toHaveLength(2); - expect(array_filter($coverage, function($value){ + expect(array_filter($coverage, function ($value) { return $value === null; }))->toHaveLength(12); @@ -138,17 +138,17 @@ }); - describe("::write()", function() { + describe("::write()", function () { - beforeEach(function() { + beforeEach(function () { $this->output = tempnam("/tmp", "KAHLAN"); }); - afterEach(function() { + afterEach(function () { unlink($this->output); }); - it("writes the coverage to a file", function() { + it("writes the coverage to a file", function () { $path = 'spec' . DS . 'Fixture' . DS . 'Reporter' . DS . 'Coverage' . DS . 'ExtraEmptyLine.php'; @@ -167,7 +167,7 @@ 'collector' => $collector, 'file' => $this->output, 'environment' => [ - 'pwd' => DS . 'home' . DS . 'crysalead' . DS . 'kahlan' + 'pwd' => DS . 'home' . DS . 'kahlan' . DS . 'kahlan' ], 'repo_token' => 'ABC' ]); @@ -184,9 +184,9 @@ }); - it("throws an exception when no file is set", function() { + it("throws an exception when no file is set", function () { - expect(function() { + expect(function () { CodeClimate::write([]); })->toThrow(new RuntimeException("Missing file name")); diff --git a/spec/Suite/Reporter/Coverage/Exporter/CoverallsSpec.php b/spec/Suite/Reporter/Coverage/Exporter/Coveralls.spec.php similarity index 87% rename from spec/Suite/Reporter/Coverage/Exporter/CoverallsSpec.php rename to spec/Suite/Reporter/Coverage/Exporter/Coveralls.spec.php index d01f6a1a..3118ba59 100644 --- a/spec/Suite/Reporter/Coverage/Exporter/CoverallsSpec.php +++ b/spec/Suite/Reporter/Coverage/Exporter/Coveralls.spec.php @@ -9,18 +9,18 @@ use Kahlan\Spec\Fixture\Reporter\Coverage\ExtraEmptyLine; use RuntimeException; -describe("Coveralls", function() { +describe("Coveralls", function () { - beforeEach(function() { + beforeEach(function () { if (!extension_loaded('xdebug') && PHP_SAPI !== 'phpdbg') { skipIf(true); } $this->driver = PHP_SAPI !== 'phpdbg' ? new Xdebug() : new Phpdbg(); }); - describe("::export()", function() { + describe("::export()", function () { - it("exports the coverage of a file with no extra end line", function() { + it("exports the coverage of a file with no extra end line", function () { $path = 'spec' . DS . 'Fixture' . DS . 'Reporter' . DS . 'Coverage' . DS . 'NoEmptyLine.php'; @@ -54,17 +54,17 @@ expect($coverage['coverage'])->toHaveLength(15); expect(array_filter($coverage['coverage']))->toHaveLength(2); - expect(array_filter($coverage['coverage'], function($value){ + expect(array_filter($coverage['coverage'], function ($value) { return $value === 0; }))->toHaveLength(2); - expect(array_filter($coverage['coverage'], function($value){ + expect(array_filter($coverage['coverage'], function ($value) { return $value === null; }))->toHaveLength(11); }); - it("exports the coverage of a file with an extra line at the end", function() { + it("exports the coverage of a file with an extra line at the end", function () { $path = 'spec' . DS . 'Fixture' . DS . 'Reporter' . DS . 'Coverage' . DS . 'ExtraEmptyLine.php'; @@ -97,11 +97,11 @@ expect($coverage['source'])->toBe(file_get_contents($path)); expect($coverage['coverage'])->toHaveLength(16); - expect(array_filter($coverage['coverage'], function($value){ + expect(array_filter($coverage['coverage'], function ($value) { return $value === 0; }))->toHaveLength(2); - expect(array_filter($coverage['coverage'], function($value){ + expect(array_filter($coverage['coverage'], function ($value) { return $value === null; }))->toHaveLength(12); @@ -109,17 +109,17 @@ }); - describe("::write()", function() { + describe("::write()", function () { - beforeEach(function() { + beforeEach(function () { $this->output = tempnam("/tmp", "KAHLAN"); }); - afterEach(function() { + afterEach(function () { unlink($this->output); }); - it("writes the coverage to a file", function() { + it("writes the coverage to a file", function () { $path = 'spec' . DS . 'Fixture' . DS . 'Reporter' . DS . 'Coverage' . DS . 'ExtraEmptyLine.php'; @@ -159,9 +159,9 @@ }); - it("throws an exception no file is set", function() { + it("throws an exception no file is set", function () { - expect(function() { + expect(function () { Coveralls::write([]); })->toThrow(new RuntimeException("Missing file name")); diff --git a/spec/Suite/Reporter/Coverage/Exporter/IstanbulSpec.php b/spec/Suite/Reporter/Coverage/Exporter/Istanbul.spec.php similarity index 59% rename from spec/Suite/Reporter/Coverage/Exporter/IstanbulSpec.php rename to spec/Suite/Reporter/Coverage/Exporter/Istanbul.spec.php index b9e28e8d..97b5104f 100644 --- a/spec/Suite/Reporter/Coverage/Exporter/IstanbulSpec.php +++ b/spec/Suite/Reporter/Coverage/Exporter/Istanbul.spec.php @@ -9,18 +9,18 @@ use Kahlan\Spec\Fixture\Reporter\Coverage\ExtraEmptyLine; use RuntimeException; -describe("Istanbul", function() { +describe("Istanbul", function () { - beforeEach(function() { + beforeEach(function () { if (!extension_loaded('xdebug') && PHP_SAPI !== 'phpdbg') { skipIf(true); } $this->driver = PHP_SAPI !== 'phpdbg' ? new Xdebug() : new Phpdbg(); }); - describe("::export()", function() { + describe("::export()", function () { - it("exports the coverage of a file with no extra end line", function() { + it("exports the coverage of a file with no extra end line", function () { $path = 'spec' . DS . 'Fixture' . DS . 'Reporter' . DS . 'Coverage' . DS . 'NoEmptyLine.php'; @@ -39,18 +39,18 @@ $json = Istanbul::export([ 'collector' => $collector, - 'base_path' => DS . 'home' . DS . 'crysalead' . DS . 'kahlan' + 'base_path' => DS . 'home' . DS . 'kahlan' . DS . 'kahlan' ]); $ds = DS; -$expected = <<toBe($expected); }); - it("exports the coverage of a file with an extra line at the end", function() { + it("exports the coverage of a file with an extra line at the end", function () { $path = 'spec' . DS . 'Fixture' . DS . 'Reporter' . DS . 'Coverage' . DS . 'ExtraEmptyLine.php'; @@ -69,12 +69,12 @@ $json = Istanbul::export([ 'collector' => $collector, - 'base_path' => DS . 'home' . DS . 'crysalead' . DS . 'kahlan' + 'base_path' => DS . 'home' . DS . 'kahlan' . DS . 'kahlan' ]); $ds = DS; -$expected = <<toBe($expected); @@ -83,17 +83,17 @@ }); - describe("::write()", function() { + describe("::write()", function () { - beforeEach(function() { + beforeEach(function () { $this->output = tempnam("/tmp", "KAHLAN"); }); - afterEach(function() { + afterEach(function () { unlink($this->output); }); - it("writes the coverage to a file", function() { + it("writes the coverage to a file", function () { $path = 'spec' . DS . 'Fixture' . DS . 'Reporter' . DS . 'Coverage' . DS . 'NoEmptyLine.php'; @@ -113,25 +113,25 @@ $success = Istanbul::write([ 'collector' => $collector, 'file' => $this->output, - 'base_path' => DS . 'home' . DS . 'crysalead' . DS . 'kahlan' + 'base_path' => DS . 'home' . DS . 'kahlan' . DS . 'kahlan' ]); - expect($success)->toBe(635); + expect($success)->toBe(629); $json = file_get_contents($this->output); $ds = DS; -$expected = <<toBe($expected); }); - it("throws exception when no file is set", function() { + it("throws exception when no file is set", function () { - expect(function() { + expect(function () { Istanbul::write([]); })->toThrow(new RuntimeException('Missing file name')); diff --git a/spec/Suite/Reporter/Coverage/Exporter/LcovSpec.php b/spec/Suite/Reporter/Coverage/Exporter/Lcov.spec.php similarity index 76% rename from spec/Suite/Reporter/Coverage/Exporter/LcovSpec.php rename to spec/Suite/Reporter/Coverage/Exporter/Lcov.spec.php index 64182585..05591ab7 100644 --- a/spec/Suite/Reporter/Coverage/Exporter/LcovSpec.php +++ b/spec/Suite/Reporter/Coverage/Exporter/Lcov.spec.php @@ -9,18 +9,18 @@ use Kahlan\Spec\Fixture\Reporter\Coverage\ExtraEmptyLine; use RuntimeException; -describe("Lcov", function() { +describe("Lcov", function () { - beforeEach(function() { + beforeEach(function () { if (!extension_loaded('xdebug') && PHP_SAPI !== 'phpdbg') { skipIf(true); } $this->driver = PHP_SAPI !== 'phpdbg' ? new Xdebug() : new Phpdbg(); }); - describe("::export()", function() { + describe("::export()", function () { - it("exports the coverage of a file with no extra end line", function() { + it("exports the coverage of a file with no extra end line", function () { $path = 'spec' . DS . 'Fixture' . DS . 'Reporter' . DS . 'Coverage' . DS . 'NoEmptyLine.php'; @@ -39,13 +39,13 @@ $txt = Lcov::export([ 'collector' => $collector, - 'base_path' => DS . 'home' . DS . 'crysalead' . DS . 'kahlan' + 'base_path' => DS . 'home' . DS . 'kahlan' . DS . 'kahlan' ]); $ds = DS; -$expected = <<toBe($expected); }); - it("exports the coverage of a file with an extra line at the end", function() { + it("exports the coverage of a file with an extra line at the end", function () { $path = 'spec' . DS . 'Fixture' . DS . 'Reporter' . DS . 'Coverage' . DS . 'ExtraEmptyLine.php'; @@ -81,13 +81,13 @@ $txt = Lcov::export([ 'collector' => $collector, - 'base_path' => DS . 'home' . DS . 'crysalead' . DS . 'kahlan' + 'base_path' => DS . 'home' . DS . 'kahlan' . DS . 'kahlan' ]); $ds = DS; -$expected = <<output = tempnam("/tmp", "KAHLAN"); }); - afterEach(function() { + afterEach(function () { unlink($this->output); }); - it("writes the coverage to a file", function() { + it("writes the coverage to a file", function () { $path = 'spec' . DS . 'Fixture' . DS . 'Reporter' . DS . 'Coverage' . DS . 'NoEmptyLine.php'; @@ -137,17 +137,17 @@ $success = Lcov::write([ 'collector' => $collector, 'file' => $this->output, - 'base_path' => DS . 'home' . DS . 'crysalead' . DS . 'kahlan' + 'base_path' => DS . 'home' . DS . 'kahlan' . DS . 'kahlan' ]); - expect($success)->toBe(178); + expect($success)->toBe(175); $txt = file_get_contents($this->output); $ds = DS; -$expected = <<toThrow(new RuntimeException('Missing file name')); diff --git a/spec/Suite/Reporter/Coverage/MetricsSpec.php b/spec/Suite/Reporter/Coverage/Metrics.spec.php similarity index 53% rename from spec/Suite/Reporter/Coverage/MetricsSpec.php rename to spec/Suite/Reporter/Coverage/Metrics.spec.php index a2acd1f4..f1c51bba 100644 --- a/spec/Suite/Reporter/Coverage/MetricsSpec.php +++ b/spec/Suite/Reporter/Coverage/Metrics.spec.php @@ -1,6 +1,8 @@ driver = PHP_SAPI !== 'phpdbg' ? new Xdebug() : new Phpdbg(); }); - beforeEach(function() { + beforeEach(function () { $this->path = [ + 'spec/Fixture/Reporter/Coverage/GlobalFunctions.php', + 'spec/Fixture/Reporter/Coverage/Functions.php', 'spec/Fixture/Reporter/Coverage/ExtraEmptyLine.php', 'spec/Fixture/Reporter/Coverage/NoEmptyLine.php' ]; @@ -31,9 +35,9 @@ ]); }); - describe("->metrics()", function() { + describe("->metrics()", function () { - it("returns the global metrics", function() { + it("returns the global metrics", function () { $empty = new ExtraEmptyLine(); $noEmpty = new NoEmptyLine(); @@ -41,24 +45,25 @@ $this->collector->start(); $empty->shallNotPass(); $noEmpty->shallNotPass(); + \shallNotPass(); + \Kahlan\Spec\Fixture\Reporter\Coverage\shallNotPass(); $this->collector->stop(); $metrics = $this->collector->metrics(); - $actual = $metrics->data(); $files = $actual['files']; unset($actual['files']); expect($actual)->toBe([ - 'loc' => 31, - 'nlloc' => 23, - 'lloc' => 8, - 'cloc' => 4, - 'coverage' => 4, - 'methods' => 2, - 'cmethods' => 2, + 'loc' => 55, + 'nlloc' => 39, + 'lloc' => 16, + 'cloc' => 8, + 'coverage' => 8, + 'methods' => 4, + 'cmethods' => 4, 'percent' => 50 ]); @@ -68,7 +73,7 @@ } }); - it("returns class metrics", function() { + it("returns class metrics", function () { $code = new ExtraEmptyLine(); @@ -98,7 +103,7 @@ expect(isset($files[$path]))->toBe(true); }); - it("returns type of metrics", function() { + it("returns type of metrics", function () { $code = new ExtraEmptyLine(); @@ -111,7 +116,7 @@ }); - it("returns a parent of metrics", function() { + it("returns a parent of metrics", function () { $code = new ExtraEmptyLine(); @@ -124,7 +129,7 @@ }); - it("returns function metrics", function() { + it("returns methods metrics", function () { $code = new ExtraEmptyLine(); @@ -158,7 +163,72 @@ expect(isset($files[$path]))->toBe(true); }); - it("return empty on unknown metric", function() { + it("returns global function metrics", function () { + + $this->collector->start(); + \shallNotPass(); + $this->collector->stop(); + + $metrics = $this->collector->metrics(); + + $actual = $metrics->get('shallNotPass()')->data(); + + $files = $actual['files']; + unset($actual['files']); + + expect($actual)->toBe([ + 'loc' => 8, + 'nlloc' => 4, + 'lloc' => 4, + 'cloc' => 2, + 'coverage' => 2, + 'methods' => 1, + 'cmethods' => 1, + 'line' => [ + 'start' => 1, + 'stop' => 9 + ], + 'percent' => 50 + ]); + + $path = realpath('spec/Fixture/Reporter/Coverage/GlobalFunctions.php'); + expect(isset($files[$path]))->toBe(true); + }); + + it("returns function metrics", function () { + + $this->collector->start(); + \Kahlan\Spec\Fixture\Reporter\Coverage\shallNotPass(); + $this->collector->stop(); + + $metrics = $this->collector->metrics(); + + $actual = $metrics->get('Kahlan\Spec\Fixture\Reporter\Coverage\shallNotPass()')->data(); + + $files = $actual['files']; + unset($actual['files']); + + expect($actual)->toBe([ + 'loc' => 8, + 'nlloc' => 4, + 'lloc' => 4, + 'cloc' => 2, + 'coverage' => 2, + 'methods' => 1, + 'cmethods' => 1, + 'line' => [ + 'start' => 3, + 'stop' => 11 + ], + 'percent' => 50 + ]); + + $path = realpath('spec/Fixture/Reporter/Coverage/Functions.php'); + expect(isset($files[$path]))->toBe(true); + + }); + + it("returns empty for unknown metric", function () { $code = new ExtraEmptyLine(); @@ -172,7 +242,7 @@ }); - it("doesn't store interfaces in metrics", function() { + it("ignores interfaces metrics", function () { $path = [ 'spec/Fixture/Reporter/Coverage/ImplementsCoverage.php', @@ -191,14 +261,14 @@ $collector->stop(); $metrics = $collector->metrics(); - $actual = $metrics->get()->data(); + $actual = $metrics->get('Kahlan\Spec\Fixture\Reporter\Coverage\ImplementsCoverage')->data(); $files = $actual['files']; unset($actual['files']); expect($actual)->toBe([ - 'loc' => 10, - 'nlloc' => 9, + 'loc' => 6, + 'nlloc' => 5, 'lloc' => 1, 'cloc' => 1, 'coverage' => 1, @@ -210,11 +280,13 @@ $path = realpath('spec/Fixture/Reporter/Coverage/ImplementsCoverage.php'); expect(isset($files[$path]))->toBe(true); + expect($metrics->get('Kahlan\Spec\Fixture\Reporter\Coverage\ImplementsCoverageInterface'))->toBe(null); + }); - describe("->childs()", function() { + describe("->children()", function () { - beforeEach(function() { + beforeEach(function () { $code = new ExtraEmptyLine(); @@ -226,43 +298,43 @@ }); - it("returns root's childs", function() { + it("returns root's children", function () { - $childs = $this->metrics->childs(); - expect(is_array($childs))->toBe(true); - expect(isset($childs['Kahlan']))->toBe(true); + $children = $this->metrics->children(); + expect(is_array($children))->toBe(true); + expect(isset($children['Kahlan\\']))->toBe(true); }); - it("returns specified child", function() { + it("returns specified child", function () { - $childs = $this->metrics->childs('Kahlan'); - expect(is_array($childs))->toBe(true); - expect(isset($childs['Spec']))->toBe(true); + $children = $this->metrics->children('Kahlan\\'); + expect(is_array($children))->toBe(true); + expect(isset($children['Spec\\']))->toBe(true); - $childs = $this->metrics->childs('Kahlan\Spec'); - expect(is_array($childs))->toBe(true); - expect(isset($childs['Fixture']))->toBe(true); + $children = $this->metrics->children('Kahlan\Spec\\'); + expect(is_array($children))->toBe(true); + expect(isset($children['Fixture\\']))->toBe(true); - $childs = $this->metrics->childs('Kahlan\Spec\Fixture'); - expect(is_array($childs))->toBe(true); - expect(isset($childs['Reporter']))->toBe(true); + $children = $this->metrics->children('Kahlan\Spec\Fixture\\'); + expect(is_array($children))->toBe(true); + expect(isset($children['Reporter\\']))->toBe(true); - $childs = $this->metrics->childs('Kahlan\Spec\Fixture\Reporter'); - expect(is_array($childs))->toBe(true); - expect(isset($childs['Coverage']))->toBe(true); + $children = $this->metrics->children('Kahlan\Spec\Fixture\Reporter\\'); + expect(is_array($children))->toBe(true); + expect(isset($children['Coverage\\']))->toBe(true); - $childs = $this->metrics->childs('Kahlan\Spec\Fixture\Reporter\Coverage'); - expect(is_array($childs))->toBe(true); - expect(isset($childs['ExtraEmptyLine']))->toBe(true); - expect(isset($childs['NoEmptyLine']))->toBe(true); + $children = $this->metrics->children('Kahlan\Spec\Fixture\Reporter\Coverage\\'); + expect(is_array($children))->toBe(true); + expect(isset($children['ExtraEmptyLine']))->toBe(true); + expect(isset($children['NoEmptyLine']))->toBe(true); }); - it("returns `null` on unknown child", function() { + it("returns `null` on unknown child", function () { - $childs = $this->metrics->childs('unknown_child'); - expect($childs)->toBe(null); + $children = $this->metrics->children('unknown_child'); + expect($children)->toBe([]); }); diff --git a/spec/Suite/ReportersSpec.php b/spec/Suite/Reporters.spec.php similarity index 66% rename from spec/Suite/ReportersSpec.php rename to spec/Suite/Reporters.spec.php index ad5399b1..457e2201 100644 --- a/spec/Suite/ReportersSpec.php +++ b/spec/Suite/Reporters.spec.php @@ -3,19 +3,19 @@ use Exception; use Kahlan\Reporters; -use Kahlan\Plugin\Stub; +use Kahlan\Plugin\Double; -describe("Reporters", function() { +describe("Reporters", function () { - beforeEach(function(){ + beforeEach(function () { $this->reporters = new Reporters; }); - describe("->add/get()", function() { + describe("->add/get()", function () { - it("stores a reporter", function() { + it("stores a reporter", function () { - $stub = Stub::create(); + $stub = Double::instance(); $this->reporters->add('my_reporter', $stub); $actual = $this->reporters->get('my_reporter'); @@ -23,9 +23,9 @@ }); - it("throws an exception with a scalar value", function() { + it("throws an exception with a scalar value", function () { - $closure = function() { + $closure = function () { $this->reporters->add('my_reporter', 'Hello World!'); }; @@ -35,9 +35,9 @@ }); - describe("->get()", function() { + describe("->get()", function () { - it("returns `null` for an unexisting reporter", function() { + it("returns `null` for an unexisting reporter", function () { $actual = $this->reporters->get('my_reporter'); expect($actual)->toBe(null); @@ -46,11 +46,11 @@ }); - describe("->exists()", function() { + describe("->exists()", function () { - it("returns `true` for an existing reporter", function() { + it("returns `true` for an existing reporter", function () { - $stub = Stub::create(); + $stub = Double::instance(); $this->reporters->add('my_reporter', $stub); $actual = $this->reporters->exists('my_reporter'); @@ -58,7 +58,7 @@ }); - it("returns `false` for an unexisting reporter", function() { + it("returns `false` for an unexisting reporter", function () { $actual = $this->reporters->exists('my_reporter'); expect($actual)->toBe(false); @@ -67,11 +67,11 @@ }); - describe("->remove()", function() { + describe("->remove()", function () { - it("removes a reporter", function() { + it("removes a reporter", function () { - $stub = Stub::create(); + $stub = Double::instance(); $this->reporters->add('my_reporter', $stub); $actual = $this->reporters->exists('my_reporter'); @@ -86,11 +86,11 @@ }); - describe("->clear()", function() { + describe("->clear()", function () { - it("clears all reporters", function() { + it("clears all reporters", function () { - $stub = Stub::create(); + $stub = Double::instance(); $this->reporters->add('my_reporter', $stub); $actual = $this->reporters->exists('my_reporter'); @@ -105,23 +105,23 @@ }); - describe("->process()", function() { + describe("->dispatch()", function () { - it("runs a method on all reporters", function() { + it("runs a method on all reporters", function () { - $stub1 = Stub::create(); + $stub1 = Double::instance(); $this->reporters->add('reporter1', $stub1); - $stub2 = Stub::create(); + $stub2 = Double::instance(); $this->reporters->add('reporter2', $stub2); expect($stub1)->toReceive('action')->with(['value']); expect($stub2)->toReceive('action')->with(['value']); - $this->reporters->process('action', ['value']); + $this->reporters->dispatch('action', ['value']); }); }); -}); \ No newline at end of file +}); diff --git a/spec/Suite/Scope.spec.php b/spec/Suite/Scope.spec.php new file mode 100644 index 00000000..6a58d5fd --- /dev/null +++ b/spec/Suite/Scope.spec.php @@ -0,0 +1,263 @@ +scope = new Scope(['message' => 'it runs a spec']); + + }); + + describe("->__construct()", function () { + + it("sets passed options", function () { + + $log = new Log(); + $summary = new Summary(); + + $scope = new Scope([ + 'type' => 'focus', + 'message' => 'test', + 'parent' => null, + 'root' => null, + 'log' => $log, + 'timeout' => 10, + 'summary' => $summary + ]); + + expect($scope->type())->toBe('focus'); + expect($scope->message())->toBe('test'); + expect($scope->parent())->toBe(null); + expect($scope->log())->toBe($log); + expect($scope->summary())->toBe($summary); + + }); + + }); + + describe("->parent()", function () { + + it("returns the parent node", function () { + + $parent = new Scope(); + $this->scope = new Scope(['parent' => $parent]); + expect($this->scope->parent())->toBe($parent); + + }); + + }); + + describe("->backtrace()", function () { + + it("returns the backtrace", function () { + + $this->scope = new Scope(); + expect(basename($this->scope->backtrace()[1]['file']))->toBe('Scope.spec.php'); + + }); + + }); + + describe("->__get/__set()", function () { + + it("defines a value in the current scope", function () { + + $this->foo = 2; + expect($this->foo)->toEqual(2); + + }); + + it("is not influenced by the previous spec", function () { + + expect(isset($this->foo))->toBe(false); + + }); + + it("throw an new exception for reserved keywords", function () { + + foreach (Scope::$blacklist as $keyword => $bool) { + $closure = function () use ($keyword) { + $this->{$keyword} = 'some value'; + }; + expect($closure)->toThrow(new Exception("Sorry `{$keyword}` is a reserved keyword, it can't be used as a scope variable.")); + } + + }); + + it("throws an exception on undefined variables", function () { + + $closure = function () { + $a = $this->unexisting; + }; + + expect($closure)->toThrow(new Exception('Undefined variable `unexisting`.')); + + }); + + it("throws properly message on expect() usage inside of describe()", function () { + + $closure = function () { + $this->expect; + }; + + + expect($closure)->toThrow(new Exception("You can't use expect() inside of describe()")); + + }); + + context("when nested", function () { + + beforeEach(function () { + $this->bar = 1; + }); + + it("can access variable from the parent scope", function () { + + expect($this->bar)->toBe(1); + + }); + }); + }); + + describe("skipIf", function () { + + it("returns none if provided false/null", function () { + + expect(skipIf(false))->toBe(null); + + }); + + $executed = 0; + + context("when used in a scope", function () use (&$executed) { + + beforeAll(function () { + skipIf(true); + }); + + it("skips this spec", function () use (&$executed) { + + expect(true)->toBe(false); + $executed++; + + }); + + it("skips this spec too", function () use (&$executed) { + + expect(true)->toBe(false); + $executed++; + + }); + + }); + + it("expects that no spec have been runned", function () use (&$executed) { + + expect($executed)->toBe(0); + + }); + + context("when used in a spec", function () use (&$executed) { + + it("skips this spec", function () use (&$executed) { + + skipIf(true); + expect(true)->toBe(false); + $executed++; + + }); + + it("doesn't skip this spec", function () use (&$executed) { + + $executed++; + expect(true)->toBe(true); + }); + + }); + + it("expects that only one test have been runned", function () use (&$executed) { + + expect($executed)->toBe(1); + + }); + + }); + + describe("__call", function () { + + $this->customMethod = function ($self) { + $self->called = true; + return 'called'; + }; + + it("calls closure assigned to scope property to be inkovable", function () { + + $actual = $this->customMethod($this); + expect($actual)->toBe('called'); + expect($this->called)->toBe(true); + + }); + + it("throws an exception on no closure variable", function () { + + $closure = function () { + $this->mystring = 'hello'; + $a = $this->mystring(); + }; + + expect($closure)->toThrow(new Exception('Uncallable variable `mystring`.')); + + }); + + }); + + describe("->pass()", function () { + + it("logs a pass", function () { + + $this->scope->log('passed', ['matcher' => 'Kahlan\Matcher\ToBe']); + $expectation = $this->scope->log()->children()[0]; + expect($expectation->matcher())->toBe('Kahlan\Matcher\ToBe'); + expect($expectation->type())->toBe('passed'); + expect($expectation->messages())->toBe(['it runs a spec']); + + }); + + }); + + describe("->fail()", function () { + + it("logs a fail", function () { + + $this->scope->log('failed', ['matcher' => 'Kahlan\Matcher\ToBe']); + $expectation = $this->scope->log()->children()[0]; + expect($expectation->matcher())->toBe('Kahlan\Matcher\ToBe'); + expect($expectation->type())->toBe('failed'); + expect($expectation->messages())->toBe(['it runs a spec']); + + }); + + }); + + describe("->timeout()", function () { + + it("gets/sets the timeout value", function () { + + $this->scope->timeout(5); + expect($this->scope->timeout())->toBe(5); + + $this->scope->timeout(null); + expect($this->scope->timeout())->toBe(null); + + }); + + }); + +}); diff --git a/spec/Suite/ScopeSpec.php b/spec/Suite/ScopeSpec.php deleted file mode 100644 index 50356f6f..00000000 --- a/spec/Suite/ScopeSpec.php +++ /dev/null @@ -1,273 +0,0 @@ -scope = new Scope(['message' => 'it runs a spec']); - - }); - - describe("->__get/__set()", function() { - - it("defines a value in the current scope", function() { - - $this->foo = 2; - expect($this->foo)->toEqual(2); - - }); - - it("is not influenced by the previous spec", function() { - - expect(isset($this->foo))->toBe(false); - - }); - - it("throw an new exception for reserved keywords", function() { - - foreach (Scope::$blacklist as $keyword => $bool) { - $closure = function() use ($keyword) { - $this->{$keyword} = 'some value'; - }; - expect($closure)->toThrow(new Exception("Sorry `{$keyword}` is a reserved keyword, it can't be used as a scope variable.")); - } - - }); - - it("throws an exception on undefined variables", function() { - - $closure = function() { - $a = $this->unexisting; - }; - - expect($closure)->toThrow(new Exception('Undefined variable `unexisting`.')); - - }); - - it("throws properly message on expect() usage inside of describe()", function() { - - $closure = function() { - $this->expect; - }; - - - expect($closure)->toThrow(new Exception("You can't use expect() inside of describe()")); - - }); - - context("when nested", function() { - - beforeEach(function() { - $this->bar = 1; - }); - - it("can access variable from the parent scope", function() { - - expect($this->bar)->toBe(1); - - }); - }); - }); - - describe("skipIf", function() { - - it("returns none if provided false/null", function() { - - expect(skipIf(false))->toBe(null); - - }); - - $executed = 0; - - context("when used in a scope", function() use (&$executed) { - - before(function() { - skipIf(true); - }); - - it("skips this spec", function() use (&$executed) { - - expect(true)->toBe(false); - $executed++; - - }); - - it("skips this spec too", function() use (&$executed) { - - expect(true)->toBe(false); - $executed++; - - }); - - }); - - it("expects that no spec have been runned", function() use (&$executed) { - - expect($executed)->toBe(0); - - }); - - context("when used in a spec", function() use (&$executed) { - - it("skips this spec", function() use (&$executed) { - - skipIf(true); - expect(true)->toBe(false); - $executed++; - - }); - - it("doesn't skip this spec", function() use (&$executed) { - - $executed++; - - }); - - }); - - it("expects that only one test have been runned", function() use (&$executed) { - - expect($executed)->toBe(1); - - }); - - }); - - describe("__call", function() { - - $this->customMethod = function($self) { - $self->called = true; - return 'called'; - }; - - it("calls closure assigned to scope property to be inkovable", function() { - - $actual = $this->customMethod($this); - expect($actual)->toBe('called'); - expect($this->called)->toBe(true); - - }); - - it("throws an exception on no closure variable", function() { - - $closure = function() { - $this->mystring = 'hello'; - $a = $this->mystring(); - }; - - expect($closure)->toThrow(new Exception('Uncallable variable `mystring`.')); - - }); - - }); - - describe("->pass()", function() { - - it("logs a pass", function() { - - $this->scope->report()->add('pass', ['matcher' => 'Kahlan\Matcher\ToBe']); - $results = $this->scope->results(); - $report = reset($results['passed']); - - expect($report->matcher())->toBe('Kahlan\Matcher\ToBe'); - expect($report->type())->toBe('pass'); - expect($report->messages())->toBe(['it runs a spec']); - expect($report->backtrace())->toBeAn('array'); - - }); - - }); - - describe("->fail()", function() { - - it("logs a fail", function() { - - $this->scope->report()->add('fail', ['matcher' => 'Kahlan\Matcher\ToBe']); - $results = $this->scope->results(); - $report = reset($results['failed']); - - expect($report->matcher())->toBe('Kahlan\Matcher\ToBe'); - expect($report->type())->toBe('fail'); - expect($report->messages())->toBe(['it runs a spec']); - expect($report->backtrace())->toBeAn('array'); - - }); - - }); - - describe("->exception()", function() { - - it("logs a fail", function() { - - $this->scope->report()->add('exception', [ - 'matcher' => 'Kahlan\Matcher\ToThrow', - 'exception' => new Exception() - ]); - $results = $this->scope->results(); - $report = reset($results['exceptions']); - - expect($report->matcher())->toBe('Kahlan\Matcher\ToThrow'); - expect($report->type())->toBe('exception'); - expect($report->messages())->toBe(['it runs a spec']); - expect($report->backtrace())->toBeAn('array'); - - }); - - }); - - describe("->skip()", function() { - - it("logs a skip", function() { - - $this->scope->report()->add('skip', [ - 'exception' => new SkipException() - ]); - $results = $this->scope->results(); - $report = reset($results['skipped']); - - expect($report->type())->toBe('skip'); - expect($report->messages())->toBe(['it runs a spec']); - expect($report->backtrace())->toBeAn('array'); - }); - - }); - - describe("->incomplete()", function() { - - it("logs a fail", function() { - - $this->scope->report()->add('incomplete', [ - 'exception' => new IncompleteException() - ]); - $results = $this->scope->results(); - $report = reset($results['incomplete']); - - expect($report->type())->toBe('incomplete'); - expect($report->messages())->toBe(['it runs a spec']); - expect($report->backtrace())->toBeAn('array'); - - }); - - }); - - describe("->timeout()", function() { - - it("gets/sets the timeout value", function() { - - $this->scope->timeout(5); - expect($this->scope->timeout())->toBe(5); - - $this->scope->timeout(null); - expect($this->scope->timeout())->toBe(null); - - }); - - }); - -}); diff --git a/spec/Suite/Specification.spec.php b/spec/Suite/Specification.spec.php new file mode 100644 index 00000000..9aac4f7b --- /dev/null +++ b/spec/Suite/Specification.spec.php @@ -0,0 +1,414 @@ +spec = new Specification(['closure' => function () {}]); + + }); + + describe("->__construct()", function () { + + it("sets spec as pending with empty closure", function () { + + $this->spec = new Specification(['closure' => null]); + + expect($this->spec->passed())->toBe(true); + + $pending = $this->spec->summary()->pending(); + expect($pending)->toBe(1); + + }); + + }); + + describe("->expect()", function () { + + it("returns the matcher instance", function () { + + $matcher = $this->spec->expect('actual'); + expect($matcher)->toBeAnInstanceOf('Kahlan\Expectation'); + + }); + + }); + + describe("->waitsFor()", function () { + + it("allows non closure", function () { + + $this->spec = new Specification([ + 'message' => 'allows non closure', + 'closure' => function () { + $this->waitsFor('something')->toBe('something'); + } + ]); + + expect($this->spec->passed())->toBe(true); + + }); + + it("returns the matcher instance setted with the correct timeout", function () { + + $matcher = $this->spec->waitsFor(function (){}, 10); + expect($matcher)->toBeAnInstanceOf('Kahlan\Expectation'); + expect($matcher->timeout())->toBe(10); + + $matcher = $this->spec->waitsFor(function (){}); + expect($matcher)->toBeAnInstanceOf('Kahlan\Expectation'); + expect($matcher->timeout())->toBe(0); + + }); + + }); + + describe("->passed()", function () { + + it("returns the closure return value", function () { + + $this->spec = new Specification([ + 'closure' => function () { + return 'hello world'; + } + ]); + + $return = null; + $this->spec->passed($return); + expect($return)->toBe('hello world'); + + }); + + it("fails when an expectation is not verified", function () { + + $this->spec = new Specification([ + 'closure' => function () { + $this->expect(true)->toBe(true); + $this->expect(true); + } + ]); + + expect($this->spec->passed())->toBe(false); + + }); + + context("when the specs passed", function () { + + it("logs a pass", function () { + + $this->spec = new Specification([ + 'message' => 'runs a spec', + 'closure' => function () { + $this->expect(true)->toBe(true); + } + ]); + + expect($this->spec->passed())->toBe(true); + + $passed = $this->spec->summary()->logs('passed'); + expect($passed)->toHaveLength(1); + + $pass = reset($passed); + $expectation = $pass->children()[0]; + + expect($expectation->matcher())->toBe('Kahlan\Matcher\ToBe'); + expect($expectation->matcherName())->toBe('toBe'); + expect($expectation->not())->toBe(false); + expect($expectation->type())->toBe('passed'); + expect($expectation->data())->toBe([ + 'actual' => true, + 'expected' => true + ]); + expect($expectation->messages())->toBe(['it runs a spec']); + + }); + + it("logs a pass with a deferred matcher", function () { + + $this->spec = new Specification([ + 'message' => 'runs a spec', + 'closure' => function () { + $stub = Double::instance(); + $this->expect($stub)->toReceive('methodName'); + $stub->methodName(); + } + ]); + + expect($this->spec->passed())->toBe(true); + + $passes = $this->spec->summary()->logs('passed'); + expect($passes)->toHaveLength(1); + + $pass = reset($passes); + $expectation = $pass->children()[0]; + + expect($expectation->matcher())->toBe('Kahlan\Matcher\ToReceive'); + expect($expectation->matcherName())->toBe('toReceive'); + expect($expectation->not())->toBe(false); + expect($expectation->type())->toBe('passed'); + expect($expectation->data())->toBe([ + 'actual received' => 'methodName', + "actual received times" => 1, + 'expected to receive' => 'methodName' + ]); + expect($expectation->description())->toBe('receive the expected method.'); + expect($expectation->messages())->toBe(['it runs a spec']); + + }); + + it("logs the not attribute", function () { + + $this->spec = new Specification([ + 'closure' => function () { + $this->expect(true)->not->toBe(false); + } + ]); + + expect($this->spec->passed())->toBe(true); + + $passes = $this->spec->summary()->logs('passed'); + expect($passes)->toHaveLength(1); + + $pass = reset($passes); + $expectation = $pass->children()[0]; + + expect($expectation->not())->toBe(true); + + }); + + it("logs deferred matcher backtrace", function () { + + $root = new Suite(); + $root->backtraceFocus(['*Spec.php', '*.spec.php']); + $this->spec = new Specification([ + 'parent' => $root, + 'closure' => function () { + $this->expect(Double::instance())->not->toReceive('helloWorld'); + } + ]); + + expect($this->spec->passed())->toBe(true); + + $passes = $this->spec->summary()->logs('passed'); + expect($passes)->toHaveLength(1); + + $pass = reset($passes); + $expectation = $pass->children()[0]; + + $file = $expectation->file(); + expect($file)->toMatch('~Specification.spec.php$~'); + + }); + + it("logs the not attribute with a deferred matcher", function () { + + $this->spec = new Specification([ + 'closure' => function () { + $stub = Double::instance(); + $this->expect($stub)->not->toReceive('methodName'); + } + ]); + + expect($this->spec->passed())->toBe(true); + + $passes = $this->spec->summary()->logs('passed'); + expect($passes)->toHaveLength(1); + + $pass = reset($passes); + $expectation = $pass->children()[0]; + + expect($expectation->not())->toBe(true); + + }); + + it("resets `not` to `false ` after any matcher call", function () { + + expect([]) + ->not->toBeNull() + ->toBeA('array') + ->toBeEmpty(); + + }); + + }); + + context("when the specs failed", function () { + + it("logs a fail", function () { + + $this->spec = new Specification([ + 'message' => 'runs a spec', + 'closure' => function () { + $this->expect(true)->toBe(false); + } + ]); + + expect($this->spec->passed())->toBe(false); + + $failed = $this->spec->summary()->logs('failed'); + expect($failed)->toHaveLength(1); + $failure = reset($failed); + + $expectation = $failure->children()[0]; + expect($expectation->matcher())->toBe('Kahlan\Matcher\ToBe'); + expect($expectation->matcherName())->toBe('toBe'); + expect($expectation->not())->toBe(false); + expect($expectation->type())->toBe('failed'); + expect($expectation->data())->toBe([ + 'actual' => true, + 'expected' => false + ]); + expect($expectation->messages())->toBe(['it runs a spec']); + + }); + + it("logs a fail with a deferred matcher", function () { + + $this->spec = new Specification([ + 'message' => 'runs a spec', + 'closure' => function () { + $stub = Double::instance(); + $this->expect($stub)->toReceive('methodName'); + } + ]); + + expect($this->spec->passed())->toBe(false); + + $failed = $this->spec->summary()->logs('failed'); + expect($failed)->toHaveLength(1); + + $failure = reset($failed); + + $expectation = $failure->children()[0]; + expect($expectation->matcher())->toBe('Kahlan\Matcher\ToReceive'); + expect($expectation->matcherName())->toBe('toReceive'); + expect($expectation->not())->toBe(false); + expect($expectation->type())->toBe('failed'); + expect($expectation->data())->toBe([ + 'actual received calls' => [], + 'expected to receive' => 'methodName' + ]); + expect($expectation->description())->toBe('receive the expected method.'); + expect($expectation->messages())->toBe(['it runs a spec']); + + }); + + it("logs the not attribute", function () { + + $this->spec = new Specification([ + 'closure' => function () { + $this->expect(true)->not->toBe(true); + } + ]); + + expect($this->spec->passed())->toBe(false); + + $failures = $this->spec->summary()->logs('failed'); + expect($failures)->toHaveLength(1); + + $failure = reset($failures); + $expectation = $failure->children()[0]; + + expect($expectation->not())->toBe(true); + + }); + + it("logs the not attribute with a deferred matcher", function () { + + $this->spec = new Specification([ + 'closure' => function () { + $stub = Double::instance(); + $this->expect($stub)->not->toReceive('methodName'); + $stub->methodName(); + } + ]); + + expect($this->spec->passed())->toBe(false); + + $failures = $this->spec->summary()->logs('failed'); + expect($failures)->toHaveLength(1); + + $failure = reset($failures); + $expectation = $failure->children()[0]; + + expect($expectation->not())->toBe(true); + expect($expectation->not())->toBe(true); + + }); + + it("logs sub spec fails", function () { + + $this->spec = new Specification([ + 'message' => 'runs a spec', + 'closure' => function () { + $this->waitsFor(function () { + $this->expect(true)->toBe(false); + }); + } + ]); + + expect($this->spec->passed())->toBe(false); + + $failured = $this->spec->summary()->logs('failed'); + expect($failured)->toHaveLength(1); + + $failure = reset($failured); + $expectation = $failure->children()[0]; + + expect($expectation->matcher())->toBe('Kahlan\Matcher\ToBe'); + expect($expectation->matcherName())->toBe('toBe'); + expect($expectation->not())->toBe(false); + expect($expectation->type())->toBe('failed'); + expect($expectation->data())->toBe([ + 'actual' => true, + 'expected' => false + ]); + expect($expectation->messages())->toBe(['it runs a spec']); + + }); + + it("logs the first failing spec only", function () { + + $this->spec = new Specification([ + 'message' => 'runs a spec', + 'closure' => function () { + $this->waitsFor(function () { + $this->expect(true)->toBe(false); + return true; + })->toBe(false); + } + ]); + + expect($this->spec->passed())->toBe(false); + + $failured = $this->spec->summary()->logs('failed'); + expect($failured)->toHaveLength(1); + + $failure = reset($failured); + $expectation = $failure->children()[0]; + + expect($expectation->matcher())->toBe('Kahlan\Matcher\ToBe'); + expect($expectation->matcherName())->toBe('toBe'); + expect($expectation->not())->toBe(false); + expect($expectation->type())->toBe('failed'); + expect($expectation->data())->toBe([ + 'actual' => true, + 'expected' => false + ]); + expect($expectation->messages())->toBe(['it runs a spec']); + + }); + + }); + + }); + +}); diff --git a/spec/Suite/SpecificationSpec.php b/spec/Suite/SpecificationSpec.php deleted file mode 100644 index 6967b4d2..00000000 --- a/spec/Suite/SpecificationSpec.php +++ /dev/null @@ -1,380 +0,0 @@ -spec = new Specification(['closure' => function() {}]); - - }); - - describe("->__construct()", function() { - - it("throws an exception with invalid closure", function() { - - $closure = function() { - $this->spec = new Specification(['closure' => null]); - }; - - expect($closure)->toThrow(new Exception('Error, invalid closure.')); - - }); - - }); - - describe("->expect()", function() { - - it("returns the matcher instance", function() { - - $matcher = $this->spec->expect('actual'); - expect($matcher)->toBeAnInstanceOf('Kahlan\Expectation'); - - }); - - }); - - describe("->waitsFor()", function() { - - it("returns the matcher instance setted with the correct timeout", function() { - - $matcher = $this->spec->waitsFor(function(){}, 10); - expect($matcher)->toBeAnInstanceOf('Kahlan\Expectation'); - expect($matcher->timeout())->toBe(10); - - $matcher = $this->spec->waitsFor(function(){}); - expect($matcher)->toBeAnInstanceOf('Kahlan\Expectation'); - expect($matcher->timeout())->toBe(0); - - }); - - }); - - describe("->process()", function() { - - it("returns the closure return value", function() { - - $this->spec = new Specification([ - 'closure' => function() { - return 'hello world'; - } - ]); - - expect($this->spec->process())->toBe('hello world'); - - }); - - context("when the specs passed", function() { - - it("logs a pass", function() { - - $this->spec = new Specification([ - 'message' => 'runs a spec', - 'closure' => function() { - $this->expect(true)->toBe(true); - } - ]); - - expect($this->spec->process())->toBe(null); - expect($this->spec->passed())->toBe(true); - - $passed = $this->spec->results()['passed']; - expect($passed)->toHaveLength(1); - - $pass = reset($passed); - - expect($pass->matcher())->toBe('Kahlan\Matcher\ToBe'); - expect($pass->matcherName())->toBe('toBe'); - expect($pass->not())->toBe(false); - expect($pass->type())->toBe('pass'); - expect($pass->params())->toBe([ - 'actual' => true, - 'expected' => true - ]); - expect($pass->messages())->toBe(['it runs a spec']); - - }); - - it("logs a pass with a deferred matcher", function() { - - $this->spec = new Specification([ - 'message' => 'runs a spec', - 'closure' => function() { - $stub = Stub::create(); - $this->expect($stub)->toReceive('methodName'); - $stub->methodName(); - } - ]); - - expect($this->spec->process())->toBe(null); - expect($this->spec->passed())->toBe(true); - - $passes = $this->spec->results()['passed']; - expect($passes)->toHaveLength(1); - - $pass = reset($passes); - - expect($pass->matcher())->toBe('Kahlan\Matcher\ToReceive'); - expect($pass->matcherName())->toBe('toReceive'); - expect($pass->not())->toBe(false); - expect($pass->type())->toBe('pass'); - expect($pass->params())->toBe([ - 'actual with' => [], - 'expected with' => [] - ]); - expect($pass->messages())->toBe(['it runs a spec']); - - }); - - it("logs the not attribute", function() { - - $this->spec = new Specification([ - 'closure' => function() { - $this->expect(true)->not->toBe(false); - } - ]); - - expect($this->spec->process())->toBe(null); - expect($this->spec->passed())->toBe(true); - - $passes = $this->spec->results()['passed']; - expect($passes)->toHaveLength(1); - - $pass = reset($passes); - - expect($pass->not())->toBe(true); - - }); - - it("logs deferred matcher backtrace", function() { - - $this->spec = new Specification([ - 'closure' => function() { - $this->expect(new stdClass())->not->toReceive('helloWorld'); - } - ]); - - expect($this->spec->process())->toBe(null); - expect($this->spec->passed())->toBe(true); - - $passes = $this->spec->results()['passed']; - expect($passes)->toHaveLength(1); - - $pass = reset($passes); - $backtrace = $pass->backtrace(); - expect($backtrace[0]['file'])->toMatch('~ToReceive.php$~'); - - }); - - it("logs the not attribute with a deferred matcher", function() { - - $this->spec = new Specification([ - 'closure' => function() { - $stub = Stub::create(); - $this->expect($stub)->not->toReceive('methodName'); - } - ]); - - expect($this->spec->process())->toBe(null); - expect($this->spec->passed())->toBe(true); - - $passes = $this->spec->results()['passed']; - expect($passes)->toHaveLength(1); - - $pass = reset($passes); - - expect($pass->not())->toBe(true); - - }); - - it("resets `not` to `false ` after any matcher call", function () { - - expect([]) - ->not->toBeNull() - ->toBeA('array') - ->toBeEmpty(); - - }); - - }); - - context("when the specs failed", function() { - - it("logs a fail", function() { - - $this->spec = new Specification([ - 'message' => 'runs a spec', - 'closure' => function() { - $this->expect(true)->toBe(false); - } - ]); - - expect($this->spec->process())->toBe(null); - expect($this->spec->passed())->toBe(false); - - $failured = $this->spec->results()['failed']; - expect($failured)->toHaveLength(1); - - $failure = reset($failured); - - expect($failure->matcher())->toBe('Kahlan\Matcher\ToBe'); - expect($failure->matcherName())->toBe('toBe'); - expect($failure->not())->toBe(false); - expect($failure->type())->toBe('fail'); - expect($failure->params())->toBe([ - 'actual' => true, - 'expected' => false - ]); - expect($failure->messages())->toBe(['it runs a spec']); - expect($failure->backtrace())->toBeAn('array'); - }); - - it("logs a fail with a deferred matcher", function() { - - $this->spec = new Specification([ - 'message' => 'runs a spec', - 'closure' => function() { - $stub = Stub::create(); - $this->expect($stub)->toReceive('methodName'); - } - ]); - - expect($this->spec->process())->toBe(null); - expect($this->spec->passed())->toBe(false); - - $failured = $this->spec->results()['failed']; - expect($failured)->toHaveLength(1); - - $failure = reset($failured); - - expect($failure->matcher())->toBe('Kahlan\Matcher\ToReceive'); - expect($failure->matcherName())->toBe('toReceive'); - expect($failure->not())->toBe(false); - expect($failure->type())->toBe('fail'); - expect($failure->params())->toBe([ - 'actual received' =>['__construct'], - 'expected' => 'methodName' - ]); - expect($failure->description())->toBe('receive the correct message.'); - expect($failure->messages())->toBe(['it runs a spec']); - expect($failure->backtrace())->toBeAn('array'); - - }); - - it("logs the not attribute", function() { - - $this->spec = new Specification([ - 'closure' => function() { - $this->expect(true)->not->toBe(true); - } - ]); - - expect($this->spec->process())->toBe(null); - expect($this->spec->passed())->toBe(false); - - $failures = $this->spec->results()['failed']; - expect($failures)->toHaveLength(1); - - $failure = reset($failures); - - expect($failure->not())->toBe(true); - - }); - - it("logs the not attribute with a deferred matcher", function() { - - $this->spec = new Specification([ - 'closure' => function() { - $stub = Stub::create(); - $this->expect($stub)->not->toReceive('methodName'); - $stub->methodName(); - } - ]); - - expect($this->spec->process())->toBe(null); - expect($this->spec->passed())->toBe(false); - - $failures = $this->spec->results()['failed']; - expect($failures)->toHaveLength(1); - - $failure = reset($failures); - - expect($failure->not())->toBe(true); - expect($failure->not())->toBe(true); - - }); - - it("logs sub spec fails", function() { - - $this->spec = new Specification([ - 'message' => 'runs a spec', - 'closure' => function() { - $this->waitsFor(function(){ - $this->expect(true)->toBe(false); - }); - } - ]); - - expect($this->spec->process())->toBe(null); - expect($this->spec->passed())->toBe(false); - - $failured = $this->spec->results()['failed']; - expect($failured)->toHaveLength(1); - - $failure = reset($failured); - - expect($failure->matcher())->toBe('Kahlan\Matcher\ToBe'); - expect($failure->matcherName())->toBe('toBe'); - expect($failure->not())->toBe(false); - expect($failure->type())->toBe('fail'); - expect($failure->params())->toBe([ - 'actual' => true, - 'expected' => false - ]); - expect($failure->messages())->toBe(['it runs a spec']); - expect($failure->backtrace())->toBeAn('array'); - }); - - it("logs the first failing spec only", function() { - - $this->spec = new Specification([ - 'message' => 'runs a spec', - 'closure' => function() { - $this->waitsFor(function(){ - $this->expect(true)->toBe(false); - return true; - })->toBe(false); - } - ]); - - expect($this->spec->process())->toBe(null); - expect($this->spec->passed())->toBe(false); - - $failured = $this->spec->results()['failed']; - expect($failured)->toHaveLength(1); - - $failure = reset($failured); - - expect($failure->matcher())->toBe('Kahlan\Matcher\ToBe'); - expect($failure->matcherName())->toBe('toBe'); - expect($failure->not())->toBe(false); - expect($failure->type())->toBe('fail'); - expect($failure->params())->toBe([ - 'actual' => true, - 'expected' => false - ]); - expect($failure->messages())->toBe(['it runs a spec']); - expect($failure->backtrace())->toBeAn('array'); - }); - - }); - - }); - -}); \ No newline at end of file diff --git a/spec/Suite/SuiteSpec.php b/spec/Suite/Suite.spec.php similarity index 50% rename from spec/Suite/SuiteSpec.php rename to spec/Suite/Suite.spec.php index 28781fae..21106b38 100644 --- a/spec/Suite/SuiteSpec.php +++ b/spec/Suite/Suite.spec.php @@ -5,36 +5,52 @@ use Exception; use InvalidArgumentException; -use Kahlan\IncompleteException; +use Kahlan\MissingImplementationException; use Kahlan\PhpErrorException; use Kahlan\Suite; use Kahlan\Matcher; +use Kahlan\Reporters; use Kahlan\Arg; -use Kahlan\Plugin\Stub; +use Kahlan\Plugin\Double; -describe("Suite", function() { +describe("Suite", function () { - beforeEach(function() { + beforeEach(function () { $this->suite = new Suite(['matcher' => new Matcher()]); + $this->reporters = new Reporters(); }); - context("when inspecting flow", function() { + describe("->__construct()", function () { - describe("->before()", function() { + it("throws an exception with invalid closure", function () { + $closure = function () { + $this->suite = new Suite([ + 'closure' => null, + 'parent' => new Suite() + ]); + }; + expect($closure)->toThrow(new Exception('Error, invalid closure.')); + }); + + }); + + context("when inspecting flow", function () { + + describe("->beforeAll()", function () { $this->nb = 0; - before(function() { + beforeAll(function () { $this->nb++; }); - it("passes if `before` has been executed", function() use (&$nb) { + it("passes if `before` has been executed", function () use (&$nb) { expect($this->nb)->toBe(1); }); - it("passes if `before` has not been executed twice", function() use (&$nb) { + it("passes if `before` has not been executed twice", function () use (&$nb) { expect($this->nb)->toBe(1); @@ -42,29 +58,29 @@ }); - describe("->beforeEach()", function() { + describe("->beforeEach()", function () { $this->nb = 0; - beforeEach(function() { + beforeEach(function () { $this->nb++; }); - it("passes if `beforeEach` has been executed", function() { + it("passes if `beforeEach` has been executed", function () { expect($this->nb)->toBe(1); }); - it("passes if `beforeEach` has been executed twice", function() { + it("passes if `beforeEach` has been executed twice", function () { expect($this->nb)->toBe(2); }); - context("with sub scope", function() { + context("with sub scope", function () { - it("passes if `beforeEach` has been executed once more", function() { + it("passes if `beforeEach` has been executed once more", function () { expect($this->nb)->toBe(3); @@ -72,7 +88,7 @@ }); - it("passes if `beforeEach` has been executed once more", function() { + it("passes if `beforeEach` has been executed once more", function () { expect($this->nb)->toBe(4); @@ -80,15 +96,15 @@ }); - describe("->after()", function() { + describe("->afterAll()", function () { $this->nb = 0; - after(function() { + afterAll(function () { $this->nb++; }); - it("passes if `after` has not been executed", function() { + it("passes if `after` has not been executed", function () { expect($this->nb)->toBe(0); @@ -96,29 +112,29 @@ }); - describe("->afterEach()", function() { + describe("->afterEach()", function () { $this->nb = 0; - afterEach(function() { + afterEach(function () { $this->nb++; }); - it("passes if `afterEach` has not been executed", function() { + it("passes if `afterEach` has not been executed", function () { expect($this->nb)->toBe(0); }); - it("passes if `afterEach` has been executed", function() { + it("passes if `afterEach` has been executed", function () { expect($this->nb)->toBe(1); }); - context("with sub scope", function() { + context("with sub scope", function () { - it("passes if `afterEach` has been executed once more", function() { + it("passes if `afterEach` has been executed once more", function () { expect($this->nb)->toBe(2); @@ -126,7 +142,7 @@ }); - it("passes if `afterEach` has been executed once more", function() { + it("passes if `afterEach` has been executed once more", function () { expect($this->nb)->toBe(3); @@ -136,45 +152,45 @@ }); - describe("->describe()", function() { + describe("->describe()", function () { - it("creates a sub suite of specs inside the root suite", function() { + it("creates a sub suite of specs inside the root suite", function () { - $suite = $this->suite->describe("->method()", function() {}); + $suite = $this->suite->describe("->method()", function () {}); expect($suite->message())->toBe('->method()'); expect($suite->parent())->toBe($this->suite); - $suites = $this->suite->childs(); + $suites = $this->suite->children(); expect($suite)->toBe(end($suites)); }); }); - describe("->context()", function() { + describe("->context()", function () { - it("creates a contextualized suite of specs inside the root suite", function() { + it("creates a contextualized suite of specs inside the root suite", function () { - $suite = $this->suite->context("->method()", function() {}); + $suite = $this->suite->context("->method()", function () {}); expect($suite->message())->toBe('->method()'); expect($suite->parent())->toBe($this->suite); - $suites = $this->suite->childs(); + $suites = $this->suite->children(); expect($suite)->toBe(end($suites)); }); }); - describe("->it()", function() { + describe("->it()", function () { - it("creates a spec", function() { + it("creates a spec", function () { - $this->suite->it("does some things", function() {}); + $this->suite->it("does some things", function () {}); - $specs = $this->suite->childs(); + $specs = $this->suite->children(); $it = end($specs); expect($it->message())->toBe('it does some things'); @@ -182,11 +198,11 @@ }); - it("creates a spec with a random message if not set", function() { + it("creates a spec with a random message if not set", function () { - $this->suite->it(function() {}); + $this->suite->it(function () {}); - $specs = $this->suite->childs(); + $specs = $this->suite->children(); $it = end($specs); expect($it->message())->toMatch('~^it spec #[0-9]+$~'); @@ -195,44 +211,44 @@ }); - describe("->before()", function() { + describe("->beforeAll()", function () { - it("creates a before callback", function() { + it("creates a before callback", function () { - $callbacks = $this->suite->callbacks('before'); + $callbacks = $this->suite->callbacks('beforeAll'); expect($callbacks)->toHaveLength(0); - $this->suite->before(function() {}); - $callbacks = $this->suite->callbacks('before'); + $this->suite->beforeAll(function () {}); + $callbacks = $this->suite->callbacks('beforeAll'); expect($callbacks)->toHaveLength(1); }); }); - describe("->after()", function() { + describe("->afterAll()", function () { - it("creates a before callback", function() { + it("creates a before callback", function () { - $callbacks = $this->suite->callbacks('after'); + $callbacks = $this->suite->callbacks('afterAll'); expect($callbacks)->toHaveLength(0); - $this->suite->after(function() {}); - $callbacks = $this->suite->callbacks('after'); + $this->suite->afterAll(function () {}); + $callbacks = $this->suite->callbacks('afterAll'); expect($callbacks)->toHaveLength(1); }); }); - describe("->beforeEach()", function() { + describe("->beforeEach()", function () { - it("creates a beforeEach callback", function() { + it("creates a beforeEach callback", function () { $callbacks = $this->suite->callbacks('beforeEach'); expect($callbacks)->toHaveLength(0); - $this->suite->beforeEach(function() {}); + $this->suite->beforeEach(function () {}); $callbacks = $this->suite->callbacks('beforeEach'); expect($callbacks)->toHaveLength(1); @@ -240,14 +256,14 @@ }); - describe("->afterEach()", function() { + describe("->afterEach()", function () { - it("creates a before callback", function() { + it("creates a before callback", function () { $callbacks = $this->suite->callbacks('afterEach'); expect($callbacks)->toHaveLength(0); - $this->suite->afterEach(function() {}); + $this->suite->afterEach(function () {}); $callbacks = $this->suite->callbacks('afterEach'); expect($callbacks)->toHaveLength(1); @@ -255,27 +271,27 @@ }); - describe("->total()/->enabled()", function() { + describe("->total()/->enabled()", function () { - it("return the total/enabled number of specs", function() { + it("return the total/enabled number of specs", function () { - $describe = $this->suite->describe("", function() { + $describe = $this->suite->describe("", function () { $this->exectuted = ['it' => 0, 'fit' => 0]; - $this->describe("fdescribe", function() { + $this->describe("fdescribe", function () { - $this->it("it", function() { + $this->it("it", function () { $this->exectuted['it']++; }); - $this->describe("describe", function() { + $this->describe("describe", function () { - $this->fit("fit", function() { + $this->fit("fit", function () { $this->exectuted['fit']++; }); - $this->it("it", function() { + $this->it("it", function () { $this->exectuted['it']++; }); @@ -292,21 +308,29 @@ }); - describe("->fdescribe()", function() { + describe("->fdescribe()", function () { - it("executes only the `it` in focused mode", function() { + it("executes only the `it` in focused mode", function () { - $describe = $this->suite->describe("", function() { + $describe = $this->suite->describe("", function () { $this->exectuted = ['it' => 0, 'fit' => 0]; - $this->fdescribe("->fdescribe()", function() { + $this->describe("->describe()", function () { - $this->fit("assumes fit due to the parent", function() { + $this->it("it", function () { + $this->exectuted['it']++; + }); + + }); + + $this->fdescribe("->fdescribe()", function () { + + $this->fit("fit", function () { $this->exectuted['fit']++; }); - $this->it("assumes fit due to the parent", function() { + $this->it("it", function () { $this->exectuted['it']++; }); @@ -314,10 +338,10 @@ }); - $this->suite->run(); + $this->suite->run(['reporters' => $this->reporters]); expect($describe->exectuted)->toEqual(['it' => 0, 'fit' => 1]); - expect($this->suite->total())->toBe(2); + expect($this->suite->total())->toBe(3); expect($this->suite->enabled())->toBe(1); expect($this->suite->focused())->toBe(true); expect($this->suite->status())->toBe(-1); @@ -325,19 +349,19 @@ }); - it("executes all `it` in focused mode if no one is focused", function() { + it("executes all `it` in focused mode if no one is focused", function () { - $describe = $this->suite->describe("", function() { + $describe = $this->suite->describe("", function () { $this->exectuted = ['it' => 0, 'fit' => 0]; - $this->fdescribe("->fdescribe()", function() { + $this->fdescribe("->fdescribe()", function () { - $this->it("assumes fit due to the parent", function() { + $this->it("assumes fit due to the parent", function () { $this->exectuted['fit']++; }); - $this->it("assumes fit due to the parent", function() { + $this->it("assumes fit due to the parent", function () { $this->exectuted['fit']++; }); @@ -345,7 +369,7 @@ }); - $this->suite->run(); + $this->suite->run(['reporters' => $this->reporters]); expect($describe->exectuted)->toEqual(['it' => 0, 'fit' => 2]); expect($this->suite->total())->toBe(2); @@ -356,29 +380,29 @@ }); - it("executes all `it` in focused mode if no one is focused in a nested way", function() { + it("executes all `it` in focused mode if no one is focused in a nested way", function () { - $describe = $this->suite->describe("", function() { + $describe = $this->suite->describe("", function () { $this->exectuted = ['it' => 0, 'fit' => 0]; - $this->fdescribe("->fdescribe()", function() { + $this->fdescribe("->fdescribe()", function () { - $this->it("assumes fit due to the parent", function() { + $this->it("assumes fit due to the parent", function () { $this->exectuted['fit']++; }); - $this->it("assumes fit due to the parent", function() { + $this->it("assumes fit due to the parent", function () { $this->exectuted['fit']++; }); - $this->describe("->describe()", function() { + $this->describe("->describe()", function () { - $this->it("assumes fit due to the parent", function() { + $this->it("assumes fit due to the parent", function () { $this->exectuted['fit']++; }); - $this->it("assumes fit due to the parent", function() { + $this->it("assumes fit due to the parent", function () { $this->exectuted['fit']++; }); @@ -388,7 +412,7 @@ }); - $this->suite->run(); + $this->suite->run(['reporters' => $this->reporters]); expect($describe->exectuted)->toEqual(['it' => 0, 'fit' => 4]); expect($this->suite->total())->toBe(4); @@ -401,21 +425,21 @@ }); - describe("->fcontext()", function() { + describe("->fcontext()", function () { - it("executes only the `it` in focused mode", function() { + it("executes only the `it` in focused mode", function () { - $context = $this->suite->context("", function() { + $context = $this->suite->context("", function () { $this->exectuted = ['it' => 0, 'fit' => 0]; - $this->fcontext("->fcontext()", function() { + $this->fcontext("->fcontext()", function () { - $this->fit("assumes fit due to the parent", function() { + $this->fit("assumes fit due to the parent", function () { $this->exectuted['fit']++; }); - $this->it("assumes fit due to the parent", function() { + $this->it("assumes fit due to the parent", function () { $this->exectuted['it']++; }); @@ -423,7 +447,7 @@ }); - $this->suite->run(); + $this->suite->run(['reporters' => $this->reporters]); expect($context->exectuted)->toEqual(['it' => 0, 'fit' => 1]); expect($this->suite->total())->toBe(2); @@ -434,19 +458,19 @@ }); - it("executes all `it` in focused mode if no one is focused", function() { + it("executes all `it` in focused mode if no one is focused", function () { - $context = $this->suite->context("", function() { + $context = $this->suite->context("", function () { $this->exectuted = ['it' => 0, 'fit' => 0]; - $this->fcontext("->fcontext()", function() { + $this->fcontext("->fcontext()", function () { - $this->it("assumes fit due to the parent", function() { + $this->it("assumes fit due to the parent", function () { $this->exectuted['fit']++; }); - $this->it("assumes fit due to the parent", function() { + $this->it("assumes fit due to the parent", function () { $this->exectuted['fit']++; }); @@ -454,7 +478,7 @@ }); - $this->suite->run(); + $this->suite->run(['reporters' => $this->reporters]); expect($context->exectuted)->toEqual(['it' => 0, 'fit' => 2]); expect($this->suite->total())->toBe(2); @@ -465,29 +489,29 @@ }); - it("executes all `it` in focused mode if no one is focused in a nested way", function() { + it("executes all `it` in focused mode if no one is focused in a nested way", function () { - $context = $this->suite->context("", function() { + $context = $this->suite->context("", function () { $this->exectuted = ['it' => 0, 'fit' => 0]; - $this->fcontext("->fcontext()", function() { + $this->fcontext("->fcontext()", function () { - $this->it("assumes fit due to the parent", function() { + $this->it("assumes fit due to the parent", function () { $this->exectuted['fit']++; }); - $this->it("assumes fit due to the parent", function() { + $this->it("assumes fit due to the parent", function () { $this->exectuted['fit']++; }); - $this->context("->context()", function() { + $this->context("->context()", function () { - $this->it("assumes fit due to the parent", function() { + $this->it("assumes fit due to the parent", function () { $this->exectuted['fit']++; }); - $this->it("assumes fit due to the parent", function() { + $this->it("assumes fit due to the parent", function () { $this->exectuted['fit']++; }); @@ -497,7 +521,7 @@ }); - $this->suite->run(); + $this->suite->run(['reporters' => $this->reporters]); expect($context->exectuted)->toEqual(['it' => 0, 'fit' => 4]); expect($this->suite->total())->toBe(4); @@ -510,33 +534,33 @@ }); - describe("->fit()", function() { + describe("->fit()", function () { - it("executes only the focused `it`", function() { + it("executes only the focused `it`", function () { - $describe = $this->suite->describe("", function() { + $describe = $this->suite->describe("", function () { $this->exectuted = ['it' => 0, 'fit' => 0]; - $this->it("an it", function() { + $this->it("an it", function () { $this->exectuted['it']++; }); - $this->fit("an fit", function() { + $this->fit("an fit", function () { $this->exectuted['fit']++; }); - $this->it("an it", function() { + $this->it("an it", function () { $this->exectuted['it']++; }); - $this->fit("an fit", function() { + $this->fit("an fit", function () { $this->exectuted['fit']++; }); }); - $this->suite->run(); + $this->suite->run(['reporters' => $this->reporters]); expect($describe->exectuted)->toEqual(['it' => 0, 'fit' => 2]); expect($this->suite->total())->toBe(4); @@ -547,21 +571,21 @@ }); - it("propagates the exclusivity up to parents", function() { + it("propagates the exclusivity up to parents", function () { - $describe = $this->suite->describe("", function() { + $describe = $this->suite->describe("", function () { $this->exectuted = ['it' => 0, 'fit' => 0]; - $this->fdescribe("fdescribe", function() { + $this->fdescribe("fdescribe", function () { - $this->describe("describe", function() { + $this->describe("describe", function () { - $this->it("it", function() { + $this->it("it", function () { $this->exectuted['it']++; }); - $this->fit("fit", function() { + $this->fit("fit", function () { $this->exectuted['fit']++; }); @@ -571,7 +595,7 @@ }); - $this->suite->run(); + $this->suite->run(['reporters' => $this->reporters]); expect($describe->exectuted)->toEqual(['it' => 0, 'fit' => 1]); expect($this->suite->total())->toBe(2); @@ -582,25 +606,25 @@ }); - it("propagates the exclusivity up to parents", function() { + it("propagates the exclusivity up to parents", function () { - $describe = $this->suite->describe("", function() { + $describe = $this->suite->describe("", function () { $this->exectuted = ['it' => 0, 'fit' => 0]; - $this->describe("fdescribe", function() { + $this->describe("fdescribe", function () { - $this->it("it", function() { + $this->it("it", function () { $this->exectuted['it']++; }); - $this->describe("describe", function() { + $this->describe("describe", function () { - $this->fit("fit", function() { + $this->fit("fit", function () { $this->exectuted['fit']++; }); - $this->it("it", function() { + $this->it("it", function () { $this->exectuted['it']++; }); @@ -610,7 +634,7 @@ }); - $this->suite->run(); + $this->suite->run(['reporters' => $this->reporters]); expect($describe->exectuted)->toEqual(['it' => 0, 'fit' => 1]); expect($this->suite->total())->toBe(3); @@ -623,27 +647,27 @@ }); - describe("->focused()", function() { + describe("->focused()", function () { - it("returns the references of runned focused specs", function() { + it("returns the references of runned focused specs", function () { - $describe = $this->suite->describe("focused suite", function() { + $describe = $this->suite->describe("focused suite", function () { $this->exectuted = ['it' => 0, 'fit' => 0]; - $this->it("an it", function() { + $this->it("an it", function () { $this->exectuted['it']++; }); - $this->fit("an fit", function() { + $this->fit("an fit", function () { $this->exectuted['fit']++; }); - $this->it("an it", function() { + $this->it("an it", function () { $this->exectuted['it']++; }); - $this->fit("an fit", function() { + $this->fit("an fit", function () { $this->exectuted['fit']++; }); @@ -651,46 +675,155 @@ $this->suite->run(); - expect($this->suite->focuses())->toHaveLength(2); + expect($this->suite->summary()->get('focused'))->toHaveLength(2); + + }); + + }); + + describe("->xdecribe()", function () { + + it("propagates the exclusion down to children", function () { + + $describe = $this->suite->describe("", function () { + + $this->exectuted = ['it' => 0]; + + $this->it("it1", function () { + $this->exectuted['it']++; + }); + + $this->xdescribe("xdescribe", function () { + + $this->it("it2", function () { + $this->exectuted['it']++; + }); + + $this->it("it3", function () { + $this->exectuted['it']++; + }); + + }); + + }); + + $this->suite->run(); + + expect($describe->exectuted)->toEqual(['it' => 1]); + expect($this->suite->total())->toBe(3); + expect($this->suite->enabled())->toBe(1); + expect($this->suite->status())->toBe(0); + expect($this->suite->passed())->toBe(true); }); }); - describe("skipIf", function() { + describe("->xcontext()", function () { - it("skips specs in a before", function() { + it("propagates the exclusion down to children", function () { - $describe = $this->suite->describe("skip suite", function() { + $describe = $this->suite->describe("", function () { $this->exectuted = ['it' => 0]; - before(function() { + $this->it("it1", function () { + $this->exectuted['it']++; + }); + + $this->xcontext("xcontext", function () { + + $this->it("it2", function () { + $this->exectuted['it']++; + }); + + $this->it("it3", function () { + $this->exectuted['it']++; + }); + + }); + + }); + + $this->suite->run(); + + expect($describe->exectuted)->toEqual(['it' => 1]); + expect($this->suite->total())->toBe(3); + expect($this->suite->enabled())->toBe(1); + expect($this->suite->status())->toBe(0); + expect($this->suite->passed())->toBe(true); + + }); + + }); + + describe("->xit()", function () { + + it("skips excluded `it`", function () { + + $describe = $this->suite->describe("", function () { + + $this->exectuted = ['it' => 0]; + + $this->it("an it", function () { + $this->exectuted['it']++; + }); + + $this->xit("an xit", function () { + $this->exectuted['it']++; + }); + + $this->it("an it", function () { + $this->exectuted['it']++; + }); + + }); + + $this->suite->run(); + + expect($describe->exectuted)->toEqual(['it' => 2]); + expect($this->suite->total())->toBe(3); + expect($this->suite->enabled())->toBe(2); + expect($describe->children()[1]->excluded())->toBe(true); + expect($this->suite->status())->toBe(0); + expect($this->suite->passed())->toBe(true); + + }); + + }); + + describe("skipIf", function () { + + it("skips specs in a before", function () { + + $describe = $this->suite->describe("skip suite", function () { + + $this->exectuted = ['it' => 0]; + + beforeAll(function () { skipIf(true); }); - $this->it("an it", function() { + $this->it("an it", function () { $this->exectuted['it']++; }); - $this->it("an it", function() { + $this->it("an it", function () { $this->exectuted['it']++; }); }); - $reporters = Stub::create(); + $reporters = Double::instance(); - expect($reporters)->toReceive('process')->with('start', ['total' => 2]); - expect($reporters)->toReceiveNext('process')->with('suiteStart', Arg::toBeAnInstanceOf('Kahlan\Report')); - expect($reporters)->toReceiveNext('process')->with('specStart', Arg::toBeAnInstanceOf('Kahlan\Report')); - expect($reporters)->toReceiveNext('process')->with('skip', Arg::toBeAnInstanceOf('Kahlan\Report')); - expect($reporters)->toReceiveNext('process')->with('specEnd', Arg::toBeAnInstanceOf('Kahlan\Report')); - expect($reporters)->toReceiveNext('process')->with('specStart', Arg::toBeAnInstanceOf('Kahlan\Report')); - expect($reporters)->toReceiveNext('process')->with('skip', Arg::toBeAnInstanceOf('Kahlan\Report')); - expect($reporters)->toReceiveNext('process')->with('specEnd', Arg::toBeAnInstanceOf('Kahlan\Report')); - expect($reporters)->toReceiveNext('process')->with('suiteEnd', Arg::toBeAnInstanceOf('Kahlan\Report')); - expect($reporters)->toReceiveNext('process')->with('end', Arg::toBeAn('array')); + expect($reporters)->toReceive('dispatch')->with('start', ['total' => 2])->ordered; + expect($reporters)->toReceive('dispatch')->with('suiteStart', $describe)->ordered; + expect($reporters)->toReceive('dispatch')->with('specStart', Arg::toBeAnInstanceOf('Kahlan\Specification'))->ordered; + expect($reporters)->toReceive('dispatch')->with('specEnd', Arg::toBeAnInstanceOf('Kahlan\Log'))->ordered; + expect($reporters)->toReceive('dispatch')->with('specStart', Arg::toBeAnInstanceOf('Kahlan\Specification'))->ordered; + expect($reporters)->toReceive('dispatch')->with('specEnd', Arg::toBeAnInstanceOf('Kahlan\Log'))->ordered; + expect($reporters)->toReceive('dispatch')->with('suiteEnd', $describe)->ordered; + expect($reporters)->toReceive('dispatch')->with('end', Arg::toBeAnInstanceOf('Kahlan\Summary'))->ordered; $this->suite->run(['reporters' => $reporters]); @@ -701,37 +834,35 @@ }); - it("skips specs in a beforeEach", function() { + it("skips specs in a beforeEach", function () { - $describe = $this->suite->describe("skip suite", function() { + $describe = $this->suite->describe("skip suite", function () { $this->exectuted = ['it' => 0]; - beforeEach(function() { + beforeEach(function () { skipIf(true); }); - $this->it("an it", function() { + $this->it("an it", function () { $this->exectuted['it']++; }); - $this->it("an it", function() { + $this->it("an it", function () { $this->exectuted['it']++; }); }); - $reporters = Stub::create(); + $reporters = Double::instance(); - expect($reporters)->toReceive('process')->with('start', ['total' => 2]); - expect($reporters)->toReceiveNext('process')->with('suiteStart', Arg::toBeAnInstanceOf('Kahlan\Report')); - expect($reporters)->toReceiveNext('process')->with('specStart', Arg::toBeAnInstanceOf('Kahlan\Report')); - expect($reporters)->toReceiveNext('process')->with('skip', Arg::toBeAnInstanceOf('Kahlan\Report')); - expect($reporters)->toReceiveNext('process')->with('specEnd', Arg::toBeAnInstanceOf('Kahlan\Report')); - expect($reporters)->toReceiveNext('process')->with('specStart', Arg::toBeAnInstanceOf('Kahlan\Report')); - expect($reporters)->toReceiveNext('process')->with('skip', Arg::toBeAnInstanceOf('Kahlan\Report')); - expect($reporters)->toReceiveNext('process')->with('suiteEnd', Arg::toBeAnInstanceOf('Kahlan\Report')); - expect($reporters)->toReceiveNext('process')->with('end', Arg::toBeAn('array')); + expect($reporters)->toReceive('dispatch')->with('start', ['total' => 2])->ordered; + expect($reporters)->toReceive('dispatch')->with('suiteStart', $describe)->ordered; + expect($reporters)->toReceive('dispatch')->with('specStart', Arg::toBeAnInstanceOf('Kahlan\Specification'))->ordered; + expect($reporters)->toReceive('dispatch')->with('specEnd', Arg::toBeAnInstanceOf('Kahlan\Log'))->ordered; + expect($reporters)->toReceive('dispatch')->with('specStart', Arg::toBeAnInstanceOf('Kahlan\Specification'))->ordered; + expect($reporters)->toReceive('dispatch')->with('suiteEnd', $describe)->ordered; + expect($reporters)->toReceive('dispatch')->with('end', Arg::toBeAnInstanceOf('Kahlan\Summary'))->ordered; $this->suite->run(['reporters' => $reporters]); @@ -744,9 +875,9 @@ }); - describe("::hash()", function() { + describe("::hash()", function () { - it("creates an hash from objects", function() { + it("creates an hash from objects", function () { $instance = new stdClass(); @@ -759,7 +890,7 @@ }); - it("creates an hash from class names", function() { + it("creates an hash from class names", function () { $class = 'hello\world\class'; $hash = Suite::hash($class); @@ -767,9 +898,9 @@ }); - it("Throws an exception if values are not string or objects", function() { + it("Throws an exception if values are not string or objects", function () { - $closure = function() { + $closure = function () { $hash = Suite::hash([]); }; @@ -779,9 +910,9 @@ }); - describe("::register()", function() { + describe("::register()", function () { - it("registers an hash", function() { + it("registers an hash", function () { $instance = new stdClass(); @@ -794,9 +925,9 @@ }); - describe("::register()", function() { + describe("::register()", function () { - it("return `false` if the hash is not registered", function() { + it("return `false` if the hash is not registered", function () { $instance = new stdClass(); @@ -808,9 +939,9 @@ }); - describe("::reset()", function() { + describe("::reset()", function () { - it("clears registered hashes", function() { + it("clears registered hashes", function () { $instance = new stdClass(); @@ -827,12 +958,12 @@ }); - describe("->status()", function() { + describe("->status()", function () { - it("returns `0` if a specs suite passes", function() { + it("returns `0` if a specs suite passes", function () { - $describe = $this->suite->describe("", function() { - $this->it("passes", function() { + $describe = $this->suite->describe("", function () { + $this->it("passes", function () { $this->expect(true)->toBe(true); }); }); @@ -842,10 +973,10 @@ }); - it("returns `-1` if a specs suite fails", function() { + it("returns `-1` if a specs suite fails", function () { - $describe = $this->suite->describe("", function() { - $this->it("fails", function() { + $describe = $this->suite->describe("", function () { + $this->it("fails", function () { $this->expect(true)->toBe(false); }); }); @@ -855,10 +986,10 @@ }); - it("forces a specified return status", function() { + it("forces a specified return status", function () { - $describe = $this->suite->describe("", function() { - $this->it("passes", function() { + $describe = $this->suite->describe("", function () { + $this->it("passes", function () { $this->expect(true)->toBe(true); }); }); @@ -873,22 +1004,38 @@ }); - describe("->run()", function() { + describe("->run()", function () { - it("calls `afterX` callbacks if an exception occurs during callbacks", function() { + it("run the suite", function () { - $describe = $this->suite->describe("", function() { + $describe = $this->suite->describe("", function () { + + $this->it("runs a spec", function () { + $this->expect(true)->toBe(true); + }); + + }); + + $this->suite->run(); + expect($this->suite->status())->toBe(0); + expect($this->suite->passed())->toBe(true); + + }); + + it("calls `afterEach` callbacks if an exception occurs during callbacks", function () { + + $describe = $this->suite->describe("", function () { $this->inAfterEach = 0; - $this->beforeEach(function() { + $this->beforeEach(function () { throw new Exception('Breaking the flow should execute afterEach anyway.'); }); - $this->it("does nothing", function() { + $this->it("does nothing", function () { }); - $this->afterEach(function() { + $this->afterEach(function () { $this->inAfterEach++; }); @@ -898,85 +1045,108 @@ expect($describe->inAfterEach)->toBe(1); - $results = $this->suite->results(); - expect($results['exceptions'])->toHaveLength(1); + $results = $this->suite->summary()->logs('errored'); + expect($results)->toHaveLength(1); - $report = reset($results['exceptions']); + $report = reset($results); $actual = $report->exception()->getMessage(); expect($actual)->toBe('Breaking the flow should execute afterEach anyway.'); + expect($this->suite->status())->toBe(-1); + expect($this->suite->passed())->toBe(false); + }); - it("logs `IncompleteException` when thrown", function() { + it("logs error if an exception is occuring during an `afterEach` callbacks", function () { - $incomplete = new IncompleteException(); + $describe = $this->suite->describe("", function () { - $describe = $this->suite->describe("", function() use ($incomplete) { + $this->it("does nothing", function () { + }); - $this->it("throws an `IncompleteException`", function() use ($incomplete) { - throw $incomplete; + $this->afterEach(function () { + throw new Exception('Errors occured in afterEach should be logged anyway.'); }); }); $this->suite->run(); - $results = $this->suite->results(); - expect($results['incomplete'])->toHaveLength(1); + $results = $this->suite->summary()->logs('errored'); - $report = reset($results['incomplete']); - expect($report->exception())->toBe($incomplete); - expect($report->type())->toBe('incomplete'); - expect($report->messages())->toBe(['', '', 'it throws an `IncompleteException`']); + expect($results)->toHaveLength(1); + + $report = reset($results); + $actual = $report->exception()->getMessage(); + expect($actual)->toBe('Errors occured in afterEach should be logged anyway.'); + + expect($this->suite->status())->toBe(-1); + expect($this->suite->passed())->toBe(false); }); - it("throws and exception if attempts to call the `run()` function inside a scope", function() { + it("logs `MissingImplementationException` when thrown", function () { + + $missing = new MissingImplementationException(); - $closure = function() { - $describe = $this->suite->describe("", function() { - $this->run(); + $describe = $this->suite->describe("", function () use ($missing) { + + $this->it("throws an `MissingImplementationException`", function () use ($missing) { + throw $missing; }); - $this->suite->run(); - }; - expect($closure)->toThrow(new Exception('Method not allowed in this context.')); + }); - }); + $this->suite->run(); - it("throws and exception if attempts to call the `process()` function inside a scope", function() { + $results = $this->suite->summary()->logs('errored'); + expect($results)->toHaveLength(1); - $describe = $this->suite->describe("", function() { + $report = reset($results); + expect($report->exception())->toBe($missing); + expect($report->type())->toBe('errored'); + expect($report->messages())->toBe(['', '', 'it throws an `MissingImplementationException`']); - $this->it("attempts to call the `process()` function", function() { - $this->process(); - }); + expect($this->suite->status())->toBe(-1); + expect($this->suite->passed())->toBe(false); - }); + }); + + it("throws and exception if attempts to call the `run()` function inside a scope", function () { + + skipIf(PHP_MAJOR_VERSION < 7); + $describe = $this->suite->describe("", function () { + $this->run(); + }); $this->suite->run(); - $results = $this->suite->results(); - expect($results['exceptions'])->toHaveLength(1); - $report = reset($results['exceptions']); - $actual = $report->exception()->getMessage(); - expect($actual)->toBe('Method not allowed in this context.'); + $results = $this->suite->summary()->logs('errored'); + expect($results)->toHaveLength(1); + + $report = reset($results); + expect($report->exception()->getMessage())->toBe('Method not allowed in this context.'); + expect($report->type())->toBe('errored'); + expect($report->messages())->toBe(['', '']); + + expect($this->suite->status())->toBe(-1); + expect($this->suite->passed())->toBe(false); }); - it("fails fast", function() { + it("fails fast", function () { - $describe = $this->suite->describe("", function() { + $describe = $this->suite->describe("", function () { - $this->it("fails1", function() { + $this->it("fails1", function () { $this->expect(true)->toBe(false); }); - $this->it("fails2", function() { + $this->it("fails2", function () { $this->expect(true)->toBe(false); }); - $this->it("fails3", function() { + $this->it("fails3", function () { $this->expect(true)->toBe(false); }); @@ -984,8 +1154,7 @@ $this->suite->run(['ff' => 1]); - $results = $this->suite->results(); - $failed = $results['failed']; + $failed = $this->suite->summary()->logs('failed'); expect($failed)->toHaveLength(1); expect($this->suite->focused())->toBe(false); @@ -994,19 +1163,19 @@ }); - it("fails after two failures", function() { + it("fails after two failures", function () { - $describe = $this->suite->describe("", function() { + $describe = $this->suite->describe("", function () { - $this->it("fails1", function() { + $this->it("fails1", function () { $this->expect(true)->toBe(false); }); - $this->it("fails2", function() { + $this->it("fails2", function () { $this->expect(true)->toBe(false); }); - $this->it("fails3", function() { + $this->it("fails3", function () { $this->expect(true)->toBe(false); }); @@ -1014,8 +1183,7 @@ $this->suite->run(['ff' => 2]); - $results = $this->suite->results(); - $failed = $results['failed']; + $failed = $this->suite->summary()->logs('failed'); expect($failed)->toHaveLength(2); expect($this->suite->focused())->toBe(false); @@ -1026,34 +1194,34 @@ }); - describe("->_errorHandler()", function() { + describe("->_errorHandler()", function () { - it("converts E_NOTICE error to an exception", function() { + it("converts E_NOTICE error to an exception", function () { - $closure = function() { + $closure = function () { $a = $b; }; expect($closure)->toThrow(new PhpErrorException("`E_NOTICE` Undefined variable: b")); }); - it("converts E_WARNING error to an exception", function() { + it("converts E_WARNING error to an exception", function () { - $closure = function() { + $closure = function () { $a = array_merge(); }; expect($closure)->toThrow(new PhpErrorException("`E_WARNING` array_merge() expects at least 1 parameter, 0 given")); }); - it("uses default error reporting settings", function() { + it("uses default error reporting settings", function () { - $describe = $this->suite->describe("", function() { + $describe = $this->suite->describe("", function () { - $this->describe("->_errorHandler()", function() { + $this->describe("->_errorHandler()", function () { - $this->it("ignores E_NOTICE", function() { - $closure = function() { + $this->it("ignores E_NOTICE", function () { + $closure = function () { $a = $b; }; $this->expect($closure)->not->toThrow(); @@ -1073,13 +1241,13 @@ }); - describe("->reporters()", function() { + describe("->reporters()", function () { - it("returns the reporters", function() { + it("returns the reporters", function () { - $describe = $this->suite->describe("", function() {}); + $describe = $this->suite->describe("", function () {}); - $reporters = Stub::create(); + $reporters = Double::instance(); $this->suite->run(['reporters' => $reporters]); expect($this->suite->reporters())->toBe($reporters); @@ -1088,17 +1256,15 @@ }); - describe("->stop()", function() { + describe("->stop()", function () { - it("sends the stop event", function() { + it("sends the stop event", function () { - $describe = $this->suite->describe("", function() {}); + $describe = $this->suite->describe("", function () {}); - $reporters = Stub::create(); + $reporters = Double::instance(); - expect($reporters)->toReceive('process')->with('stop', Arg::toMatch(function($actual) { - return isset($actual['specs']) && isset($actual['focuses']); - })); + expect($reporters)->toReceive('dispatch')->with('stop', Arg::toBeAnInstanceOf('Kahlan\Summary')); $this->suite->run(['reporters' => $reporters]); $this->suite->stop(); diff --git a/spec/Suite/Summary.spec.php b/spec/Suite/Summary.spec.php new file mode 100644 index 00000000..2ff12eda --- /dev/null +++ b/spec/Suite/Summary.spec.php @@ -0,0 +1,198 @@ +result = new Summary(); + + }); + + describe("->__construct()", function () { + + it("correctly sets default values", function () { + + expect($this->result->total())->toBe(0); + expect($this->result->executable())->toBe(0); + expect($this->result->expectation())->toBe(0); + expect($this->result->passed())->toBe(0); + expect($this->result->pending())->toBe(0); + expect($this->result->skipped())->toBe(0); + expect($this->result->excluded())->toBe(0); + expect($this->result->failed())->toBe(0); + expect($this->result->errored())->toBe(0); + expect($this->result->get('focused'))->toBe([]); + expect($this->result->logs())->toBe([]); + expect($this->result->logs('passed'))->toBe([]); + expect($this->result->logs('pending'))->toBe([]); + expect($this->result->logs('skipped'))->toBe([]); + expect($this->result->logs('excluded'))->toBe([]); + expect($this->result->logs('failed'))->toBe([]); + expect($this->result->logs('errored'))->toBe([]); + expect($this->result->memoryUsage())->toBe(0); + + }); + + }); + + describe("->total()", function () { + + it("gets the total number of specs", function () { + + $this->result->log(new Log(['type' => 'passed'])); + $this->result->log(new Log(['type' => 'pending'])); + $this->result->log(new Log(['type' => 'skipped'])); + $this->result->log(new Log(['type' => 'excluded'])); + $this->result->log(new Log(['type' => 'failed'])); + $this->result->log(new Log(['type' => 'errored'])); + + expect($this->result->total())->toBe(6); + + }); + + }); + + describe("->expectation()", function () { + + it("gets the total number of expectations", function () { + + $log1 = new Log(); + $log1->add('passed', []); + $log1->add('passed', []); + + $log2 = new Log(); + $log2->add('failed', []); + $log1->add('passed', []); + $log2->add('failed', []); + $log2->add('failed', []); + $log2->add('failed', []); + + $this->result->log($log1); + $this->result->log($log2); + + expect($this->result->expectation())->toBe(7); + + }); + + }); + + describe("->__call()", function () { + + it("gets number of passed specs", function () { + + $this->result->log(new Log(['type' => 'passed'])); + expect($this->result->passed())->toBe(1); + + }); + + it("gets number of pending specs", function () { + + $this->result->log(new Log(['type' => 'pending'])); + expect($this->result->pending())->toBe(1); + + }); + + it("gets number of skipped specs", function () { + + $this->result->log(new Log(['type' => 'skipped'])); + expect($this->result->skipped())->toBe(1); + + }); + + it("gets number of excluded specs", function () { + + $this->result->log(new Log(['type' => 'excluded'])); + expect($this->result->excluded())->toBe(1); + + }); + + it("gets number of failed specs", function () { + + $this->result->log(new Log(['type' => 'failed'])); + expect($this->result->failed())->toBe(1); + + }); + + it("gets number of errored specs", function () { + + $this->result->log(new Log(['type' => 'errored'])); + expect($this->result->errored())->toBe(1); + + }); + + }); + + describe("->add()/->get()", function () { + + it("adds some custom data", function () { + + $value1 = 'value1'; + $value2 = 'value2'; + + $this->result->add('focused', $value1); + $this->result->add('focused', $value2); + + expect($this->result->get('focused'))->toBe([ + $value1, + $value2, + ]); + + }); + + }); + + describe("->logs()", function () { + + it("returns the total number of specs of a specific type", function () { + + $this->result->log(new Log(['type' => 'passed'])); + $this->result->log(new Log(['type' => 'passed'])); + $this->result->log(new Log(['type' => 'passed'])); + $this->result->log(new Log(['type' => 'pending'])); + $this->result->log(new Log(['type' => 'skipped'])); + $this->result->log(new Log(['type' => 'excluded'])); + $this->result->log(new Log(['type' => 'failed'])); + $this->result->log(new Log(['type' => 'errored'])); + + expect($this->result->logs('passed'))->toHaveLength(3); + expect($this->result->logs('skipped'))->toHaveLength(1); + expect($this->result->logs('skipped'))->toHaveLength(1); + expect($this->result->logs('excluded'))->toHaveLength(1); + expect($this->result->logs('failed'))->toHaveLength(1); + expect($this->result->logs('errored'))->toHaveLength(1); + + }); + + it("returns all spec logs", function () { + + $this->result->log(new Log(['type' => 'passed'])); + $this->result->log(new Log(['type' => 'passed'])); + $this->result->log(new Log(['type' => 'passed'])); + $this->result->log(new Log(['type' => 'pending'])); + $this->result->log(new Log(['type' => 'skipped'])); + $this->result->log(new Log(['type' => 'excluded'])); + $this->result->log(new Log(['type' => 'failed'])); + $this->result->log(new Log(['type' => 'errored'])); + + expect($this->result->logs())->toHaveLength(8); + + }); + + }); + + describe("->memoryUsage", function () { + + it("gets/adds some memory usage", function () { + + $this->result->memoryUsage(1024); + expect($this->result->memoryUsage())->toBe(1024); + + }); + + }); + +}); diff --git a/spec/Suite/Util/TextSpec.php b/spec/Suite/Util/Text.spec.php similarity index 75% rename from spec/Suite/Util/TextSpec.php rename to spec/Suite/Util/Text.spec.php index f8b8c338..3f6b58d1 100644 --- a/spec/Suite/Util/TextSpec.php +++ b/spec/Suite/Util/Text.spec.php @@ -4,13 +4,13 @@ use stdClass; use Exception; use Kahlan\Util\Text; -use Kahlan\Plugin\Stub; +use Kahlan\Plugin\Double; -describe("Text", function() { +describe("Text", function () { - describe("::insert()", function() { + describe("::insert()", function () { - it("inserts scalar variables in a string", function() { + it("inserts scalar variables in a string", function () { $string = 'Obi-Wan is {:adjective}.'; $expected = 'Obi-Wan is awesome.'; @@ -19,20 +19,20 @@ }); - it("inserts object variables supporting `__toString()` in a string", function() { + it("inserts object variables supporting `__toString()` in a string", function () { $string = 'Obi-Wan is a {:noun}.'; $expected = 'Obi-Wan is a jedi.'; - $stub = Stub::create(); - Stub::on($stub)->method('__toString')->andReturn('jedi'); + $stub = Double::instance(); + allow($stub)->toReceive('__toString')->andReturn('jedi'); $result = Text::insert($string, ['noun' => $stub]); $this->expect($result)->toBe($expected); }); - it("inserts a blank for object variables which doesn't support `__toString()`", function() { + it("inserts a blank for object variables which doesn't support `__toString()`", function () { $string = 'Obi-Wan is a {:noun}.'; $expected = 'Obi-Wan is a .'; @@ -42,7 +42,7 @@ }); - it("inserts a variable as many time as it exists a placeholder", function() { + it("inserts a variable as many time as it exists a placeholder", function () { $string = '{:a} {:b} {:a} {:a}'; $expected = '1 2 1 1'; @@ -51,7 +51,7 @@ }); - it("inserts a variable with custom placeholder", function() { + it("inserts a variable with custom placeholder", function () { $string = '%a %b %a %a'; $expected = '1 2 1 1'; @@ -60,7 +60,7 @@ }); - it("escapes escaped placeholder", function() { + it("escapes escaped placeholder", function () { $string = '{:a} {:b} \{:a} {:a}'; $expected = '1 2 {:a} 1'; @@ -71,23 +71,23 @@ }); - describe("::clean()", function() { + describe("::clean()", function () { - it("cleans placeholder", function() { + it("cleans placeholder", function () { $result = Text::clean('{:incomplete}'); $this->expect($result)->toBe(''); }); - it("cleans placeholder with a default string", function() { + it("cleans placeholder with a default string", function () { $result = Text::clean('{:incomplete}', ['replacement' => 'complete']); $this->expect($result)->toBe('complete'); }); - it("cleans placeholder and adjacent spaces", function() { + it("cleans placeholder and adjacent spaces", function () { $result = Text::clean('{:a} 2 3'); $this->expect($result)->toBe('2 3'); @@ -100,7 +100,7 @@ }); - it("cleans placeholder and adjacent commas", function() { + it("cleans placeholder and adjacent commas", function () { $result = Text::clean('{:a}, 2, 3'); $this->expect($result)->toBe('2, 3'); @@ -119,7 +119,7 @@ }); - it("cleans placeholder and adjacent `'and'`", function() { + it("cleans placeholder and adjacent `'and'`", function () { $result = Text::clean('{:a} and 2 and 3'); $this->expect($result)->toBe('2 and 3'); @@ -138,7 +138,7 @@ }); - it("cleans placeholder and adjacent comma and `'and'`", function() { + it("cleans placeholder and adjacent comma and `'and'`", function () { $result = Text::clean('{:a}, 2 and 3'); $this->expect($result)->toBe('2 and 3'); @@ -150,35 +150,35 @@ }); - describe("::toString()", function() { + describe("::toString()", function () { - it("exports an empty array", function() { + it("exports an empty array", function () { $dump = Text::toString([]); $this->expect($dump)->toBe("[]"); }); - it("exports an object", function() { + it("exports an object", function () { $dump = Text::toString(new stdClass()); $this->expect($dump)->toBe("`stdClass`"); }); - it("exports an object supporting __toString()", function() { + it("exports an object supporting __toString()", function () { - $stub = Stub::create(); - Stub::on($stub)->method('__toString')->andReturn('jedi'); + $stub = Double::instance(); + allow($stub)->toReceive('__toString')->andReturn('jedi'); $dump = Text::toString($stub); $this->expect($dump)->toBe("jedi"); }); - it("exports an object using a closure", function() { + it("exports an object using a closure", function () { - $toString = function($instance) { + $toString = function ($instance) { return 'an instance of `' . get_class($instance) . '`'; }; $dump = Text::toString(new stdClass(), ['object' => ['method' => $toString]]); @@ -186,61 +186,61 @@ }); - it("exports an exception", function() { + it("exports an exception", function () { $dump = Text::toString(new Exception()); - $this->expect($dump)->toMatch("~`Exception` Code\(0\) with no message in .*?" . DS . "TextSpec.php.*?$~"); + $this->expect($dump)->toMatch("~`Exception` Code\(0\) with no message in .*?" . DS . "Text.spec.php.*?$~"); $dump = Text::toString(new Exception('error', 500)); - $this->expect($dump)->toMatch("~`Exception` Code\(500\) with message \"error\" in .*?" . DS . "TextSpec.php.*?$~"); + $this->expect($dump)->toMatch("~`Exception` Code\(500\) with message \"error\" in .*?" . DS . "Text.spec.php.*?$~"); }); - it("exports a Closure", function() { + it("exports a Closure", function () { - $dump = Text::toString(function(){}); + $dump = Text::toString(function (){}); $this->expect($dump)->toBe("`Closure`"); }); - context("with double quote", function() { + context("with double quote", function () { - it("exports a string", function() { + it("exports a string", function () { $dump = Text::toString('Hello', ['quote' => '"']); $this->expect($dump)->toBe('"Hello"'); }); - it("escapes double quote", function() { + it("escapes double quote", function () { $dump = Text::toString('Hel"lo', ['quote' => '"']); $this->expect($dump)->toBe('"Hel\"lo"'); }); - it("doesn't escape simple quote", function() { + it("doesn't escape simple quote", function () { $dump = Text::toString("Hel'lo", ['quote' => '"']); $this->expect($dump)->toBe('"Hel\'lo"'); }); - it("exports an array", function() { + it("exports an array", function () { $dump = Text::toString(['Hello', 'World'], ['quote' => '"']); $this->expect($dump)->toBe("[\n 0 => \"Hello\",\n 1 => \"World\"\n]"); }); - it("exports an nested array", function() { + it("exports an nested array", function () { $dump = Text::toString([['Hello'], ['World']], ['quote' => '"']); $this->expect($dump)->toBe("[\n 0 => [\n 0 => \"Hello\"\n ],\n 1 => [\n 0 => \"World\"\n ]\n]"); }); - it("exports an array using string as key", function() { + it("exports an array using string as key", function () { $dump = Text::toString(['Hello' => 'World'], ['quote' => '"']); $this->expect($dump)->toBe("[\n \"Hello\" => \"World\"\n]"); @@ -249,44 +249,44 @@ }); - context("with simple quote", function() { + context("with simple quote", function () { - it("exports a string", function() { + it("exports a string", function () { $dump = Text::toString('Hello', ['quote' => "'"]); $this->expect($dump)->toBe("'Hello'"); }); - it("escapes simple quote", function() { + it("escapes simple quote", function () { $dump = Text::toString("Hel'lo", ['quote' => "'"]); $this->expect($dump)->toBe("'Hel\\'lo'"); }); - it("doesn't escape double quote", function() { + it("doesn't escape double quote", function () { $dump = Text::toString('Hel"lo', ['quote' => "'"]); $this->expect($dump)->toBe("'Hel\"lo'"); }); - it("exports an array", function() { + it("exports an array", function () { $dump = Text::toString(['Hello', 'World'], ['quote' => "'"]); $this->expect($dump)->toBe("[\n 0 => 'Hello',\n 1 => 'World'\n]"); }); - it("exports an nested array", function() { + it("exports an nested array", function () { $dump = Text::toString([['Hello'], ['World']], ['quote' => "'"]); $this->expect($dump)->toBe("[\n 0 => [\n 0 => 'Hello'\n ],\n 1 => [\n 0 => 'World'\n ]\n]"); }); - it("exports an array using string as key", function() { + it("exports an array using string as key", function () { $dump = Text::toString(['Hello' => 'World'], ['quote' => "'"]); $this->expect($dump)->toBe("[\n 'Hello' => 'World'\n]"); @@ -295,9 +295,9 @@ }); - context("with no quote", function() { + context("with no quote", function () { - it("exports a string to a non quoted string dump", function() { + it("exports a string to a non quoted string dump", function () { $dump = Text::toString('Hello', ['quote' => false]); $this->expect($dump)->toBe('Hello'); @@ -308,16 +308,16 @@ }); - describe("::dump()", function() { + describe("::dump()", function () { - it("dumps null to a string dump", function() { + it("dumps null to a string dump", function () { $dump = Text::dump(null); $this->expect($dump)->toBe("null"); }); - it("dumps booleans to a string dump", function() { + it("dumps booleans to a string dump", function () { $dump = Text::dump(true); $this->expect($dump)->toBe("true"); @@ -327,7 +327,7 @@ }); - it("dumps numeric to a string dump", function() { + it("dumps numeric to a string dump", function () { $dump = Text::dump(77); $this->expect($dump)->toBe("77"); @@ -337,42 +337,42 @@ }); - it("dumps a string with double quote", function() { + it("dumps a string with double quote", function () { $dump = Text::dump('Hel"lo'); $this->expect($dump)->toBe('"Hel\\"lo"'); }); - it("dumps a string with simple quote", function() { + it("dumps a string with simple quote", function () { $dump = Text::dump("Hel'lo", "'"); $this->expect($dump)->toBe("'Hel\'lo'"); }); - it("expands escape sequences and escape special chars", function() { + it("expands escape sequences and escape special chars", function () { $dump = Text::dump(" \t \nHello \x07 \x08 \r\n \v \f World\n\n"); $this->expect($dump)->toBe("\" \\t \\nHello \\x07 \\x08 \\r\\n \\v \\f World\\n\\n\""); }); - it("expands an empty string as \"\"", function() { + it("expands an empty string as \"\"", function () { $dump = Text::dump(''); $this->expect($dump)->toBe('""'); }); - it("expands an zero string as 0", function() { + it("expands an zero string as 0", function () { $dump = Text::dump('2014'); $this->expect($dump)->toBe('"2014"'); }); - it("expands espcape special chars", function() { + it("expands espcape special chars", function () { $dump = Text::dump('20$14'); $this->expect($dump)->toBe('"20\$14"'); diff --git a/src/Allow.php b/src/Allow.php new file mode 100644 index 00000000..cd5b96d4 --- /dev/null +++ b/src/Allow.php @@ -0,0 +1,136 @@ +_isClass = true; + $this->_stub = Stub::on($actual); + } + $this->_actual = $actual; + } + + /** + * Stub a chain of methods. + * + * @param string $expected the method to be stubbed or a chain of methods. + * @return self. + */ + public function toReceive() + { + if (!$this->_isClass) { + throw new Exception("Error `toReceive()` are only available on classes/instances not functions."); + } + return $this->_method = $this->_stub->method(func_get_args()); + } + + /** + * Stub function. + * + * @return self. + */ + public function toBeCalled() + { + if ($this->_isClass) { + throw new Exception("Error `toBeCalled()` are are only available on functions not classes/instances."); + } + return Monkey::patch($this->_actual); + } + + /** + * Sets the stub logic. + * + * @param mixed ... The substitue(s). + */ + public function toBe() + { + if (!is_string($this->_actual)) { + throw new Exception("Error `toBe()` need to be applied on a fully-namespaced class or function name."); + } + $method = Monkey::patch($this->_actual); + call_user_func_array([$method, 'toBe'], func_get_args()); + } + + /** + * Sets the stub logic. + * + * @param mixed $substitute The logic. + */ + public function toBeOK() + { + if (!is_string($this->_actual)) { + throw new Exception("Error `toBeOK()` need to be applied on a fully-namespaced class or function name."); + } + if ($this->_isClass) { + Monkey::patch($this->_actual, Double::classname()); + } else { + Monkey::patch($this->_actual, function () { + }); + } + } + + /** + * Set return values. + * + * @param mixed ... <0,n> Return value(s). + */ + public function andReturn() + { + throw new Exception("You must to call `toReceive()/toBeCalled()` before defining a return value."); + } + + /** + * Set return values. + * + * @param mixed ... <0,n> Return value(s). + */ + public function andRun() + { + throw new Exception("You must to call `toReceive()/toBeCalled()` before defining a return value."); + } +} diff --git a/src/Analysis/Debugger.php b/src/Analysis/Debugger.php index 32459cc3..a57d50c3 100644 --- a/src/Analysis/Debugger.php +++ b/src/Analysis/Debugger.php @@ -90,7 +90,7 @@ public static function backtrace($options = []) $mask = $options['args'] ? 0 : DEBUG_BACKTRACE_IGNORE_ARGS; $mask = $options['object'] ? $mask | DEBUG_BACKTRACE_PROVIDE_OBJECT : $mask; - $backtrace = static::normalise($options['trace'] ?: debug_backtrace($mask)); + $backtrace = static::normalize($options['trace'] ?: debug_backtrace($mask)); $traceDefaults = [ 'line' => '?', @@ -102,7 +102,7 @@ public static function backtrace($options = []) $back = []; $ignoreFunctions = ['call_user_func_array', 'trigger_error']; - foreach($backtrace as $i => $trace) { + foreach ($backtrace as $i => $trace) { $trace += $traceDefaults; if (strpos($trace['function'], '{closure}') !== false || in_array($trace['function'], $ignoreFunctions)) { continue; @@ -120,9 +120,9 @@ public static function backtrace($options = []) * @param array|object $backtrace A backtrace array or an exception instance. * @return array A backtrace array. */ - public static function normalise($backtrace) + public static function normalize($backtrace) { - if (!$backtrace instanceof Exception) { + if (!static::isThrowable($backtrace)) { return $backtrace; } return array_merge([[ @@ -133,6 +133,20 @@ public static function normalise($backtrace) ]], $backtrace->getTrace()); } + /** + * Check if a value is "Throwable" or not. + * + * @param mixed $value A value. + * @return boolean Return `true` if throwable. + */ + public static function isThrowable($value) + { + if (!is_object($value)) { + return false; + } + return is_a($value, 'Exception') || is_a($value, 'Throwable'); + } + /** * Generates a string message from a backtrace array. * @@ -141,7 +155,7 @@ public static function normalise($backtrace) */ public static function message($backtrace) { - if ($backtrace instanceof Exception) { + if (static::isThrowable($backtrace)) { $name = get_class($backtrace); $code = $backtrace->getCode(); return "`{$name}` Code({$code}): " . $backtrace->getMessage(); @@ -221,7 +235,8 @@ protected static function _findPos($file, $callLine) * @return array A cleaned backtrace. * */ - public static function focus($pattern, $backtrace, $depth = null, $maxLookup = 10) { + public static function focus($pattern, $backtrace, $depth = null, $maxLookup = 10) + { if (!$pattern) { return $backtrace; } @@ -263,8 +278,7 @@ public static function loader($loader = null) public static function errorType($value) { - switch($value) - { + switch ($value) { case E_ERROR: return 'E_ERROR'; case E_WARNING: @@ -298,5 +312,4 @@ public static function errorType($value) } return ''; } - } diff --git a/src/Analysis/Inspector.php b/src/Analysis/Inspector.php index 667ab50b..06c2fa36 100644 --- a/src/Analysis/Inspector.php +++ b/src/Analysis/Inspector.php @@ -18,7 +18,8 @@ class Inspector * @param string $class The class name to inspect. * @return object The ReflectionClass instance. */ - public static function inspect($class) { + public static function inspect($class) + { if (!isset(static::$_cache[$class])) { static::$_cache[$class] = new ReflectionClass($class); } @@ -68,15 +69,16 @@ public static function typehint($parameter) $typehint = ''; if ($parameter->getClass()) { $typehint = '\\' . $parameter->getClass()->getName(); - } elseif (preg_match('/.*?\[ \<[^\>]+\> (\w+)(.*?)\$/', (string) $parameter, $match)) { + } elseif (preg_match('/.*?\[ \<[^\>]+\> (?:HH\\\)?(\w+)(.*?)\$/', (string) $parameter, $match)) { $typehint = $match[1]; if ($typehint === 'integer') { $typehint = 'int'; } elseif ($typehint === 'boolean') { $typehint = 'bool'; + } elseif ($typehint === 'mixed') { + $typehint = ''; } } return $typehint; } - } diff --git a/src/Arg.php b/src/Arg.php index 7bbed549..eaa476f4 100644 --- a/src/Arg.php +++ b/src/Arg.php @@ -29,11 +29,11 @@ class Arg protected $_matchers = []; /** - * The expected params. + * The expected arguments. * * @var array */ - protected $_params = []; + protected $_args = []; /** * If `true`, the result of the test will be inverted. @@ -48,27 +48,27 @@ class Arg * @param array $config The argument matcher options. Possible values are: * - `'not'` _boolean_: indicate if the matcher is a negative matcher. * - `'matcher'` _string_ : the fully namespaced matcher class name. - * - `'params'` _string_ : the expected parameters. + * - `'args'` _string_ : the expected arcuments. */ public function __construct($config = []) { - $defaults = ['name' => '', 'not' => false, 'matchers' => [], 'params' => []]; + $defaults = ['name' => '', 'not' => false, 'matchers' => [], 'args' => []]; $config += $defaults; $this->_name = $config['name']; $this->_not = $config['not']; $this->_matchers = $config['matchers']; - $this->_params = $config['params']; + $this->_args = $config['args']; } /** * Create an Argument Matcher * - * @param string $name The name of the matcher. - * @param array $params The parameters to pass to the matcher. + * @param string $name The name of the matcher. + * @param array $args The arguments to pass to the matcher. * @return boolean */ - public static function __callStatic($name, $params) + public static function __callStatic($name, $args) { $not = false; if (preg_match('/^not/', $name)) { @@ -79,7 +79,7 @@ public static function __callStatic($name, $params) } $class = static::$_classes['matcher']; if ($matchers = $class::get($matcher, true)) { - return new static(compact('name', 'matchers', 'not', 'params')); + return new static(compact('name', 'matchers', 'not', 'args')); } throw new Exception("Unexisting matchers attached to `'{$name}'`."); } @@ -105,9 +105,9 @@ public function match($actual) if (!$matcher) { throw new Exception("Unexisting matcher attached to `'{$this->_name}'` for `{$target}`."); } - $params = $this->_params; - array_unshift($params, $actual); - $boolean = call_user_func_array($matcher . '::match', $params); + $args = $this->_args; + array_unshift($args, $actual); + $boolean = call_user_func_array($matcher . '::match', $args); return $this->_not ? !$boolean : $boolean; } } diff --git a/src/Box/Box.php b/src/Box/Box.php index 594e2ef4..fe49f9b7 100644 --- a/src/Box/Box.php +++ b/src/Box/Box.php @@ -26,7 +26,8 @@ class Box * @param array $config The instance configuration. Possible values: * - `'wrapper'` _string_: the the wrapper class name to use. */ - public function __construct($config = []) { + public function __construct($config = []) + { $defaults = [ 'wrapper' => 'Kahlan\Box\Wrapper' ]; diff --git a/src/Box/Wrapper.php b/src/Box/Wrapper.php index 3b1e6dba..8b87d3db 100644 --- a/src/Box/Wrapper.php +++ b/src/Box/Wrapper.php @@ -82,5 +82,4 @@ public function get() array_unshift($params, $this->__name); return $this->__dependency = call_user_func_array([$this->__box, 'get'], $params); } - -} \ No newline at end of file +} diff --git a/src/Cli/Args.php b/src/Cli/Args.php deleted file mode 100644 index e83a7971..00000000 --- a/src/Cli/Args.php +++ /dev/null @@ -1,235 +0,0 @@ - $config) { - $this->argument($name, $config); - } - } - - /** - * Returns all arguments attributes. - * - * @return array - */ - public function arguments() { - return $this->_arguments; - } - - - /** - * Gets/Sets/Overrides an argument's attributes. - * - * @param string $name The name of the argument. - * @param array $config The argument attributes to set. - * @return array - */ - public function argument($name = null, $config = [], $value = null) - { - $defaults = [ - 'type' => 'string', - 'array' => false, - 'value' => null, - 'default' => null - ]; - if (func_num_args() === 1) { - if (isset($this->_arguments[$name])) { - return $this->_arguments[$name]; - } - return $defaults; - } - $config = is_array($config) ? $config + $defaults : [$config => $value] + $this->argument($name); - - return $this->_arguments[$name] = $config; - } - - /** - * Parses a command line argv. - * - * @param array $argv An argv data. - * @param boolean $override If set to `false` it doesn't override already setted data. - * @return array The parsed attributes - */ - public function parse($argv, $override = true) - { - $exists = []; - $override ? $this->_values = [] : $exists = array_fill_keys(array_keys($this->_values), true); - - foreach($argv as $arg) { - if ($arg === '--') { - break; - } - if ($arg[0] === '-') { - list($name, $value) = $this->_parse(ltrim($arg,'-')); - if ($override || !isset($exists[$name])) { - $this->add($name, $value, $override); - } - } - } - return $this->get(); - } - - /** - * Helper for `parse()`. - * - * @param string $arg A string argument. - * @return array The parsed argument - */ - protected function _parse($arg) - { - $pos = strpos($arg, '='); - if ($pos !== false) { - $name = substr($arg, 0, $pos); - $value = substr($arg, $pos + 1); - } else { - $name = $arg; - $value = true; - } - return [$name, $value]; - } - - /** - * Checks if an argument has been setted the value of a specific argument. - * - * @param string $name The name of the argument. - * @return boolean - */ - public function exists($name) - { - if (array_key_exists($name, $this->_values)) { - return true; - } - if (isset($this->_arguments[$name])) { - return isset($this->_arguments[$name]['default']); - } - return false; - } - - /** - * Sets the value of a specific argument. - * - * @param string $name The name of the argument. - * @param mixed $value The value of the argument to set. - * @return array The setted value. - */ - public function set($name, $value) - { - return $this->_values[$name] = $value; - } - - /** - * Adds a value to a specific argument (or set if it's not an array). - * - * @param string $name The name of the argument. - * @param mixed $value The value of the argument to set. - * @return array The setted value. - */ - public function add($name, $value) - { - $config = $this->argument($name); - if ($config['array']) { - $this->_values[$name][] = $value; - } else { - $this->set($name, $value); - } - return $this->_values[$name]; - } - - /** - * Gets the value of a specific argument. - * - * @param string $name The name of the argument. - * @return array The value. - */ - public function get($name = null) - { - if ($name !== null) { - return $this->_get($name); - } - $result = []; - foreach ($this->_arguments as $key => $value) { - if (isset($value['default'])) { - $result[$key] = $this->_get($key); - } - } - foreach ($this->_values as $key => $value) { - $result[$key] = $this->_get($key); - } - return $result; - } - - /** - * Helper for `get()`. - * - * @param string $name The name of the argument. - * @return array The casted value. - */ - protected function _get($name) - { - $config = $this->argument($name); - $value = isset($this->_values[$name]) ? $this->_values[$name] : $config['default']; - $casted = $this->cast($value, $config['type'], $config['array']); - if (!isset($config['value'])) { - return $casted; - } - if (is_callable($config['value'])) { - return array_key_exists($name, $this->_values) ? $config['value']($casted, $name, $this) : $casted; - } - return $config['value']; - } - - /** - * Casts a value according to the argument attributes. - * - * @param string $value The value to cast. - * @param string $type The type of the value. - * @param boolean $array If `true`, the argument value is considered to be an array. - * @return array The casted value. - */ - public function cast($value, $type, $array = false) - { - if (is_array($value)) { - $result = []; - foreach ($value as $key => $item) { - $result[$key] = $this->cast($item, $type); - } - return $result; - } - if ($type === 'boolean') { - $value = ($value === 'false' || $value === '0' || $value === false || $value === null) ? false : true; - } elseif ($type === 'numeric') { - $value = $value !== null ? $value + 0 : 1; - } elseif ($type === 'string') { - $value = ($value !== true && $value !== null) ? (string) $value : null; - } - if ($array) { - return $value ? (array) $value : []; - } - return $value; - } - -} diff --git a/src/Cli/Cli.php b/src/Cli/Cli.php index 169cf224..087c55ad 100644 --- a/src/Cli/Cli.php +++ b/src/Cli/Cli.php @@ -169,5 +169,4 @@ public static function color($string, $options = null) return $format . $string . "\e[0m"; } - } diff --git a/src/Cli/CommandLine.php b/src/Cli/CommandLine.php new file mode 100644 index 00000000..b6dfb3d1 --- /dev/null +++ b/src/Cli/CommandLine.php @@ -0,0 +1,289 @@ + $config) { + $this->option($name, $config); + } + } + + /** + * Returns all options attributes. + * + * @return array + */ + public function options() + { + return $this->_options; + } + + + /** + * Gets/Sets/Overrides an option's attributes. + * + * @param string $name The name of the option. + * @param array $config The option attributes to set. + * @return array + */ + public function option($name = null, $config = [], $value = null) + { + $defaults = [ + 'type' => 'string', + 'group' => false, + 'array' => false, + 'value' => null, + 'default' => null + ]; + if (func_num_args() === 1) { + if (isset($this->_options[$name])) { + return $this->_options[$name]; + } + return $defaults; + } + $config = is_array($config) ? $config + $defaults : [$config => $value] + $this->option($name); + + $this->_options[$name] = $config; + + list($key, $extra) = $this->_splitOptionName($name); + + if ($extra) { + $this->option($key, ['group' => true, 'array' => true]); + } + if ($config['default'] !== null) { + $this->_defaults[$key][$extra] = $this->_get($name); + } + + return $config; + } + + /** + * Parses a command line argv. + * + * @param array $argv An argv data. + * @param boolean $override If set to `false` it doesn't override already setted data. + * @return array The parsed attributes + */ + public function parse($argv, $override = true) + { + $exists = []; + $override ? $this->_values = $this->_defaults : $exists = array_fill_keys(array_keys($this->_values), true); + + foreach ($argv as $arg) { + if ($arg === '--') { + break; + } + if ($arg[0] === '-') { + list($name, $value) = $this->_parse(ltrim($arg, '-')); + if ($override || !isset($exists[$name])) { + $this->add($name, $value, $override); + } + } + } + + return $this->get(); + } + + /** + * Helper for `parse()`. + * + * @param string $arg A string argument. + * @return array The parsed argument + */ + protected function _parse($arg) + { + $pos = strpos($arg, '='); + if ($pos !== false) { + $name = substr($arg, 0, $pos); + $value = substr($arg, $pos + 1); + } else { + $name = $arg; + $value = true; + } + return [$name, $value]; + } + + /** + * Checks if an option has been setted. + * + * @param string $name The name of the option. + * @return boolean + */ + public function exists($name) + { + list($key, $extra) = $this->_splitOptionName($name); + if (isset($this->_values[$key]) && is_array($this->_values[$key]) && array_key_exists($extra, $this->_values[$key])) { + return true; + } + if (isset($this->_options[$name])) { + return isset($this->_options[$name]['default']); + } + return false; + } + + /** + * Sets the value of a specific option. + * + * @param string $name The name of the option. + * @param mixed $value The value of the option to set. + * @return array The setted value. + */ + public function set($name, $value) + { + list($key, $extra) = $this->_splitOptionName($name); + if ($extra && !isset($this->_options[$key])) { + $this->option($key, ['group' => true, 'array' => true]); + } + return $this->_values[$key][$extra] = $value; + } + + /** + * Adds a value to a specific option (or set if it's not an array). + * + * @param string $name The name of the option. + * @param mixed $value The value of the option to set. + * @return array The setted value. + */ + public function add($name, $value) + { + $config = $this->option($name); + list($key, $extra) = $this->_splitOptionName($name); + + if ($config['array']) { + $this->_values[$key][$extra][] = $value; + } else { + $this->set($name, $value); + } + return $this->_values[$key][$extra]; + } + + /** + * Gets the value of a specific option. + * + * @param string $name The name of the option. + * @return array The value. + */ + public function get($name = null) + { + if (func_num_args()) { + return $this->_get($name); + } + $result = []; + foreach ($this->_values as $key => $data) { + foreach ($data as $extra => $value) { + if ($extra === '') { + $result[$key] = $this->_get($key); + } else { + $result[$key][$extra] = $this->_get($key . ':' . $extra); + } + } + } + return $result; + } + + /** + * Helper for `get()`. + * + * @param string $name The name of the option. + * @return array The casted value. + */ + protected function _get($name) + { + $config = $this->option($name); + list($key, $extra) = $this->_splitOptionName($name); + + if ($extra === '' && $config['group']) { + $result = []; + if (!isset($this->_values[$key])) { + return $result; + } + foreach ($this->_values[$key] as $extra => $value) { + $result[$extra] = $this->_get($name . ':' . $extra); + } + return $result; + } else { + $value = isset($this->_values[$key][$extra]) ? $this->_values[$key][$extra] : $config['default']; + } + + $casted = $this->cast($value, $config['type'], $config['array']); + if (!isset($config['value'])) { + return $casted; + } + if (is_callable($config['value'])) { + return array_key_exists($key, $this->_values) ? $config['value']($casted, $name, $this) : $casted; + } + return $config['value']; + } + + /** + * Casts a value according to the option attributes. + * + * @param string $value The value to cast. + * @param string $type The type of the value. + * @param boolean $array If `true`, the argument value is considered to be an array. + * @return array The casted value. + */ + public function cast($value, $type, $array = false) + { + if (is_array($value)) { + $result = []; + foreach ($value as $key => $item) { + $result[$key] = $this->cast($item, $type); + } + return $result; + } + if ($type === 'boolean') { + $value = ($value === 'false' || $value === '0' || $value === false || $value === null) ? false : true; + } elseif ($type === 'numeric') { + $value = $value !== null ? (int) $value + 0 : 1; + } elseif ($type === 'string') { + $value = ($value !== true && $value !== null) ? (string) $value : null; + } + if ($array) { + return $value ? (array) $value : []; + } + return $value; + } + + /** + * Helper to split option name + * + * @param string $name The option name. + * @return array + */ + protected function _splitOptionName($name) + { + $parts = explode(':', $name, 2); + return [$parts[0], isset($parts[1]) ? $parts[1] : '']; + } +} diff --git a/src/Cli/Kahlan.php b/src/Cli/Kahlan.php index b6734771..4a8de721 100644 --- a/src/Cli/Kahlan.php +++ b/src/Cli/Kahlan.php @@ -21,11 +21,11 @@ use Kahlan\Reporter\Coverage\Exporter\Lcov; use Composer\Script\Event; -class Kahlan { - +class Kahlan +{ use Filterable; - const VERSION = '2.5.7'; + const VERSION = '3.0.0'; /** * Starting time. @@ -60,7 +60,7 @@ class Kahlan { * * @var object */ - protected $_args = null; + protected $_commandLine = null; /** * Warning ! @@ -97,38 +97,38 @@ public function __construct($options = []) $this->_suite = $options['suite']; $this->_reporters = new Reporters(); - $this->_args = $args = new Args(); - - $args->argument('src', ['array' => true, 'default' => ['src']]); - $args->argument('spec', ['array' => true, 'default' => ['spec']]); - $args->argument('reporter', ['array' => true, 'default' => ['dot']]); - $args->argument('pattern', ['default' => '*Spec.php']); - $args->argument('coverage', ['type' => 'string']); - $args->argument('config', ['default' => 'kahlan-config.php']); - $args->argument('ff', ['type' => 'numeric', 'default' => 0]); - $args->argument('cc', ['type' => 'boolean', 'default' => false]); - $args->argument('no-colors', ['type' => 'boolean', 'default' => false]); - $args->argument('no-header', ['type' => 'boolean', 'default' => false]); - $args->argument('include', [ + $this->_commandLine = $commandLine = new CommandLine(); + + $commandLine->option('src', ['array' => true, 'default' => ['src']]); + $commandLine->option('spec', ['array' => true, 'default' => ['spec']]); + $commandLine->option('reporter', ['array' => true, 'default' => ['dot']]); + $commandLine->option('pattern', ['default' => ['*Spec.php', '*.spec.php']]); + $commandLine->option('coverage', ['type' => 'string']); + $commandLine->option('config', ['default' => 'kahlan-config.php']); + $commandLine->option('ff', ['type' => 'numeric', 'default' => 0]); + $commandLine->option('cc', ['type' => 'boolean', 'default' => false]); + $commandLine->option('no-colors', ['type' => 'boolean', 'default' => false]); + $commandLine->option('no-header', ['type' => 'boolean', 'default' => false]); + $commandLine->option('include', [ 'array' => true, 'default' => ['*'], - 'value' => function($value) { + 'value' => function ($value) { return array_filter($value); } ]); - $args->argument('exclude', [ + $commandLine->option('exclude', [ 'array' => true, 'default' => [], - 'value' => function($value) { + 'value' => function ($value) { return array_filter($value); } ]); - $args->argument('persistent', ['type' => 'boolean', 'default' => true]); - $args->argument('autoclear', ['array' => true, 'default' => [ + $commandLine->option('persistent', ['type' => 'boolean', 'default' => true]); + $commandLine->option('autoclear', ['array' => true, 'default' => [ 'Kahlan\Plugin\Monkey', - 'Kahlan\Plugin\Call', 'Kahlan\Plugin\Stub', - 'Kahlan\Plugin\Quit' + 'Kahlan\Plugin\Quit', + 'Kahlan\Plugin\Call\Calls' ]]); } @@ -147,9 +147,9 @@ public function autoloader() * * @return object */ - public function args() + public function commandLine() { - return $this->_args; + return $this->_commandLine; } /** @@ -179,25 +179,25 @@ public function reporters() */ public function loadConfig($argv = []) { - $args = new Args(); - $args->argument('config', ['default' => 'kahlan-config.php']); - $args->argument('help', ['type' => 'boolean']); - $args->argument('version', ['type' => 'boolean']); - $args->parse($argv); - - $run = function($args) { - if (file_exists($args->get('config'))) { - require $args->get('config'); + $commandLine = new CommandLine(); + $commandLine->option('config', ['default' => 'kahlan-config.php']); + $commandLine->option('help', ['type' => 'boolean']); + $commandLine->option('version', ['type' => 'boolean']); + $commandLine->parse($argv); + + $run = function ($commandLine) { + if (file_exists($commandLine->get('config'))) { + require $commandLine->get('config'); } }; - $run($args); - $this->_args->parse($argv, false); + $run($commandLine); + $this->_commandLine->parse($argv, false); - if ($args->get('help')) { + if ($commandLine->get('help')) { return $this->_help(); } - if ($args->get('version')) { + if ($commandLine->get('version')) { return $this->_version(); } } @@ -210,8 +210,8 @@ public function loadConfig($argv = []) protected function _terminal() { return new Terminal([ - 'colors' => !$this->args()->get('no-colors'), - 'header' => !$this->args()->get('no-header') + 'colors' => !$this->commandLine()->get('no-colors'), + 'header' => !$this->commandLine()->get('no-header') ]); } @@ -221,17 +221,17 @@ protected function _terminal() protected function _version() { $terminal = $this->_terminal(); - if (!$this->args()->get('no-header')) { + if (!$this->commandLine()->get('no-header')) { $terminal->write($terminal->kahlan() ."\n\n"); $terminal->write($terminal->kahlanBaseline(), 'd'); $terminal->write("\n\n"); } $terminal->write("version "); - $terminal->write(static::VERSION , 'green'); + $terminal->write(static::VERSION, 'green'); $terminal->write("\n\n"); $terminal->write("For additional help you must use "); - $terminal->write("--help" , 'green'); + $terminal->write("--help", 'green'); $terminal->write("\n\n"); QuitStatement::quit(); } @@ -242,7 +242,7 @@ protected function _version() protected function _help() { $terminal = $this->_terminal(); - if (!$this->args()->get('no-header')) { + if (!$this->commandLine()->get('no-header')) { $terminal->write($terminal->kahlan() ."\n\n"); $terminal->write($terminal->kahlanBaseline(), 'd'); $terminal->write("\n\n"); @@ -256,7 +256,7 @@ protected function _help() --config= The PHP configuration file to use (default: `'kahlan-config.php'`). --src= Paths of source directories (default: `['src']`). --spec= Paths of specification directories (default: `['spec']`). - --pattern= A shell wildcard pattern (default: `'*Spec.php'`). + --pattern= A shell wildcard pattern (default: `['*Spec.php', '*.spec.php']`). Reporter Options: @@ -278,12 +278,12 @@ protected function _help() Test Execution Options: --ff= Fast fail option. `0` mean unlimited (default: `0`). - --no-colors= To turn off colors. (default: `false`). - --no-header= To turn off header. (default: `false`). + --no-colors To turn off colors. (default: `false`). + --no-header To turn off header. (default: `false`). --include= Paths to include for patching. (default: `['*']`). --exclude= Paths to exclude from patching. (default: `[]`). --persistent= Cache patched files (default: `true`). - --cc= Clear cache before spec run. (default: `false`). + --cc Clear cache before spec run. (default: `false`). --autoclear Classes to autoclear after each spec (default: [ `'Kahlan\Plugin\Monkey'`, `'Kahlan\Plugin\Call'`, @@ -308,7 +308,8 @@ protected function _help() /** * Regiter built-in matchers. */ - public static function registerMatchers() { + public static function registerMatchers() + { Matcher::register('toBe', 'Kahlan\Matcher\ToBe'); Matcher::register('toBeA', 'Kahlan\Matcher\ToBeA'); Matcher::register('toBeAn', 'Kahlan\Matcher\ToBeA'); @@ -328,7 +329,7 @@ public static function registerMatchers() { Matcher::register('toHaveLength', 'Kahlan\Matcher\ToHaveLength'); Matcher::register('toMatch', 'Kahlan\Matcher\ToMatch'); Matcher::register('toReceive', 'Kahlan\Matcher\ToReceive'); - Matcher::register('toReceiveNext', 'Kahlan\Matcher\ToReceiveNext'); + Matcher::register('toBeCalled', 'Kahlan\Matcher\ToBeCalled'); Matcher::register('toThrow', 'Kahlan\Matcher\ToThrow'); Matcher::register('toMatchEcho', 'Kahlan\Matcher\ToMatchEcho'); } @@ -344,8 +345,7 @@ public function run() } $this->_start = microtime(true); - return Filter::on($this, 'workflow', [], function($chain) { - + return Filter::on($this, 'workflow', [], function ($chain) { $this->_bootstrap(); $this->_interceptor(); @@ -367,7 +367,6 @@ public function run() $this->_stop(); $this->_quit(); - }); } @@ -386,11 +385,11 @@ public function status() */ protected function _bootstrap() { - return Filter::on($this, 'bootstrap', [], function($chain) { - $this->suite()->backtraceFocus($this->args()->get('pattern')); - if (!$this->args()->exists('coverage')) { - if ($this->args()->exists('clover') || $this->args()->exists('istanbul') || $this->args()->exists('lcov')) { - $this->args()->set('coverage', 1); + return Filter::on($this, 'bootstrap', [], function ($chain) { + $this->suite()->backtraceFocus($this->commandLine()->get('pattern')); + if (!$this->commandLine()->exists('coverage')) { + if ($this->commandLine()->exists('clover') || $this->commandLine()->exists('istanbul') || $this->commandLine()->exists('lcov')) { + $this->commandLine()->set('coverage', 1); } } }); @@ -401,14 +400,14 @@ protected function _bootstrap() */ protected function _interceptor() { - return Filter::on($this, 'interceptor', [], function($chain) { + return Filter::on($this, 'interceptor', [], function ($chain) { Interceptor::patch([ 'loader' => [$this->autoloader(), 'loadClass'], - 'include' => $this->args()->get('include'), - 'exclude' => array_merge($this->args()->get('exclude'), ['Kahlan\\']), - 'persistent' => $this->args()->get('persistent'), + 'include' => $this->commandLine()->get('include'), + 'exclude' => array_merge($this->commandLine()->get('exclude'), ['Kahlan\\']), + 'persistent' => $this->commandLine()->get('persistent'), 'cachePath' => rtrim(realpath(sys_get_temp_dir()), DS) . DS . 'kahlan', - 'clearCache' => $this->args()->get('cc') + 'clearCache' => $this->commandLine()->get('cc') ]); }); } @@ -418,8 +417,8 @@ protected function _interceptor() */ protected function _namespaces() { - return Filter::on($this, 'namespaces', [], function($chain) { - $paths = $this->args()->get('spec'); + return Filter::on($this, 'namespaces', [], function ($chain) { + $paths = $this->commandLine()->get('spec'); foreach ($paths as $path) { $path = realpath($path); $namespace = basename($path) . '\\'; @@ -433,10 +432,11 @@ protected function _namespaces() */ protected function _patchers() { - return Filter::on($this, 'patchers', [], function($chain) { - if (!$interceptor = Interceptor::instance()) { - return; - } + if (!$interceptor = Interceptor::instance()) { + return; + } + return Filter::on($this, 'patchers', [], function ($chain) { + $interceptor = Interceptor::instance(); $patchers = $interceptor->patchers(); $patchers->add('pointcut', new Pointcut()); $patchers->add('monkey', new Monkey()); @@ -450,13 +450,20 @@ protected function _patchers() */ protected function _load() { - return Filter::on($this, 'load', [], function($chain) { - $files = Dir::scan($this->args()->get('spec'), [ - 'include' => $this->args()->get('pattern'), + return Filter::on($this, 'load', [], function ($chain) { + $specDirs = $this->commandLine()->get('spec'); + foreach ($specDirs as $dir) { + if (!file_exists($dir)) { + fwrite(STDERR, "ERROR: unexisting `{$dir}` directory, use --spec option to set a valid one (ex: --spec=tests).\n"); + exit(-1); + } + } + $files = Dir::scan($specDirs, [ + 'include' => $this->commandLine()->get('pattern'), 'exclude' => '*/.*', 'type' => 'file' ]); - foreach($files as $file) { + foreach ($files as $file) { require $file; } }); @@ -467,7 +474,7 @@ protected function _load() */ protected function _reporters() { - return Filter::on($this, 'reporters', [], function($chain) { + return Filter::on($this, 'reporters', [], function ($chain) { $this->_console(); $this->_coverage(); }); @@ -478,27 +485,30 @@ protected function _reporters() */ protected function _console() { - return Filter::on($this, 'console', [], function($chain) { + return Filter::on($this, 'console', [], function ($chain) { $collection = $this->reporters(); - $reporters = $this->args()->get('reporter'); + $reporters = $this->commandLine()->get('reporter'); if (!$reporters) { return; } - foreach($reporters as $reporter) { + foreach ($reporters as $reporter) { $parts = explode(":", $reporter); $name = $parts[0]; $output = isset($parts[1]) ? $parts[1] : null; + $args = $this->commandLine()->get('dot'); + $args = $args ?: []; + if (!$name === null || $name === 'none') { continue; } - $params = [ + $params = $args + [ 'start' => $this->_start, - 'colors' => !$this->args()->get('no-colors'), - 'header' => !$this->args()->get('no-header') + 'colors' => !$this->commandLine()->get('no-colors'), + 'header' => !$this->commandLine()->get('no-header') ]; if (isset($output) && strlen($output) > 0) { @@ -515,6 +525,10 @@ protected function _console() } $class = 'Kahlan\Reporter\\' . str_replace(' ', '', ucwords(str_replace(['_', '-'], ' ', trim($name)))); + if (!class_exists($class)) { + fwrite(STDERR, "Error: unexisting reporter `'{$name}'` can't find class `$class`.\n"); + exit(-1); + } $collection->add($name, new $class($params)); } }); @@ -525,8 +539,8 @@ protected function _console() */ protected function _coverage() { - return Filter::on($this, 'coverage', [], function($chain) { - if (!$this->args()->exists('coverage')) { + return Filter::on($this, 'coverage', [], function ($chain) { + if (!$this->commandLine()->exists('coverage')) { return; } $reporters = $this->reporters(); @@ -540,11 +554,18 @@ protected function _coverage() fwrite(STDERR, "ERROR: PHPDBG SAPI has not been detected and Xdebug is not installed, code coverage can't be used.\n"); exit(-1); } + $srcDirs = $this->commandLine()->get('src'); + foreach ($srcDirs as $dir) { + if (!file_exists($dir)) { + fwrite(STDERR, "ERROR: unexisting `{$dir}` directory, use --src option to set a valid one (ex: --src=app).\n"); + exit(-1); + } + } $coverage = new Coverage([ - 'verbosity' => $this->args()->get('coverage') === null ? 1 : $this->args()->get('coverage'), + 'verbosity' => $this->commandLine()->get('coverage') === null ? 1 : $this->commandLine()->get('coverage'), 'driver' => $driver, - 'path' => $this->args()->get('src'), - 'colors' => !$this->args()->get('no-colors') + 'path' => $srcDirs, + 'colors' => !$this->commandLine()->get('no-colors') ]); $reporters->add('coverage', $coverage); }); @@ -555,7 +576,7 @@ protected function _coverage() */ protected function _matchers() { - return Filter::on($this, 'matchers', [], function($chain) { + return Filter::on($this, 'matchers', [], function ($chain) { static::registerMatchers(); }); } @@ -565,11 +586,11 @@ protected function _matchers() */ protected function _run() { - return Filter::on($this, 'run', [], function($chain) { + return Filter::on($this, 'run', [], function ($chain) { $this->suite()->run([ 'reporters' => $this->reporters(), - 'autoclear' => $this->args()->get('autoclear'), - 'ff' => $this->args()->get('ff') + 'autoclear' => $this->commandLine()->get('autoclear'), + 'ff' => $this->commandLine()->get('ff') ]); }); } @@ -579,27 +600,27 @@ protected function _run() */ protected function _reporting() { - return Filter::on($this, 'reporting', [], function($chain) { + return Filter::on($this, 'reporting', [], function ($chain) { $reporter = $this->reporters()->get('coverage'); if (!$reporter) { return; } - if ($this->args()->exists('clover')) { + if ($this->commandLine()->exists('clover')) { Clover::write([ 'collector' => $reporter, - 'file' => $this->args()->get('clover') + 'file' => $this->commandLine()->get('clover') ]); } - if ($this->args()->exists('istanbul')) { + if ($this->commandLine()->exists('istanbul')) { Istanbul::write([ 'collector' => $reporter, - 'file' => $this->args()->get('istanbul') + 'file' => $this->commandLine()->get('istanbul') ]); } - if ($this->args()->exists('lcov')) { + if ($this->commandLine()->exists('lcov')) { Lcov::write([ 'collector' => $reporter, - 'file' => $this->args()->get('lcov') + 'file' => $this->commandLine()->get('lcov') ]); } }); @@ -610,7 +631,7 @@ protected function _reporting() */ protected function _stop() { - return Filter::on($this, 'stop', [], function($chain) { + return Filter::on($this, 'stop', [], function ($chain) { $this->suite()->stop(); }); } @@ -621,10 +642,9 @@ protected function _stop() */ protected function _quit() { - return Filter::on($this, 'quit', [$this->suite()->passed()], function($chain, $success) { + return Filter::on($this, 'quit', [$this->suite()->passed()], function ($chain, $success) { }); } - } define('KAHLAN_VERSION', Kahlan::VERSION); diff --git a/src/Code/Code.php b/src/Code/Code.php index 594dafa4..08160638 100644 --- a/src/Code/Code.php +++ b/src/Code/Code.php @@ -25,7 +25,7 @@ public static function run($callable, $timeout = 0) throw new Exception("PCNTL threading is not supported by your system."); } - pcntl_signal(SIGALRM, function($signal) use ($timeout) { + pcntl_signal(SIGALRM, function ($signal) use ($timeout) { throw new TimeoutException("Timeout reached, execution aborted after {$timeout} second(s)."); }, true); @@ -57,8 +57,7 @@ public static function spin($callable, $timeout = 0, $delay = 100000) throw new InvalidArgumentException(); } - $closure = function() use ($callable, $timeout, $delay) { - + $closure = function () use ($callable, $timeout, $delay) { $timeout = (float) $timeout; $result = false; $start = microtime(true); @@ -69,7 +68,6 @@ public static function spin($callable, $timeout = 0, $delay = 100000) } usleep($delay); $current = microtime(true); - } while ($current - $start < $timeout); throw new TimeoutException("Timeout reached, execution aborted after {$timeout} second(s)."); diff --git a/src/Dir/Dir.php b/src/Dir/Dir.php index ef82579b..47c9abf6 100644 --- a/src/Dir/Dir.php +++ b/src/Dir/Dir.php @@ -174,7 +174,7 @@ public static function remove($path, $options = []) * @param string $dest Destination directory. * @param array $options Scanning options. Possible values are: * -`'mode'` _integer_ : Mode used for directory creation. - * -`'childsOnly'` _boolean_ : Excludes parent directory if `true`. + * -`'childrenOnly'` _boolean_ : Excludes parent directory if `true`. * -`'followSymlinks'` _boolean_ : Follows Symlinks if `true`. * -`'recursive'` _boolean_ : Scans recursively if `true`. * @return array @@ -184,7 +184,7 @@ public static function copy($path, $dest, $options = []) { $defaults = [ 'mode' => 0755, - 'childsOnly' => false, + 'childrenOnly' => false, 'followSymlinks' => true, 'recursive' => true ]; @@ -215,7 +215,7 @@ public static function copy($path, $dest, $options = []) * @param string $dest Destination directory. * @param array $options Scanning options. Possible values are: * -`'mode'` _integer_ : Mode used for directory creation. - * -`'childsOnly'` _boolean_ : Excludes parent directory if `true`. + * -`'childrenOnly'` _boolean_ : Excludes parent directory if `true`. * -`'followSymlinks'` _boolean_ : Follows Symlinks if `true`. * -`'recursive'` _boolean_ : Scans recursively if `true`. * -`'include'` _string|array_: An array of includes. @@ -226,7 +226,7 @@ public static function copy($path, $dest, $options = []) protected static function _copy($path, $dest, $options) { $ds = DIRECTORY_SEPARATOR; - $root = $options['childsOnly'] ? $path : dirname($path); + $root = $options['childrenOnly'] ? $path : dirname($path); $dest = rtrim($dest, $ds); $paths = static::scan($path, $options); @@ -337,7 +337,7 @@ public function accept() */ protected function _excluded($path) { - foreach($this->_exclude as $exclude) { + foreach ($this->_exclude as $exclude) { if (fnmatch($exclude, $path)) { return true; } @@ -352,7 +352,7 @@ protected function _excluded($path) */ protected function _included($path) { - foreach($this->_include as $include) { + foreach ($this->_include as $include) { if (fnmatch($include, $path)) { return true; } diff --git a/src/Expectation.php b/src/Expectation.php index 7ced9e2d..bd4c2216 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -38,29 +38,19 @@ */ class Expectation { - /** - * Class dependencies. - * - * @var array - */ - protected $_classes = [ - 'specification' => 'Kahlan\Specification', - 'matcher' => 'matcher' - ]; - /** * Deferred expectation. * * @var array */ - protected $_deferred = []; + protected $_deferred = null; /** * Stores the success value. * * @var boolean */ - protected $_passed = true; + protected $_passed = null; /** * The result logs. @@ -90,14 +80,6 @@ class Expectation */ protected $_timeout = -1; - /** - * Factory method. - */ - public static function expect($actual, $timeout = -1) - { - return new static(compact('actual', 'timeout')); - } - /** * Constructor. * @@ -166,55 +148,55 @@ public function timeout() /** * Calls a registered matcher. * - * @param string $matcherName The name of the matcher. - * @param array $params The parameters to pass to the matcher. + * @param string $matcherName The name of the matcher. + * @param array $args The arguments to pass to the matcher. * @return boolean */ - public function __call($matcherName, $params) + public function __call($matcherName, $args) { $result = true; $spec = $this->_actual; - $specification = $this->_classes['specification']; + $this->_passed = true; - $closure = function () use ($spec, $specification, $matcherName, $params, &$actual, &$result) { - if ($spec instanceof $specification) { - $actual = $spec->run(); - if (!$spec->passed()) { + $closure = function () use ($spec, $matcherName, $args, &$actual, &$result) { + if ($spec instanceof Specification) { + $actual = null; + if (!$spec->passed($actual)) { return false; } } else { $actual = $spec; } - array_unshift($params, $actual); + array_unshift($args, $actual); $matcher = $this->_matcher($matcherName, $actual); - $result = call_user_func_array($matcher . '::match', $params); + $result = call_user_func_array($matcher . '::match', $args); return is_object($result) || $result === !$this->_not; }; try { $this->_spin($closure); } catch (TimeoutException $e) { - $data['params']['timeout'] = $e->getMessage(); + $data['data']['timeout'] = $e->getMessage(); } - array_unshift($params, $actual); + array_unshift($args, $actual); $matcher = $this->_matcher($matcherName, $actual); - $params = Inspector::parameters($matcher, 'match', $params); - $data = compact('matcherName', 'matcher', 'params'); - if ($spec instanceof $specification) { + $data = Inspector::parameters($matcher, 'match', $args); + $report = compact('matcherName', 'matcher', 'data'); + if ($spec instanceof Specification) { foreach ($spec->logs() as $value) { $this->_logs[] = $value; } - $this->_passed = $this->_passed && $spec->passed(); + $this->_passed = $spec->passed() && $this->_passed; } if (!is_object($result)) { - $data['description'] = $data['matcher']::description(); - $this->_log($result, $data); + $report['description'] = $report['matcher']::description(); + $this->_log($result, $report); return $this; } - $this->_deferred[] = $data + [ + $this->_deferred = $report + [ 'instance' => $result, 'not' => $this->_not, ]; @@ -224,9 +206,9 @@ public function __call($matcherName, $params) /** * Returns a compatible matcher class name according to a passed actual value. * - * @param string $matcherName The name of the matcher. - * @param string $actual The actual value. - * @return string A matcher class name. + * @param string $matcherName The name of the matcher. + * @param mixed $actual The actual value. + * @return string A matcher class name. */ public function _matcher($matcherName, $actual) { @@ -261,33 +243,32 @@ public function _matcher($matcherName, $actual) * * @return mixed */ - public function run() + protected function _run() { + if ($this->_passed !== null) { + return $this; + } $spec = $this->_actual; - $specification = $this->_classes['specification']; - if (!$spec instanceof $specification) { + if (!$spec instanceof Specification) { $this->_passed = false; - return $this; } $closure = function () use ($spec) { + $success = true; try { - $spec->run(); + $success = $spec->passed(); } catch (Exception $e) { } - - return $spec->passed(); + return $success; }; try { $this->_spin($closure); } catch (TimeoutException $e) { } - foreach ($spec->logs() as $value) { - $this->_logs[] = $value; - } - $this->_passed = $this->_passed && $spec->passed(); + $this->_logs = $spec->logs(); + $this->_passed = $spec->passed() && $this->_passed; return $this; } @@ -311,15 +292,19 @@ protected function _spin($closure) */ protected function _resolve() { - foreach ($this->_deferred as $data) { - $instance = $data['instance']; - $this->_not = $data['not']; - $boolean = $instance->resolve(); - $data['description'] = $instance->description(); - $data['backtrace'] = $instance->backtrace(); - $this->_log($boolean, $data); + if (!$this->_deferred) { + return; } - $this->_deferred = []; + $data = $this->_deferred; + + $instance = $data['instance']; + $this->_not = $data['not']; + $boolean = $instance->resolve(); + $data['description'] = $instance->description(); + $data['backtrace'] = $instance->backtrace(); + $this->_log($boolean, $data); + + $this->_deferred = null; } /** @@ -334,15 +319,15 @@ protected function _log($boolean, $data = []) $not = $this->_not; $pass = $not ? !$boolean : $boolean; if ($pass) { - $data['type'] = 'pass'; + $data['type'] = 'passed'; } else { - $data['type'] = 'fail'; + $data['type'] = 'failed'; $this->_passed = false; } $description = $data['description']; if (is_array($description)) { - $data['params'] = $description['params']; + $data['data'] = $description['data']; $data['description'] = $description['description']; } $data += ['backtrace' => Debugger::backtrace()]; @@ -374,25 +359,11 @@ public function __get($name) */ public function passed() { - if ($this->_deferred) { - $this->_resolve(); - } + $this->_run(); + $this->_resolve(); return $this->_passed; } - /** - * Checks if all test passed. - * - * @return boolean Returns `true` if no error occurred, `false` otherwise. - */ - public function runned() - { - if ($this->_deferred) { - $this->_resolve(); - } - return !empty($this->_logs); - } - /** * Clears the instance. */ @@ -403,6 +374,6 @@ public function clear() $this->_not = false; $this->_timeout = -1; $this->_logs = []; - $this->_deferred = []; + $this->_deferred = null; } } diff --git a/src/Filter/Behavior/Filterable.php b/src/Filter/Behavior/Filterable.php index c916d2d3..f3b2c39c 100644 --- a/src/Filter/Behavior/Filterable.php +++ b/src/Filter/Behavior/Filterable.php @@ -25,5 +25,4 @@ public function methodFilters($methodFilters = null) } return $this->_methodFilters; } - } diff --git a/src/Filter/Filter.php b/src/Filter/Filter.php index ca7bf3c3..e8b01d21 100644 --- a/src/Filter/Filter.php +++ b/src/Filter/Filter.php @@ -172,8 +172,8 @@ public static function filters($context = null, $method = null) * @param string $method The name of the method to get the filters from. * @return array The whole filters data or filters associated to a class/instance's method. */ - public static function _classFilters($context, $method) { - + public static function _classFilters($context, $method) + { if (isset(static::$_cachedFilters[$context][$method])) { return static::$_cachedFilters[$context][$method]; } diff --git a/src/IncompleteException.php b/src/IncompleteException.php deleted file mode 100644 index 3e8832dd..00000000 --- a/src/IncompleteException.php +++ /dev/null @@ -1,6 +0,0 @@ -loadFile($file); } return true; @@ -520,7 +520,7 @@ public function clearCache() $dir = new RecursiveDirectoryIterator($cachePath, RecursiveDirectoryIterator::SKIP_DOTS); $files = new RecursiveIteratorIterator($dir, RecursiveIteratorIterator::CHILD_FIRST); - foreach($files as $file) { + foreach ($files as $file) { $path = $file->getRealPath(); $file->isDir() ? rmdir($path) : unlink($path); } @@ -565,7 +565,8 @@ public function __call($method, $params) * * @return array */ - public function getPrefixes() { + public function getPrefixes() + { $ds = DIRECTORY_SEPARATOR; $getPrefixes = $this->_getPrefixes; $getPrefixesPsr4 = $this->_getPrefixesPsr4; diff --git a/src/Jit/Node/BlockDef.php b/src/Jit/Node/BlockDef.php index 72e92b50..b3316457 100644 --- a/src/Jit/Node/BlockDef.php +++ b/src/Jit/Node/BlockDef.php @@ -3,14 +3,14 @@ class BlockDef extends NodeDef { - /** + /** * The node's type. * * @var string */ public $type = null; - /** + /** * Boolean indicating if this node has methods (i.e class, trait or interface) * * @var boolean @@ -24,7 +24,7 @@ class BlockDef extends NodeDef */ public $name = ''; - /** + /** * The defined uses (for class only) * * @var array @@ -44,5 +44,4 @@ class BlockDef extends NodeDef * @var array|null */ public $implements = null; - } diff --git a/src/Jit/Node/FunctionDef.php b/src/Jit/Node/FunctionDef.php index 4513fe89..33006139 100644 --- a/src/Jit/Node/FunctionDef.php +++ b/src/Jit/Node/FunctionDef.php @@ -67,5 +67,4 @@ public function argsToParams() } return join(', ', $args); } - } diff --git a/src/Jit/Node/NodeDef.php b/src/Jit/Node/NodeDef.php index 0a9280c1..c0f48bd5 100644 --- a/src/Jit/Node/NodeDef.php +++ b/src/Jit/Node/NodeDef.php @@ -78,7 +78,7 @@ class NodeDef public $close = ''; /** - * The childs of the node. + * The children of the node. * * @var array */ @@ -116,10 +116,10 @@ public function __construct($body = '', $type = null) */ public function __toString() { - $childs = ''; + $children = ''; foreach ($this->tree as $node) { - $childs .= (string) $node; + $children .= (string) $node; } - return $this->body . $childs . $this->close; + return $this->body . $children . $this->close; } } diff --git a/src/Jit/Parser.php b/src/Jit/Parser.php index 8d9644ab..424181da 100644 --- a/src/Jit/Parser.php +++ b/src/Jit/Parser.php @@ -90,34 +90,34 @@ protected function _parser($content, $lines = false) $this->_states['body'] .= $token[1]; $this->_codeNode('open'); $this->_states['php'] = true; - break; + break; case T_CLOSE_TAG: $this->_codeNode(); $this->_states['php'] = false; $this->_states['body'] .= $token[1]; $this->_codeNode('close'); - break; + break; case T_DOC_COMMENT: case T_COMMENT: $this->_commentNode(); - break; + break; case T_CONSTANT_ENCAPSED_STRING: $this->_stringNode(''); - break; + break; case T_START_HEREDOC: $name = substr($token[1], 3, -1); $this->_stringNode("\n" . $name . ';'); - break; + break; case '"': $this->_stringNode('"'); - break; + break; case '{': $this->_states['body'] .= $token[0]; $this->_states['current'] = $this->_codeNode(); - break; + break; case '}': $this->_closeCurly(); - break; + break; case '(': case '[': $this->_states['body'] .= $token[0]; @@ -125,7 +125,7 @@ protected function _parser($content, $lines = false) $lines = explode("\n", $this->_states['body']); $blockStartLines[$token[0]][] = $this->_states['num'] + (count($lines) - 1); } - break; + break; case ')': case ']': $this->_states['body'] .= $token[0]; @@ -133,7 +133,7 @@ protected function _parser($content, $lines = false) $char = $token[0] === ']' ? '[' : '('; $blockStartLine[$token[0]] = array_pop($blockStartLines[$char]); } - break; + break; case ';': $this->_states['body'] .= $token[1]; $node = $this->_codeNode(null, true); @@ -149,22 +149,22 @@ protected function _parser($content, $lines = false) } } } - break; + break; case T_NAMESPACE: $this->_namespaceNode(); - break; + break; case T_USE: $this->_useNode(); - break; + break; case T_TRAIT: $this->_traitNode(); - break; + break; case T_INTERFACE: $this->_interfaceNode(); - break; + break; case T_CLASS: $this->_classNode(); - break; + break; case T_FINAL: case T_ABSTRACT: case T_PRIVATE: @@ -173,11 +173,11 @@ protected function _parser($content, $lines = false) case T_STATIC: $this->_states['visibility'][$token[1]] = true; $this->_states['body'] .= $token[1]; - break; + break; case T_FUNCTION: $this->_functionNode(); $buffered = ''; - break; + break; case $T_YIELD: // use T_YIELD directly when PHP 5.4 support will be removed. $parent = $this->_states['current']; while ($parent && !$parent instanceof FunctionDef) { @@ -185,14 +185,14 @@ protected function _parser($content, $lines = false) } $parent->isGenerator = true; $this->_states['body'] .= $token[1]; - break; + break; case T_VARIABLE: $this->_states['visibility'] = []; $this->_states['body'] .= $token[1]; break; default: $this->_states['body'] .= $token[1]; - break; + break; } $this->_stream->next(); } @@ -251,20 +251,21 @@ protected function _useNode() $as ? $this->_states['uses'][$alias] = $prefix . $use : $this->_states['uses'][$last] = $prefix . $use; $last = $alias = $use = ''; $as = false; - break; + break; case T_STRING: $last = $token[1]; + /* Always prefix */ case T_NS_SEPARATOR: $as ? $alias .= $token[1] : $use .= $token[1]; - break; + break; case T_AS: $as = true; - break; + break; case '{': $prefix = $use; $use = ''; $stop = '}'; - break; + break; } } $this->_states['body'] .= $token[0]; @@ -475,27 +476,28 @@ protected function _parseArgs() $value .= $token[1]; } $cpt++; - break; + break; case '=': $name = $value; $value = ''; - break; + break; case ')': $cpt--; if ($cpt) { $value .= $token[1]; break; } + /* Same behavior as comma */ case ',': $value = trim($value); if ($value !== '') { $name ? $args[trim($name)] = $value : $args[] = $value; } $name = $value = ''; - break; + break; default: $value .= $token[1]; - break; + break; } if ($token[1] === ')' && $cpt === 0) { break; @@ -595,7 +597,7 @@ protected function _initLines($content) $lines = explode("\n", $content); $nbLines = count($lines); if ($this->_states['lines']) { - for($i = 0; $i < $nbLines; $i++) { + for ($i = 0; $i < $nbLines; $i++) { $this->_root->lines['content'][$i] = [ 'body' => $lines[$i], 'nodes' => [], @@ -611,7 +613,8 @@ protected function _initLines($content) * @param object $node The node to match. * @param string $body The to match. */ - protected function _assignLines($node) { + protected function _assignLines($node) + { if (!$this->_states['lines']) { return; } @@ -635,7 +638,8 @@ protected function _assignLines($node) { * @param object $node The node to match. * @param string $body The to match. */ - protected function _assignLine($index, $node, $line) { + protected function _assignLine($index, $node, $line) + { if ($node->lines['start'] === null) { $node->lines['start'] = $index; } @@ -648,7 +652,8 @@ protected function _assignLine($index, $node, $line) { /** * Assign coverable data to lines. */ - protected function _assignCoverable() { + protected function _assignCoverable() + { if (!$this->_states['lines']) { return; } @@ -664,7 +669,8 @@ protected function _assignCoverable() { * @param integer $index The line to check. * @return boolean */ - protected function _isCoverable($index) { + protected function _isCoverable($index) + { $coverable = false; foreach ($this->_root->lines['content'][$index]['nodes'] as $node) { if ($node->coverable && ($node->lines['stop'] === $index)) { diff --git a/src/Jit/Patcher/Layer.php b/src/Jit/Patcher/Layer.php index ced0bfef..fa430d73 100644 --- a/src/Jit/Patcher/Layer.php +++ b/src/Jit/Patcher/Layer.php @@ -1,9 +1,10 @@ _override) { return; } - $this->_processTree($node->tree); + $this->_processTree($node); return $node; } /** * Helper for `Layer::process()`. * - * @param array $nodes A array of nodes to patch. + * @param array $parent The node instance tor process. */ - protected function _processTree($nodes) + protected function _processTree($parent) { - foreach ($nodes as $node) { + foreach ($parent->tree as $node) { if ($node->processable && $node->type === 'class' && $node->extends) { $namespace = $node->namespace->name . '\\'; $parent = $node->extends; @@ -120,7 +121,7 @@ protected function _processTree($nodes) $pattern = preg_quote($parent); $node->body = preg_replace("~(extends\s+){$pattern}~", "\\1{$layerClass}", $node->body); - $code = Stub::generate([ + $code = Double::generate([ 'class' => $layerClass, 'extends' => $extends, 'openTag' => false, @@ -129,13 +130,11 @@ protected function _processTree($nodes) ]); $parser = $this->_classes['parser']; - $nodes = $parser::parse($code, ['php' => true]); - $node->close .= str_replace("\n", '', $parser::unparse($this->_pointcut->process($nodes))); - + $root = $parser::parse($code, ['php' => true]); + $node->close .= str_replace("\n", '', $parser::unparse($this->_pointcut->process($root))); } elseif (count($node->tree)) { - $this->_processTree($node->tree); + $this->_processTree($node); } } } - } diff --git a/src/Jit/Patcher/Monkey.php b/src/Jit/Patcher/Monkey.php index ea1e6196..45b5cf0e 100644 --- a/src/Jit/Patcher/Monkey.php +++ b/src/Jit/Patcher/Monkey.php @@ -1,37 +1,17 @@ 'Kahlan\Jit\Node\NodeDef', - ]; - - /** - * Prefix to use for custom variable name. - * - * @var string - */ - protected $_prefix = ''; - - /** - * Counter for building unique variable name. - * - * @var integer - */ - protected $_counter = 0; - /** * Ignoring the following statements which are not valid function or class names. * * @var array */ - protected $_blacklist = [ + protected static $_blacklist = [ '__halt_compiler' => true, 'and' => true, 'array' => true, @@ -48,6 +28,9 @@ class Monkey 'extract' => true, 'for' => true, 'foreach' => true, + 'func_get_arg' => true, + 'func_get_args' => true, + 'func_num_args' => true, 'function' => true, 'if' => true, 'include' => true, @@ -63,11 +46,26 @@ class Monkey 'self' => true, 'static' => true, 'switch' => true, + 'throw' => true, 'unset' => true, 'while' => true, 'xor' => true ]; + /** + * Prefix to use for custom variable name. + * + * @var string + */ + protected $_prefix = ''; + + /** + * Counter for building unique variable name. + * + * @var integer + */ + protected $_counter = 0; + /** * Uses for the parsed node's namespace. * @@ -82,6 +80,20 @@ class Monkey */ protected $_variables = []; + /** + * Nested function depth level. + * + * @var integer + */ + protected $_depth = 0; + + /** + * The regex. + * + * @var string + */ + protected $_regex = null; + /** * The constructor. * @@ -91,13 +103,15 @@ class Monkey public function __construct($config = []) { $defaults = [ - 'classes' => [], 'prefix' => 'KMONKEY' ]; $config += $defaults; - $this->_classes += $config['classes']; $this->_prefix = $config['prefix']; + + $alpha = '[\\\a-zA-Z_\\x7f-\\xff]'; + $alphanum = '[\\\a-zA-Z0-9_\\x7f-\\xff]'; + $this->_regex = "/(new\s+)?(?|{$alphanum})(\s*)({$alpha}{$alphanum}*)(\s*)(?=\(|;|::{$alpha}{$alphanum}*\s*\()/m"; } /** @@ -133,7 +147,13 @@ public function patchable($class) */ public function process($node, $path = null) { - $this->_processTree($node->tree); + $this->_depth = 0; + $this->_variables[$this->_depth] = []; + $this->_processTree($node); + if ($this->_variables[$this->_depth]) { + $this->_flushVariables($node); + } + $this->_variables = []; return $node; } @@ -142,83 +162,184 @@ public function process($node, $path = null) * * @param array $nodes A array of nodes to patch. */ - protected function _processTree($nodes) + protected function _processTree($parent) { - $alpha = '[\\\a-zA-Z_\\x7f-\\xff]'; - $alphanum = '[\\\a-zA-Z0-9_\\x7f-\\xff]'; - $regex = "/(new\s+)?(?|{$alphanum})(\s*)({$alpha}{$alphanum}*)(\s*)(\(|;|::{$alpha}{$alphanum}*\s*\()/m"; - - foreach ($nodes as $node) { - $this->_variables = []; + $hasScope = $parent instanceof FunctionDef || $parent->type === 'namespace'; + if ($hasScope) { + $this->_variables[++$this->_depth] = []; + } + foreach ($parent->tree as $index => $node) { + if (count($node->tree)) { + $this->_processTree($node); + } if ($node->processable && $node->type === 'code') { $this->_uses = $node->namespace ? $node->namespace->uses : []; - $node->body = preg_replace_callback($regex, [$this, '_patchNode'], $node->body); - $code = $this->_classes['node']; - $body = ''; - - if ($this->_variables) { - foreach ($this->_variables as $variable) { - $body .= $variable['name'] . $variable['patch']; - } - $parent = $node->function ?: $node->parent; - if (!$parent->inPhp) { - $body = ''; - } - $patch = new $code($body, 'code'); - $patch->parent = $parent; - $patch->function = $node->function; - $patch->namespace = $node->namespace; - array_unshift($parent->tree, $patch); - } - } - if (count($node->tree)) { - $this->_processTree($node->tree); + $this->_monkeyPatch($node, $parent, $index); } } + if ($hasScope) { + $this->_flushVariables($parent); + $this->_depth--; + } } /** - * Helper for `Monkey::_processTree()`. + * Flush stored variables in the passed node. * - * @param array $matches An array of calls to patch. - * @return string The patched code. + * @param array $node The node to store variables in. */ - protected function _patchNode($matches) + protected function _flushVariables($node) { - $name = $matches[3]; + if (!$this->_variables[$this->_depth]) { + return; + } - $static = preg_match('/^::/', $matches[5]); + $body = ''; + foreach ($this->_variables[$this->_depth] as $variable) { + if ($variable['isClass']) { + $body .= $variable['name'] . '__=null;'; + } + $body .= $variable['name'] . $variable['patch']; + } - if (isset($this->_blacklist[strtolower($name)]) || (!$matches[1] && $matches[5] !== '(' && !$static)) { - return $matches[0]; + if (!$node->inPhp) { + $body = ''; } - $tokens = explode('\\', $name, 2); + $patch = new NodeDef($body, 'code'); + $patch->parent = $node; + $patch->function = $node->function; + $patch->namespace = $node->namespace; + array_unshift($node->tree, $patch); + $this->_variables[$this->_depth] = []; + } - if ($name[0] === '\\') { - $name = substr($name, 1); - $args = "null , '{$name}'"; - } elseif (isset($this->_uses[$tokens[0]])) { - $ns = $this->_uses[$tokens[0]]; - if (count($tokens) === 2) { - $ns .= '\\' . $tokens[1]; - } - $args = "null, '" . $ns . "'"; - } else { - $isFunc = $matches[1] || $static ? 'false' : 'true'; - $args = "__NAMESPACE__ , '{$name}', {$isFunc}"; + /** + * Monkey patch a node body. + * + * @param object $node The node to monkey patch. + * @param array $parent The parent array. + * @param integer $index The index of node in parent children. + */ + protected function _monkeyPatch($node, $parent, $index) + { + if (!preg_match_all($this->_regex, $node->body, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) { + return; } + $offset = 0; + foreach (array_reverse($matches) as $match) { + $len = strlen($match[0][0]); + $pos = $match[0][1]; + $name = $match[3][0]; + + $nextChar = $node->body[$pos + $len]; + + $isInstance = !!$match[1][0]; + $isClass = $nextChar === ':' || $isInstance; + + if (!isset(static::$_blacklist[strtolower($name)]) && ($isClass || $nextChar === '(')) { + $tokens = explode('\\', $name, 2); + + if ($name[0] === '\\') { + $name = substr($name, 1); + $args = "null , '{$name}'"; + } elseif (isset($this->_uses[$tokens[0]])) { + $ns = $this->_uses[$tokens[0]]; + if (count($tokens) === 2) { + $ns .= '\\' . $tokens[1]; + } + $args = "null, '" . $ns . "'"; + } else { + $args = "__NAMESPACE__ , '{$name}'"; + } - if (!isset($this->_variables[$name])) { - $variable = '$__' . $this->_prefix . '__' . $this->_counter++; - $this->_variables[$name]['name'] = $variable; - $this->_variables[$name]['patch'] = " = \Kahlan\Plugin\Monkey::patched({$args});"; - } else { - $variable = $this->_variables[$name]['name']; + if (!isset($this->_variables[$this->_depth][$name])) { + $variable = '$__' . $this->_prefix . '__' . $this->_counter++; + + if ($isClass) { + $args .= ', false, ' . $variable . '__'; + } + + $this->_variables[$this->_depth][$name] = [ + 'name' => $variable, + 'isClass' => $isClass, + 'patch' => "=\Kahlan\Plugin\Monkey::patched({$args});" + ]; + } else { + $variable = $this->_variables[$this->_depth][$name]['name']; + } + $substitute = $variable . '__'; + if (!$isInstance) { + $replace = $match[2][0] . $variable . $match[4][0]; + } else { + $added = $this->_addClosingParenthesis($pos + $len, $index, $parent); + if ($added) { + $replace = '(' . $substitute . '?' . $substitute . ':' . $match[1][0] . $match[2][0] . $variable . $match[4][0]; + } else { + $replace = $match[1][0] . $match[2][0] . $variable . $match[4][0]; + } + } + $node->body = substr_replace($node->body, $replace, $pos, $len); + $offset = $pos + strlen($replace); + } else { + $offset = $pos + $len; + } } + } + + /** + * Add a closing parenthesis + * + * @param object $node The node to monkey patch. + * @param array $parent The parent array. + * @param integer $index The index of node in parent children. + * @return boolean Returns `true` if succeed, `false` otherwise. + */ + protected function _addClosingParenthesis($pos, $index, $parent) + { + $count = 0; + $nodes = $parent->tree; + $total = count($nodes); - return $matches[1] . $matches[2] . $variable . $matches[4] . $matches[5]; + for ($i = $index; $i < $total; $i++) { + $node = $nodes[$i]; + if (!$node->processable || $node->type !== 'code') { + continue; + } + $code = $node->body; + $len = strlen($code); + while ($pos < $len) { + if ($count === 0 && $code[$pos] === ';') { + $node->body = substr_replace($code, ');', $pos, 1); + return true; + } elseif ($code[$pos] === '(' || $code[$pos] === '{') { + $count++; + } elseif ($code[$pos] === ')' || $code[$pos] === '}') { + $count--; + if ($count === 0) { + $node->body = substr_replace($code, $code[$pos] . ')', $pos, 1); + return true; + } + } + $pos++; + } + $pos = 0; + } + return false; } + /** + * Check if a function is part of the blacklisted ones. + * + * @param string $name A function name. + * @return boolean + */ + public static function blacklisted($name = null) + { + if (!func_num_args()) { + return array_keys(static::$_blacklist); + } + return isset(static::$_blacklist[strtolower($name)]); + } } diff --git a/src/Jit/Patcher/Pointcut.php b/src/Jit/Patcher/Pointcut.php index a158242e..2519c666 100644 --- a/src/Jit/Patcher/Pointcut.php +++ b/src/Jit/Patcher/Pointcut.php @@ -71,22 +71,22 @@ public function patchable($class) */ public function process($node, $path = null) { - $this->_processTree($node->tree); + $this->_processTree($node); return $node; } /** * Helper for `Pointcut::process()`. * - * @param array $nodes A node array to patch. + * @param array $parent The node instance tor process. */ - protected function _processTree($nodes) + protected function _processTree($parent) { - foreach ($nodes as $node) { + foreach ($parent->tree as $node) { if ($node->hasMethods && $node->type !== 'interface') { - $this->_processMethods($node->tree); + $this->_processMethods($node); } elseif (count($node->tree)) { - $this->_processTree($node->tree); + $this->_processTree($node); } } } @@ -94,11 +94,11 @@ protected function _processTree($nodes) /** * Helper for `Pointcut::process()`. * - * @param object The node instance to patch. + * @param array $parent The node instance tor process. */ - protected function _processMethods($node) + protected function _processMethods($parent) { - foreach ($node as $child) { + foreach ($parent->tree as $child) { if (!$child->processable) { continue; } @@ -128,7 +128,6 @@ protected function _before($generator) { $prefix = $this->_prefix; $statement = $generator ? 'yield' : 'return'; - return "\$__{$prefix}_ARGS__ = func_get_args(); \$__{$prefix}_SELF__ = isset(\$this) ? \$this : get_called_class(); if (\$__{$prefix}__ = \Kahlan\Plugin\Pointcut::before(__METHOD__, \$__{$prefix}_SELF__, \$__{$prefix}_ARGS__)) { \$r = \$__{$prefix}__(\$__{$prefix}_SELF__, \$__{$prefix}_ARGS__); {$statement} \$r; }"; + return "\$__{$prefix}_ARGS__ = func_get_args(); \$__{$prefix}_SELF__ = isset(\$this) ? \$this : get_called_class(); if (\$__{$prefix}__ = \Kahlan\Plugin\Pointcut::before(__METHOD__, \$__{$prefix}_SELF__, \$__{$prefix}_ARGS__)) { \$r = \$__{$prefix}__(\$__{$prefix}_ARGS__, \$__{$prefix}_SELF__); {$statement} \$r; }"; } - } diff --git a/src/Jit/Patcher/Quit.php b/src/Jit/Patcher/Quit.php index 1f0cd02e..da818516 100644 --- a/src/Jit/Patcher/Quit.php +++ b/src/Jit/Patcher/Quit.php @@ -1,7 +1,8 @@ _processTree($node->tree); + $this->_processTree($node); return $node; } /** * Helper for `Quit::process()`. * - * @param array $nodes A array of nodes to patch. + * @param array $parent The node instance tor process. */ - protected function _processTree($nodes) + protected function _processTree($parent) { $alphanum = '[\\\a-zA-Z0-9_\\x7f-\\xff]'; $regex = "/(?|{$alphanum})(\s*)((?:exit|die)\s*\()/m"; - foreach ($nodes as $node) { + foreach ($parent->tree as $node) { if ($node->processable && $node->type === 'code') { $node->body = preg_replace($regex, '\1\Kahlan\Plugin\Quit::quit(', $node->body); } if (count($node->tree)) { - $this->_processTree($node->tree); + $this->_processTree($node); } } } - } diff --git a/src/Jit/Patcher/Rebase.php b/src/Jit/Patcher/Rebase.php index 0d98a8da..975fc6b0 100644 --- a/src/Jit/Patcher/Rebase.php +++ b/src/Jit/Patcher/Rebase.php @@ -1,7 +1,8 @@ _processTree($node->tree, $path); + $this->_processTree($node, $path); return $node; } /** * Helper for `Rebase::process()`. * - * @param array $nodes An array of nodes to patch. - * @param string $path The file path of the source code. + * @param array $parent The node instance tor process. + * @param string $path The file path of the source code. */ - protected function _processTree($nodes, $path) + protected function _processTree($parent, $path) { $path = addcslashes($path, "'"); $dir = "'" . dirname($path) . "'"; @@ -56,15 +57,14 @@ protected function _processTree($nodes, $path) $dirRegex = "/(?|{$alphanum})(\s*)(__DIR__)/"; $fileRegex = "/(?|{$alphanum})(\s*)(__FILE__)/"; - foreach ($nodes as $node) { + foreach ($parent->tree as $node) { if ($node->processable && $node->type === 'code') { $node->body = preg_replace($dirRegex, $dir, $node->body); $node->body = preg_replace($fileRegex, $file, $node->body); } if (count($node->tree)) { - $this->_processTree($node->tree, $path); + $this->_processTree($node, $path); } } } - } diff --git a/src/Jit/Patchers.php b/src/Jit/Patchers.php index 432f2b4a..0620e5e3 100644 --- a/src/Jit/Patchers.php +++ b/src/Jit/Patchers.php @@ -6,7 +6,8 @@ /** * Patcher manager */ -class Patchers { +class Patchers +{ /** * The registered patchers. @@ -120,5 +121,4 @@ public function process($code, $path = null) } return Parser::unparse($nodes); } - } diff --git a/src/Jit/TokenStream.php b/src/Jit/TokenStream.php index 97da194b..38fc9ae3 100644 --- a/src/Jit/TokenStream.php +++ b/src/Jit/TokenStream.php @@ -309,7 +309,8 @@ public function skipWhile($skips = []) * @param array $skips The elements array to skip. * @return The skipped string. */ - protected function _skip($skips) { + protected function _skip($skips) + { $skipped = ''; $count = $this->count(); while ($this->_current < $count) { @@ -416,5 +417,4 @@ public function __toString() { return $this->source(); } - -} \ No newline at end of file +} diff --git a/src/Report.php b/src/Log.php similarity index 72% rename from src/Report.php rename to src/Log.php index 0fe1d194..5c4b37e8 100644 --- a/src/Report.php +++ b/src/Log.php @@ -3,7 +3,7 @@ use Kahlan\Analysis\Debugger; -class Report +class Log { /** * The scope context instance. @@ -62,11 +62,11 @@ class Report protected $_matcherName = null; /** - * The matcher params. + * The matcher data. * * @var array */ - protected $_params = []; + protected $_data = []; /** * The related exception. @@ -75,12 +75,19 @@ class Report */ protected $_exception = null; + /** + * The backtrace. + * + * @var array + */ + protected $_backtrace = []; + /** * The reports of executed expectations. * * @var array */ - protected $_childs = []; + protected $_children = []; /** * The Constructor. @@ -92,12 +99,12 @@ public function __construct($config = []) { $defaults = [ 'scope' => null, - 'type' => 'pass', + 'type' => 'passed', 'not' => false, 'description' => null, 'matcher' => null, 'matcherName' => null, - 'params' => [], + 'data' => [], 'backtrace' => [], 'exception' => null ]; @@ -109,14 +116,12 @@ public function __construct($config = []) $this->_description = $config['description']; $this->_matcher = $config['matcher']; $this->_matcherName = $config['matcherName']; - $this->_params = $config['params']; - $this->_backtrace = $config['backtrace']; - $this->_exception = $config['exception']; - - if ($this->_backtrace) { - $trace = reset($this->_backtrace); - $this->_file = preg_replace('~' . preg_quote(getcwd(), '~') . '~', '', $trace['file']); - $this->_line = $trace['line']; + $this->_data = $config['data']; + $this->exception($config['exception']); + if ($config['backtrace']) { + $this->backtrace($config['backtrace']); + } elseif ($this->scope()) { + $this->backtrace($this->scope()->backtrace()); } } @@ -135,9 +140,23 @@ public function scope() * * @return string */ - public function type() + public function type($type = null) { - return $this->_type; + if (!func_num_args()) { + return $this->_type; + } + $this->_type = $type; + return $this; + } + + /** + * Return the state of the log. + * + * @return boolean + */ + public function passed() + { + return $this->_type !== 'failed' && $this->_type !== 'errored'; } /** @@ -181,33 +200,45 @@ public function matcherName() } /** - * Gets the matcher params. + * Gets the matcher data. * * @return array */ - public function params() + public function data() { - return $this->_params; + return $this->_data; } /** - * Gets the backtrace related to the report. + * Gets the exception related to the report. * - * @return array + * @return object */ - public function backtrace() + public function exception($exception = null) { - return $this->_backtrace; + if (!func_num_args()) { + return $this->_exception; + } + $this->_exception = $exception; + return $this; } /** - * Gets the exception related to the report. + * Gets the backtrace related to the report. * - * @return object + * @return array */ - public function exception() + public function backtrace($backtrace = []) { - return $this->_exception; + if (!func_num_args()) { + return $this->_backtrace; + } + if ($this->_backtrace = $backtrace) { + $trace = reset($this->_backtrace); + $this->_file = preg_replace('~' . preg_quote(getcwd(), '~') . '~', '', '.' . $trace['file']); + $this->_line = $trace['line']; + } + return $this; } /** @@ -245,9 +276,9 @@ public function messages() * * @return array The executed expectations reports. */ - public function childs() + public function children() { - return $this->_childs; + return $this->_children; } /** @@ -257,33 +288,17 @@ public function childs() */ public function add($type, $data = []) { - $data['type'] = $type; - if ($type !== 'pass' && $type !== 'skip') { - $this->scope()->failure(); - } - - $data['backtrace'] = $this->_backtrace($data); - $this->_type = ($data['type'] !== 'pass') ? $data['type'] : 'pass'; - $child = new static($data + ['scope' => $this->_scope]); - $this->_childs[] = $child; - $this->scope()->dispatch($child); - } - - /** - * Helper which extracts the backtrace of a report. - * - * @param array $data The report data. - */ - public function _backtrace($data) - { - if (isset($data['exception'])) { - return Debugger::backtrace(['trace' => $data['exception']]); + if ($this->type() === 'passed' && $type === 'failed') { + $this->type('failed'); } - $type = $data['type']; - $depth = ($type === 'pass' || $type === 'fail' | $type === 'skip') ? 1 : null; + $data['type'] = $type; if (!isset($data['backtrace'])) { $data['backtrace'] = []; + } else { + $data['backtrace'] = Debugger::focus($this->scope()->backtraceFocus(), $data['backtrace'], 1); } - return Debugger::focus($this->scope()->backtraceFocus(), $data['backtrace'], $depth); + $child = new static($data + ['scope' => $this->_scope]); + $this->_children[] = $child; + return $child; } } diff --git a/src/Matcher/AnyException.php b/src/Matcher/AnyException.php index f75085a2..a9ae083e 100644 --- a/src/Matcher/AnyException.php +++ b/src/Matcher/AnyException.php @@ -20,7 +20,6 @@ class AnyException extends \Exception public function __construct($message = null, $code = 0, $previous = null) { $this->message = $message; - $this->code = $code; + $this->code = $code; } - } diff --git a/src/Matcher/ToBe.php b/src/Matcher/ToBe.php index b7202b6f..30d34fce 100644 --- a/src/Matcher/ToBe.php +++ b/src/Matcher/ToBe.php @@ -24,5 +24,4 @@ public static function description() { return "be identical to expected (===)."; } - } diff --git a/src/Matcher/ToBeA.php b/src/Matcher/ToBeA.php index 66bae53a..277898bf 100644 --- a/src/Matcher/ToBeA.php +++ b/src/Matcher/ToBeA.php @@ -66,9 +66,9 @@ public static function expected($expected) public static function _buildDescription($actual, $expected) { $description = "have the expected type."; - $params['actual'] = $actual; - $params['expected'] = $expected; - static::$_description = compact('description', 'params'); + $data['actual'] = $actual; + $data['expected'] = $expected; + static::$_description = compact('description', 'data'); } /** @@ -78,5 +78,4 @@ public static function description() { return static::$_description; } - } diff --git a/src/Matcher/ToBeAnInstanceOf.php b/src/Matcher/ToBeAnInstanceOf.php index 3b6feeb2..41a20b20 100644 --- a/src/Matcher/ToBeAnInstanceOf.php +++ b/src/Matcher/ToBeAnInstanceOf.php @@ -24,5 +24,4 @@ public static function description() { return "be an instance of expected."; } - } diff --git a/src/Matcher/ToBeCalled.php b/src/Matcher/ToBeCalled.php new file mode 100644 index 00000000..9e05849d --- /dev/null +++ b/src/Matcher/ToBeCalled.php @@ -0,0 +1,209 @@ +_actual = $actual; + Suite::register(Suite::hash($actual)); + $this->_message = new Message(['name' => $actual]); + $this->_backtrace = Debugger::backtrace(); + } + + /** + * Sets arguments requirement. + * + * @param mixed ... <0,n> Argument(s). + * @return self + */ + public function with() + { + call_user_func_array([$this->_message, 'with'], func_get_args()); + return $this; + } + + /** + * Sets the number of occurences. + * + * @return self + */ + public function once() + { + $this->times(1); + return $this; + } + + /** + * Gets/sets the number of occurences. + * + * @param integer $times The number of occurences to set or none to get it. + * @return mixed The number of occurences on get or `self` otherwise. + */ + public function times($times = null) + { + if (!func_num_args()) { + return $this->_times; + } + $this->_times = $times; + return $this; + } + + /** + * Magic getter, if called with `'ordered'` will set ordered to `true`. + * + * @param string + */ + public function __get($name) + { + if ($name !== 'ordered') { + throw new Exception("Unsupported attribute `{$name}` only `ordered` is available."); + } + $this->_ordered = true; + return $this; + } + + /** + * Resolves the matching. + * + * @return boolean Returns `true` if successfully resolved, `false` otherwise. + */ + public function resolve() + { + $startIndex = $this->_ordered ? Calls::lastFindIndex() : 0; + $report = Calls::find($this->_message, $startIndex, $this->times()); + $this->_report = $report; + $this->_buildDescription($startIndex); + return $report['success']; + } + + /** + * Gets the backtrace reference. + * + * @return object + */ + public function backtrace() + { + return $this->_backtrace; + } + + /** + * Build the description of the runned `::match()` call. + * + * @param mixed $startIndex The startIndex in calls log. + */ + public function _buildDescription($startIndex = 0) + { + $with = $this->_message->args(); + $times = $this->times(); + + $report = $this->_report; + + $expectedTimes = $times ? ' the expected times' : ''; + $expectedParameters = $with ? ' with expected parameters' : ''; + + $this->_description['description'] = "be called{$expectedParameters}{$expectedTimes}."; + + $calledTimes = count($report['args']); + + $this->_description['data']['actual'] = $this->_actual . '()'; + $this->_description['data']['actual called times'] = $calledTimes; + + if ($calledTimes && $with !== null) { + $this->_description['data']['actual called parameters list'] = $report['args']; + } + + $this->_description['data']['expected to be called'] = $this->_actual . '()'; + + if ($with !== null) { + $this->_description['data']['expected parameters'] = $with; + } + + if ($times) { + $this->_description['data']['expected called times'] = $times; + } + } + + /** + * Returns the description report. + */ + public function description() + { + return $this->_description; + } +} diff --git a/src/Matcher/ToBeCloseTo.php b/src/Matcher/ToBeCloseTo.php index d08fcbbf..b9501c2e 100644 --- a/src/Matcher/ToBeCloseTo.php +++ b/src/Matcher/ToBeCloseTo.php @@ -38,10 +38,10 @@ public static function match($actual, $expected, $precision = 2) public static function _buildDescription($actual, $expected, $precision) { $description = "be close to expected relying to a precision of {$precision}."; - $params['actual'] = $actual; - $params['expected'] = $expected; - $params['gap is >='] = pow(10, -$precision) / 2; - static::$_description = compact('description', 'params'); + $data['actual'] = $actual; + $data['expected'] = $expected; + $data['gap is >='] = pow(10, -$precision) / 2; + static::$_description = compact('description', 'data'); } /** @@ -51,5 +51,4 @@ public static function description() { return static::$_description; } - } diff --git a/src/Matcher/ToContainKey.php b/src/Matcher/ToContainKey.php index 1ff1c19b..7416e796 100644 --- a/src/Matcher/ToContainKey.php +++ b/src/Matcher/ToContainKey.php @@ -15,19 +15,19 @@ class ToContainKey */ public static function match($actual, $expected) { - $params = func_get_args(); - $expected = count($params) > 2 ? array_slice($params, 1) : $expected; + $args = func_get_args(); + $expected = count($args) > 2 ? array_slice($args, 1) : $expected; $expected = (array) $expected; if (is_array($actual)) { - foreach($expected as $key) { + foreach ($expected as $key) { if (!array_key_exists($key, $actual)) { return false; } } return true; } elseif ($actual instanceof ArrayAccess) { - foreach($expected as $key) { + foreach ($expected as $key) { if (!isset($actual[$key])) { return false; } diff --git a/src/Matcher/ToEcho.php b/src/Matcher/ToEcho.php index 944aa6b6..ea519dd3 100644 --- a/src/Matcher/ToEcho.php +++ b/src/Matcher/ToEcho.php @@ -49,9 +49,9 @@ public static function actual($actual) public static function _buildDescription($actual, $expected) { $description = "echo the expected string."; - $params['actual'] = $actual; - $params['expected'] = $expected; - static::$_description = compact('description', 'params'); + $data['actual'] = $actual; + $data['expected'] = $expected; + static::$_description = compact('description', 'data'); } /** diff --git a/src/Matcher/ToHaveLength.php b/src/Matcher/ToHaveLength.php index 203c6555..5129825f 100644 --- a/src/Matcher/ToHaveLength.php +++ b/src/Matcher/ToHaveLength.php @@ -52,11 +52,11 @@ public static function actual($actual) public static function _buildDescription($actual, $length, $expected) { $description = "have the expected length."; - $params['actual'] = $actual; - $params['actual length'] = $length; - $params['expected length'] = $expected; + $data['actual'] = $actual; + $data['actual length'] = $length; + $data['expected length'] = $expected; - static::$_description = compact('description', 'params'); + static::$_description = compact('description', 'data'); } /** diff --git a/src/Matcher/ToMatchEcho.php b/src/Matcher/ToMatchEcho.php index 5d93ea89..3f84677d 100644 --- a/src/Matcher/ToMatchEcho.php +++ b/src/Matcher/ToMatchEcho.php @@ -30,11 +30,10 @@ public static function match($actual, $expected = null) */ public static function _buildDescription($actual, $expected) { - $description = "matches expected regex in echoed string."; - $params['actual'] = $actual; - $params['expected'] = $expected; + $description = "matches expected regex in echoed string."; + $data['actual'] = $actual; + $data['expected'] = $expected; - static::$_description = compact('description', 'params'); + static::$_description = compact('description', 'data'); } - } diff --git a/src/Matcher/ToReceive.php b/src/Matcher/ToReceive.php index 9ecb542c..d9b2e22b 100644 --- a/src/Matcher/ToReceive.php +++ b/src/Matcher/ToReceive.php @@ -1,60 +1,67 @@ 'Kahlan\Plugin\Call' - ]; + protected $_messages = []; /** - * A fully-namespaced class name or an object instance. + * The expected method method name to be called. * - * @var string|object + * @var array */ - protected $_actual = null; + protected $_expected = []; /** - * The expected method method name to be called. + * The expectation backtrace reference. * - * @var string + * @var array */ - protected $_expected = null; + protected $_backtrace = null; /** - * The expectation backtrace reference. + * The report. * * @var array */ - protected $_backtrace = null; + protected $_report = []; /** - * The call instance. + * The description report. * - * @var object + * @var array */ - protected $_call = null; + protected $_description = []; /** - * The message instance. + * Number of occurences to match. * - * @var object + * @var integer */ - protected $_message = null; + protected $_times = 0; /** - * The description report. + * If `true`, will take the calling order into account. * - * @var array + * @var boolean */ - protected $_description = []; + protected $_ordered = false; /** * Checks that `$actual` receive the `$expected` message. @@ -63,10 +70,12 @@ class ToReceive * @param mixed $expected The expected message. * @return boolean */ - public static function match($actual, $expected) + public static function match($actual, $expected = null) { $class = get_called_class(); - return new static($actual, $expected); + $args = func_get_args(); + $actual = array_shift($args); + return new static($actual, $args); } /** @@ -77,51 +86,201 @@ public static function match($actual, $expected) */ public function __construct($actual, $expected) { - if (preg_match('/^::.*/', $expected)) { - $actual = is_object($actual) ? get_class($actual) : $actual; + $this->_backtrace = Debugger::backtrace(); + + if (is_string($actual)) { + $actual = ltrim($actual, '\\'); } - $call = $this->_classes['call']; - $this->_actual = $actual; - $this->_expected = $expected; - $this->_call = new $call($actual); - $this->_message = $this->_call->method($expected); - $this->_backtrace = Debugger::backtrace(); + $this->_check($actual); + + if (!$expected) { + throw new InvalidArgumentException("Method name can't be empty."); + } + + $names = is_array($expected) ? $expected : [$expected]; + + $reference = $actual; + + if (count($names) > 1) { + if (!Stub::registered(Suite::hash($reference))) { + throw new InvalidArgumentException("Kahlan can't Spy chained methods on real PHP code, you need to Stub the chain first."); + } + } + + $reference = $this->_reference($reference); + + foreach ($names as $index => $name) { + if (preg_match('/^::.*/', $name)) { + $reference = is_object($reference) ? get_class($reference) : $reference; + } + $this->_expected[] = $name; + $this->_messages[$name] = $this->_watch(new Message([ + 'parent' => $this, + 'reference' => $reference, + 'name' => $name + ])); + $reference = null; + } } /** - * Delegates calls to the message instance. + * Check the actual value can receive messages. * - * @param string $method The method name. - * @param array $params The parameters to passe. - * @return mixed The message instance response. + * @param mixed $reference An instance or a fully-namespaced class name. */ - public function __call($method, $params) + protected function _check($reference) { - return call_user_func_array([$this->_message, $method], $params); + $isString = is_string($reference); + if ($isString) { + if (!class_exists($reference)) { + throw new InvalidArgumentException("Can't Spy the unexisting class `{$reference}`."); + } + $reflection = Inspector::inspect($reference); + } else { + $reflection = Inspector::inspect(get_class($reference)); + } + + if (!$reflection->isInternal()) { + return; + } + if (!$isString) { + throw new InvalidArgumentException("Can't Spy built-in PHP instances, create a test double using `Double::instance()`."); + } } /** - * Resolves the matching. + * Return the actual reference which must be used. * - * @return boolean Returns `true` if successfully resolved, `false` otherwise. + * @param mixed $reference An instance or a fully-namespaced class name. + * @param mixed The reference or the monkey patched one if exist. */ - public function resolve() + protected function _reference($reference) { - $call = $this->_classes['call']; - $success = !!$call::find($this->_actual, $this->_message, 0, $this->_message->times()); - $this->_buildDescription(); - return $success; + if (!is_string($reference)) { + return $reference; + } + + $pos = strrpos($reference, '\\'); + if ($pos !== false) { + $namespace = substr($reference, 0, $pos); + $basename = substr($reference, $pos + 1); + } else { + $namespace = null; + $basename = $reference; + } + $substitute = null; + $reference = Monkey::patched($namespace, $basename, false, $substitute); + + return $substitute ?: $reference; } /** - * Gets the message instance. + * Watch a message. * - * @return object + * @param string|object $actual A fully-namespaced class name or an object instance. + * @param string $method The expected method method name to be called. + * @param object A message instance. + */ + protected function _watch($message) + { + $reference = $message->reference(); + if (!$reference) { + Suite::register($message->name()); + return $message; + } + if (is_object($reference)) { + Suite::register(get_class($reference)); + } + Suite::register(Suite::hash($reference)); + return $message; + } + + /** + * Sets arguments requirement. + * + * @param mixed ... <0,n> Argument(s). + * @return self + */ + public function with() + { + $message = end($this->_messages); + call_user_func_array([$message, 'with'], func_get_args()); + return $this; + } + + /** + * Set arguments requirement indexed by method name. + * + * @param mixed ... <0,n> Argument(s). + * @return self + */ + public function where($requirements = []) + { + foreach ($requirements as $name => $args) { + if (!isset($this->_messages[$name])) { + throw new InvalidArgumentException("Unexisting `{$name}` as method as part of the chain definition."); + } + if (!is_array($args)) { + throw new InvalidArgumentException("Argument requirements must be an arrays for `{$name}` method."); + } + call_user_func_array([$this->_messages[$name], 'with'], $args); + } + return $this; + } + + /** + * Sets the number of occurences. + * + * @return self + */ + public function once() + { + $this->times(1); + return $this; + } + + /** + * Gets/sets the number of occurences. + * + * @param integer $times The number of occurences to set or none to get it. + * @return mixed The number of occurences on get or `self` otherwise. */ - public function message() + public function times($times = null) { - return $this->_message; + if (!func_num_args()) { + return $this->_times; + } + $this->_times = $times; + return $this; + } + + /** + * Magic getter, if called with `'ordered'` will set ordered to `true`. + * + * @param string + */ + public function __get($name) + { + if ($name !== 'ordered') { + throw new Exception("Unsupported attribute `{$name}` only `ordered` is available."); + } + $this->_ordered = true; + return $this; + } + + /** + * Resolves the matching. + * + * @return boolean Returns `true` if successfully resolved, `false` otherwise. + */ + public function resolve() + { + $startIndex = $this->_ordered ? Calls::lastFindIndex() : 0; + $report = Calls::find($this->_messages, $startIndex, $this->times()); + $this->_report = $report; + $this->_buildDescription($startIndex); + return $report['success']; } /** @@ -141,29 +300,43 @@ public function backtrace() */ public function _buildDescription($startIndex = 0) { - $call = $this->_classes['call']; + $times = $this->times(); - $with = $this->_message->params(); - $this->_message->with(); + $report = $this->_report; + $reference = $report['message']->reference(); + $expected = $report['message']->name(); + $with = $report['message']->args(); - $times = $this->_message->times(); - if ($log = $call::find($this->_actual, $this->_message, $startIndex, $times)) { - $this->_description['description'] = 'receive correct parameters.'; - $this->_description['params']['actual with'] = $log['params']; - $this->_description['params']['expected with'] = $with; - return; + $expectedTimes = $times ? ' the expected times' : ''; + $expectedParameters = $with ? ' with expected parameters' : ''; + + $this->_description['description'] = "receive the expected method{$expectedParameters}{$expectedTimes}."; + + $calledTimes = count($report['args']); + + if (!$calledTimes) { + $logged = []; + foreach (Calls::logs($reference, $startIndex) as $log) { + $logged[] = $log['static'] ? '::' . $log['name'] : $log['name']; + } + $this->_description['data']['actual received calls'] = $logged; + } elseif ($calledTimes) { + $this->_description['data']['actual received'] = $expected; + $this->_description['data']['actual received times'] = $calledTimes; + if ($with !== null) { + $this->_description['data']['actual received parameters list'] = $report['args']; + } } - $this->_description['description'] = 'receive the correct message.'; - $called = []; - foreach($call::find($this->_actual, null, $startIndex) as $log) { - $called[] = $log['static'] ? '::' . $log['name'] : $log['name']; + $this->_description['data']['expected to receive'] = $expected; + + if ($with !== null) { + $this->_description['data']['expected parameters'] = $with; } - $this->_description['params']['actual received'] = $called; - $this->_description['params']['expected'] = $this->_expected; + if ($times) { - $this->_description['params']['called times'] = $times; + $this->_description['data']['expected received times'] = $times; } } @@ -174,5 +347,4 @@ public function description() { return $this->_description; } - } diff --git a/src/Matcher/ToReceiveNext.php b/src/Matcher/ToReceiveNext.php deleted file mode 100644 index a1234f01..00000000 --- a/src/Matcher/ToReceiveNext.php +++ /dev/null @@ -1,20 +0,0 @@ -_classes['call']; - $startIndex = $call::lastFindIndex(); - $success = !!$call::find($this->_actual, $this->_message, $startIndex, $this->_message->times()); - $this->_buildDescription($startIndex); - return $success; - } - -} diff --git a/src/Matcher/ToThrow.php b/src/Matcher/ToThrow.php index 597edb47..0a622546 100644 --- a/src/Matcher/ToThrow.php +++ b/src/Matcher/ToThrow.php @@ -100,7 +100,7 @@ public static function _matchException($actual, $exception) */ public static function _sameMessage($actual, $expected) { - if (preg_match('~^(?P\~|/|@|#).*?(?P=char)$~', $expected)) { + if (preg_match('~^(?P\~|/|@|#).*?(?P=char)$~', (string) $expected)) { $same = preg_match($expected, $actual); } else { $same = $actual === $expected; @@ -117,10 +117,10 @@ public static function _sameMessage($actual, $expected) public static function _buildDescription($actual, $expected) { $description = "throw a compatible exception."; - $params['actual'] = $actual; - $params['expected'] = $expected; + $data['actual'] = $actual; + $data['expected'] = $expected; - static::$_description = compact('description', 'params'); + static::$_description = compact('description', 'data'); } /** @@ -130,5 +130,4 @@ public static function description() { return static::$_description; } - } diff --git a/src/MissingImplementationException.php b/src/MissingImplementationException.php new file mode 100644 index 00000000..baa835f7 --- /dev/null +++ b/src/MissingImplementationException.php @@ -0,0 +1,6 @@ +message = $config; diff --git a/src/Plugin/Call.php b/src/Plugin/Call/Calls.php similarity index 59% rename from src/Plugin/Call.php rename to src/Plugin/Call/Calls.php index bc6420b7..ec19884d 100644 --- a/src/Plugin/Call.php +++ b/src/Plugin/Call/Calls.php @@ -1,10 +1,10 @@ _reference = $reference; - if (is_object($reference)) { - Suite::register(get_class($reference)); - } - Suite::register(Suite::hash($reference)); - } - - /** - * Enable logging for the passed method name. - * - * @param string $name The method name. - */ - public function method($name) - { - $static = false; - if (preg_match('/^::.*/', $name)) { - $static = true; - $name = substr($name, 2); - } - return $this->_message = new Message([ - 'reference' => $this->_reference, - 'static' => $static, - 'name' => $name - ]); - } - /** * Logs a call. * @@ -92,26 +45,44 @@ public static function log($reference, $call) * @param object|string $reference An instance or a fully-namespaced class name. * @param string $call The method name. */ - public static function _call($reference, $call) { + public static function _call($reference, $call) + { $static = false; if (preg_match('/^::.*/', $call['name'])) { $call['name'] = substr($call['name'], 2); $call['static'] = true; } if (is_object($reference)) { - $call += ['instance' => $reference, 'class' => get_class($reference), 'static' => $static]; + $call += ['instance' => $reference, 'class' => get_class($reference), 'static' => $static, 'method' => null]; + } elseif ($reference) { + $call += ['instance' => null, 'class' => $reference, 'static' => $static, 'method' => null]; } else { - $call += ['instance' => null, 'class' => $reference, 'static' => $static]; + $call += ['instance' => null, 'class' => null, 'static' => false, 'method' => null]; } return $call; } /** - * Returns Logged calls. + * Get all logs or all logs related to an instance or a fully-namespaced class name. + * + * @param object|string $reference An instance or a fully-namespaced class name. + * @param interger $index Start index. + * @return array The founded log calls. */ - public static function logs() + public static function logs($reference = null, $index = 0) { - return static::$_logs; + if (!func_num_args()) { + return static::$_logs; + } + $result = []; + $count = count(static::$_logs); + for ($i = $index; $i < $count; $i++) { + $logs = static::$_logs[$i]; + if ($log = static::_matchReference($reference, $logs)) { + $result[] = $log; + } + } + return $result; } /** @@ -131,16 +102,22 @@ public static function lastFindIndex($index = null) /** * Finds a logged call. * - * @param object|string $reference An instance or a fully-namespaced class name. - * @param string $method The method name. + * @param object $message The message method name. * @param interger $index Start index. * @return array|false Return founded log call. */ - public static function find($reference, $call = null, $index = 0, $times = 0) + public static function find($message, $index = 0, $times = 0) { - if ($call === null) { - return static::_findAll($reference, $index); - } + $success = false; + $messages = !is_array($message) ? [$message] : $message; + + $message = reset($messages); + $reference = $message->reference(); + $reference = $message->isStatic() && is_object($reference) ? get_class($reference) : $reference; + + $lastFound = null; + $args = []; + $count = count(static::$_logs); for ($i = $index; $i < $count; $i++) { @@ -149,42 +126,50 @@ public static function find($reference, $call = null, $index = 0, $times = 0) continue; } - if (!$call->match($log)) { + if (!$message->match($log, false)) { + continue; + } + $args[] = $log['args']; + + if (!$message->matchArgs($log['args'])) { continue; } + + if ($message = next($messages)) { + $lastFound = $message; + $args = []; + if (!$reference = $message->reference() && $log['method']) { + $reference = $log['method']->actualReturn(); + } + if (!is_object($reference)) { + $message = reset($messages); + $reference = $message->reference(); + } + $reference = $message->isStatic() && is_object($reference) ? get_class($reference) : $reference; + continue; + } + $times -= 1; if ($times < 0) { static::$_index = $i + 1; - return $log; + $success = true; + break; } elseif ($times === 0) { - if (!!static::find($reference, $call, $i + 1)) { - return false; + $next = static::find($messages, $i + 1); + if ($next['success']) { + $args = array_merge($args, $next['args']); + $success = false; + } else { + $success = true; + static::$_index = $i + 1; } - static::$_index = $i + 1; - return $log; + break; } + return static::find($messages, $i + 1, $times); } - return false; - } - - /** - * Helper for the `find()` method. - * - * @param object|string $reference An instance or a fully-namespaced class name. - * @param interger $index Start index. - * @return array The founded log calls. - */ - protected static function _findAll($reference, $index) - { - $result = []; - $count = count(static::$_logs); - for ($i = $index; $i < $count; $i++) { - $logs = static::$_logs[$i]; - if ($log = static::_matchReference($reference, $logs)) { - $result[] = $log; - } - } - return $result; + $index = static::$_index; + $message = $lastFound ?: reset($messages); + return compact('success', 'message', 'args', 'index'); } /** @@ -197,7 +182,11 @@ protected static function _findAll($reference, $index) protected static function _matchReference($reference, $logs = []) { foreach ($logs as $log) { - if (is_object($reference)) { + if (!$reference) { + if (empty($log['class']) && empty($log['instance'])) { + return $log; + } + } elseif (is_object($reference)) { if ($reference === $log['instance']) { return $log; } diff --git a/src/Plugin/Call/Message.php b/src/Plugin/Call/Message.php index 8d363c93..9d4cc9d8 100644 --- a/src/Plugin/Call/Message.php +++ b/src/Plugin/Call/Message.php @@ -13,124 +13,143 @@ class Message ]; /** - * Message name. + * Parent instance. * - * @var array + * @var mixed */ - protected $_name = null; + protected $_parent = null; /** - * Message params. + * Message reference. * - * @var array + * @var mixed */ - protected $_params = []; + protected $_reference = null; /** - * Static call. + * Message name. + * + * @var string + */ + protected $_name = null; + + /** + * Message arguments. * * @var array */ - protected $_static = false; + protected $_args = null; /** - * Number of occurences to match. + * Static call. * - * @var integer + * @var boolean */ - protected $_times = 0; + protected $_static = false; /** * The Constructor. * * @param array $config Possible options are: - * - `'name'` _string_ : The method name. - * - `'params'` _array_ : The method params. - * - `'static'` _boolean_: `true` if the method is static, `false` otherwise. + * - `'reference'` _mixed_ : The message reference. + * - `'name'` _string_ : The message name. + * - `'args'` _array_ : The message arguments. + * - `'static'` _boolean_: `true` if the method is static, `false` otherwise. */ public function __construct($config = []) { - $defaults = ['name' => null, 'params' => [], 'static' => false]; + $defaults = [ + 'parent' => null, + 'reference' => null, + 'name' => null, + 'args' => null, + 'static' => false + ]; $config += $defaults; - $this->_name = $config['name']; - $this->_params = $config['params']; - $this->_static = $config['static']; + $static = $config['static']; + $name = $config['name']; + if (preg_match('/^::.*/', $name)) { + $static = true; + $name = substr($name, 2); + } + + $this->_parent = $config['parent']; + $this->_reference = $config['reference']; + $this->_name = $name; + $this->_args = $config['args']; + $this->_static = $static; } /** - * Sets params requirement. + * Set arguments requirement. * - * @param mixed ... <0,n> Parameter(s). + * @param mixed ... <0,n> Argument(s). * @return self */ public function with() { - $this->_params = func_get_args(); + $this->_args = func_get_args(); return $this; } /** - * Sets the number of occurences. - * - * @return object $this. - */ - public function once() - { - return $this->times(1); - } - - /** - * Gets/sets the number of occurences. + * Set arguments requirement indexed by method name. * - * @param integer $times The number of occurences to set or none to get it. - * @return mixed The number of occurences on get or `$this` otherwise. + * @param mixed ... <0,n> Argument(s). + * @return self */ - public function times($times = null) + public function where($requirements = []) { - if (!func_num_args()) { - return $this->_times; - } - $this->_times = $times; + $this->_parent->where($requirements); return $this; } /** - * Checks if this message is compatible with passed call array. + * Check if this message is compatible with passed call array. * - * @param array $call A call array. + * @param array $call A call array. + * @param boolean $withArgs Boolean indicating if matching should take arguments into account. * @return boolean */ - public function match($call) + public function match($call, $withArgs = true) { - if ($call['static'] !== $this->_static) { - return false; + if (preg_match('/^::.*/', $call['name'])) { + $call['static'] = true; + $call['name'] = substr($call['name'], 2); + } + + if (isset($call['static'])) { + if ($call['static'] !== $this->_static) { + return false; + } } if ($call['name'] !== $this->_name) { return false; } - if (!$this->matchParams($call['params'])) { - return false; + if ($withArgs) { + return $this->matchArgs($call['args']); } + return true; } /** - * Checks if this stub is compatible with passed args. + * Check if this stub is compatible with passed args. * - * @param array $params The passed args. + * @param array $args The passed arguments. * @return boolean */ - public function matchParams($params) + public function matchArgs($args) { - if (!$this->_params) { + if ($this->_args === null || $args === null) { return true; } $arg = $this->_classes['arg']; - foreach ($this->_params as $expected) { - $actual = array_shift($params); + foreach ($this->_args as $expected) { + $actual = array_shift($args); if ($expected instanceof $arg) { if (!$expected->match($actual)) { return false; @@ -143,27 +162,47 @@ public function matchParams($params) } /** - * Gets the method name. + * Get the parent. + * + * @return mixed + */ + public function parent() + { + return $this->_parent; + } + + /** + * Get the message reference. + * + * @return mixed + */ + public function reference() + { + return $this->_reference; + } + + /** + * Get message name. * * @return string */ public function name() { - return $this->_name; + return $this->isStatic() ? '::' . $this->_name : $this->_name; } /** - * Gets the method params. + * Get message arguments. * * @return array */ - public function params() + public function args() { - return $this->_params; + return $this->_args; } /** - * Checks if the method is a static method. + * Check if the method is a static method. * * @return boolean */ diff --git a/src/Plugin/Double.php b/src/Plugin/Double.php new file mode 100644 index 00000000..9f11ed3f --- /dev/null +++ b/src/Plugin/Double.php @@ -0,0 +1,458 @@ + 'Kahlan\Jit\Parser', + 'pointcut' => 'Kahlan\Jit\Patcher\Pointcut' + ]; + + /** + * The pointcut patcher instance. + * + * @var object + */ + protected static $_pointcut = null; + + /** + * Registered stubbed instance/class methods. + * + * @var array + */ + protected static $_registered = []; + + /** + * Stub index counter. + * + * @var integer + */ + protected static $_index = 0; + + /** + * Creates a polyvalent instance. + * + * @param array $options Array of options. Options are: + * - `'class'` _string_: the fully-namespaced class name. + * - `'extends'` _string_: the fully-namespaced parent class name. + * - `'args'` _array_: arguments to pass to the constructor. + * - `'methods'` _string_: override the method defined. + * @return object The created instance. + */ + public static function instance($options = []) + { + $class = static::classname($options); + + if (isset($options['args'])) { + $refl = new ReflectionClass($class); + $instance = $refl->newInstanceArgs($options['args']); + } else { + $instance = new $class(); + } + return $instance; + } + + /** + * Creates a polyvalent static class. + * + * @param array $options Array of options. Options are: + * - `'class'` : the fully-namespaced class name. + * - `'extends'` : the fully-namespaced parent class name. + * @return string The created fully-namespaced class name. + */ + public static function classname($options = []) + { + $defaults = ['class' => 'Kahlan\Spec\Plugin\Double\Double' . static::$_index++]; + $options += $defaults; + + if (!static::$_pointcut) { + $pointcut = static::$_classes['pointcut']; + static::$_pointcut = new $pointcut(); + } + + if (!class_exists($options['class'], false)) { + $parser = static::$_classes['parser']; + $code = static::generate($options); + $nodes = $parser::parse($code); + $code = $parser::unparse(static::$_pointcut->process($nodes)); + eval('?>' . $code); + } + return $options['class']; + } + + /** + * Creates a class definition. + * + * @param array $options Array of options. Options are: + * - `'class'` _string_ : the fully-namespaced class name. + * - `'extends'` _string_ : the fully-namespaced parent class name. + * - `'implements'` _array_ : the implemented interfaces. + * - `'uses'` _array_ : the used traits. + * - `'methods'` _array_ : the methods to stubs. + * - `'layer'` _boolean_: indicate if public methods should be layered. + * @return string The generated class string content. + */ + public static function generate($options = []) + { + $defaults = [ + 'class' => 'Kahlan\Spec\Plugin\Double\Double' . static::$_index++, + 'extends' => '', + 'implements' => [], + 'uses' => [], + 'methods' => [], + 'layer' => null, + 'openTag' => true, + 'closeTag' => true + ]; + $options += $defaults; + + if ($options['extends']) { + $options += ['magicMethods' => false]; + } else { + $options += ['magicMethods' => true]; + } + + $class = $options['class']; + $namespace = ''; + if (($pos = strrpos($class, '\\')) !== false) { + $namespace = substr($class, 0, $pos); + $class = substr($class, $pos + 1); + } + + if ($namespace) { + $namespace = "namespace {$namespace};\n"; + } + + $uses = static::_generateUses($options['uses']); + $extends = static::_generateExtends($options['extends']); + $implements = static::_generateImplements($options['implements']); + + $methods = static::_generateMethodStubs($options['methods'], $options['magicMethods']); + if ($options['extends']) { + $methods += static::_generateClassMethods($options['extends'], $options['layer']); + } + $methods += static::_generateInterfaceMethods($options['implements']); + + $methods = $methods ? ' ' . join("\n ", $methods) : ''; + + $openTag = $options['openTag'] ? "" : ''; + + return $openTag . $namespace . << "public function __construct() {}", + '__destruct' => "public function __destruct() {}", + '__call' => "public function __call(\$name, \$args) { return new static(); }", + '::__callStatic' => "public static function __callStatic(\$name, \$args) { return get_called_class(); }", + '__get' => "public function __get(\$key){ return new static(); }", + '__set' => "public function __set(\$key, \$value) { \$this->{\$key} = \$value; }", + '__isset' => "public function __isset(\$key) { return isset(\$this->{\$key}); }", + '__unset' => "public function __unset(\$key) { unset(\$this->{\$key}); }", + '__sleep' => "public function __sleep() { return []; }", + '__wakeup' => "public function __wakeup() {}", + '__toString' => "public function __toString() { return get_class(); }", + '__invoke' => "public function __invoke() {}", + '__set_sate' => "public function __set_sate(\$properties) {}", + '__clone' => "public function __clone() {}" + ]; + } + + /** + * Creates a `use` definition. + * + * @param array $uses An array of traits. + * @return string The generated `use` definition. + */ + protected static function _generateUses($uses) + { + if (!$uses) { + return ''; + } + $traits = []; + foreach ((array) $uses as $use) { + if (!trait_exists($use)) { + throw new MissingImplementationException("Unexisting trait `{$use}`"); + } + $traits[] = '\\' . ltrim($use, '\\'); + } + return ' use ' . join(', ', $traits) . ';'; + } + + /** + * Creates an `extends` definition. + * + * @param string $extends The parent class name. + * @return string The generated `extends` definition. + */ + protected static function _generateExtends($extends) + { + if (!$extends) { + return ''; + } + return ' extends \\' . ltrim($extends, '\\'); + } + + /** + * Creates an `implements` definition. + * + * @param array $uses An array of interfaces. + * @return string The generated `implements` definition. + */ + protected static function _generateImplements($implements) + { + if (!$implements) { + return ''; + } + $classes = []; + foreach ((array) $implements as $implement) { + $classes[] = '\\' . ltrim($implement, '\\'); + } + return ' implements ' . join(', ', $classes); + } + + /** + * Creates method stubs. + * + * @param array $methods An array of method definitions. + * @param boolean $defaults If `true`, Magic Methods will be appended. + * @return string The generated method definitions. + */ + protected static function _generateMethodStubs($methods, $defaults = true) + { + $result = []; + $methods = $methods !== null ? (array) $methods : []; + + if ($defaults) { + $methods = array_merge($methods, array_keys(static::_getMagicMethods())); + } + $methods = array_unique($methods); + + $magicMethods = static::_getMagicMethods(); + + foreach ($methods as $name) { + if (isset($magicMethods[$name])) { + $result[$name] = $magicMethods[$name]; + } else { + $static = $return = ''; + if ($name[0] === '&') { + $return = '$r = null; return $r;'; + } + if (preg_match('/^&?::.*/', $name)) { + $static = 'static '; + $name = substr($name, 2); + } + $result[$name] = "public {$static}function {$name}() {{$return}}"; + } + } + + return $result; + } + + /** + * Creates method definitions from a class name. + * + * @param string $class A class name. + * @param boolean $layer If `true`, all public methods are "overriden". + * @return string The generated methods. + */ + protected static function _generateClassMethods($class, $layer = null) + { + $result = []; + if (!class_exists($class)) { + throw new MissingImplementationException("Unexisting class `{$class}`"); + } + $result = static::_generateAbstractMethods($class); + + if ($layer === false) { + return $result; + } + + $reflection = Inspector::inspect($class); + + if (!$layer && !$reflection->isInternal()) { + return $result; + } + + $finals = $reflection->getMethods(ReflectionMethod::IS_FINAL); + $methods = array_diff($reflection->getMethods(ReflectionMethod::IS_PUBLIC), $finals); + foreach ($methods as $method) { + $result[$method->getName()] = static::_generateMethod($method, true); + } + return $result; + } + + /** + * Creates method definitions from a class name. + * + * @param string $class A class name. + * @param integer $mask The method mask to filter. + * @return string The generated methods. + */ + protected static function _generateAbstractMethods($class) + { + $result = []; + if (!class_exists($class)) { + throw new MissingImplementationException("Unexisting parent class `{$class}`"); + } + $reflection = Inspector::inspect($class); + $methods = $reflection->getMethods(ReflectionMethod::IS_ABSTRACT); + foreach ($methods as $method) { + $result[$method->getName()] = static::_generateMethod($method); + } + return $result; + } + + /** + * Creates method definitions from an interface array. + * + * @param array $interfaces A array on interfaces. + * @param integer $mask The method mask to filter. + * @return string The generated methods. + */ + protected static function _generateInterfaceMethods($interfaces, $mask = 255) + { + if (!$interfaces) { + return []; + } + $result = []; + foreach ((array) $interfaces as $interface) { + if (!interface_exists($interface)) { + throw new MissingImplementationException("Unexisting interface `{$interface}`"); + } + $reflection = Inspector::inspect($interface); + $methods = $reflection->getMethods($mask); + foreach ($methods as $method) { + $result[$method->getName()] = static::_generateMethod($method); + } + } + return $result; + } + + /** + * Creates a method definition from a `ReflectionMethod` instance. + * + * @param object $method A instance of `ReflectionMethod`. + * @return string The generated method. + */ + protected static function _generateMethod($method, $callParent = false) + { + $result = join(' ', Reflection::getModifierNames($method->getModifiers())); + $result = preg_replace('/abstract\s*/', '', $result); + $name = $method->getName(); + $parameters = static::_generateSignature($method); + $type = static::_generateReturnType($method); + $body = "{$result} function {$name}({$parameters}) {$type}{"; + if ($callParent) { + $parameters = static::_generateParameters($method); + $return = 'return '; + if ($method->isConstructor() || $method->isDestructor()) { + $return = ''; + } + $body .= "{$return}parent::{$name}({$parameters});"; + } + return $body . "}"; + } + + /** + * Extract the return type of a method. + * + * @param objedct $method A instance of `ReflectionMethod`. + * @return string The return type. + */ + protected static function _generateReturnType($method) + { + if (Suite::$PHP < 7) { + return ''; + } + $type = $method->getReturnType(); + if ($type) { + if (!$type->isBuiltin()) { + $type = '\\' . $type; + } + if (defined('HHVM_VERSION')) { + $type = preg_replace('~\\\?HH\\\(mixed|void)?~', '', $type); + } + } + return $type ? ": {$type} " : ''; + } + + /** + * Creates a parameters signature of a `ReflectionMethod` instance. + * + * @param object $method A instance of `ReflectionMethod`. + * @return string The parameters definition list. + */ + protected static function _generateSignature($method) + { + $params = []; + $isVariadic = Suite::$PHP >= 7 ? $method->isVariadic() : false; + + foreach ($method->getParameters() as $num => $parameter) { + $typehint = Inspector::typehint($parameter); + $name = $parameter->getName(); + $name = ($name && $name !== '...') ? $name : 'param' . $num; + $reference = $parameter->isPassedByReference() ? '&' : ''; + $default = ''; + if ($parameter->isDefaultValueAvailable()) { + $default = var_export($parameter->getDefaultValue(), true); + $default = ' = ' . preg_replace('/\s+/', '', $default); + } elseif ($parameter->isOptional()) { + if ($isVariadic && $parameter->isVariadic()) { + $reference = '...'; + $default = ''; + } else { + $default = ' = NULL'; + } + } + $typehint = $typehint ? $typehint . ' ' : $typehint; + $params[] = "{$typehint}{$reference}\${$name}{$default}"; + } + return join(', ', $params); + } + + /** + * Creates a parameters list from a `ReflectionMethod` instance. + * + * @param object $method A instance of `ReflectionMethod`. + * @return string The parameters definition list. + */ + protected static function _generateParameters($method) + { + $params = []; + foreach ($method->getParameters() as $num => $parameter) { + $name = $parameter->getName(); + $name = ($name && $name !== '...') ? $name : 'param' . $num; + $params[] = "\${$name}"; + } + return join(', ', $params); + } +} diff --git a/src/Plugin/Monkey.php b/src/Plugin/Monkey.php index dc405dab..05fdfa02 100644 --- a/src/Plugin/Monkey.php +++ b/src/Plugin/Monkey.php @@ -1,6 +1,12 @@ toBe($dest); + return $method; } /** @@ -29,13 +49,41 @@ public static function patch($source, $dest) * @param boolean $isFunc Boolean indicating if $ref is a function reference. * @return string A fully namespaced reference. */ - public static function patched($namespace, $ref, $isFunc = true) + public static function patched($namespace, $ref, $isFunc = true, &$substitute = null) { - $map = $ref; - if(!$isFunc || function_exists("{$namespace}\\{$ref}")) { - $map = "{$namespace}\\{$ref}"; + $name = $ref; + + if ($namespace) { + if (!$isFunc || function_exists("{$namespace}\\{$ref}")) { + $name = "{$namespace}\\{$ref}"; + } } - return isset(static::$_registered[$map]) ? static::$_registered[$map] : $map; + + $method = isset(static::$_registered[$name]) ? static::$_registered[$name] : null; + $fake = $method ? $method->substitute() : null; + + if (!$isFunc) { + if (is_object($fake)) { + $substitute = $fake; + } + return $fake ?: $name; + } + + if (!Suite::registered($name) && !$method) { + return $name; + } + + return function () use ($name, $method) { + $args = func_get_args(); + + if (Suite::registered($name)) { + Calls::log(null, compact('name', 'args')); + } + if ($method && $method->matchArgs($args)) { + return $method($args); + } + return call_user_func_array($name, $args); + }; } /** diff --git a/src/Plugin/Pointcut.php b/src/Plugin/Pointcut.php index 20e7a2c2..ad343013 100644 --- a/src/Plugin/Pointcut.php +++ b/src/Plugin/Pointcut.php @@ -2,6 +2,7 @@ namespace Kahlan\Plugin; use Kahlan\Suite; +use Kahlan\Plugin\Call\Calls; class Pointcut { @@ -10,10 +11,8 @@ class Pointcut * * @var array */ - protected static $_classes = [ - 'call' => 'Kahlan\Plugin\Call', - 'stub' => 'Kahlan\Plugin\Stub' + 'stub' => 'Kahlan\Plugin\Stub' ]; /** @@ -21,7 +20,7 @@ class Pointcut * * @return boolean If `true` is returned, the normal execution of the method is aborted. */ - public static function before($method, $self, &$params) + public static function before($method, $self, &$args) { if (!Suite::registered()) { return false; @@ -36,11 +35,11 @@ public static function before($method, $self, &$params) } if ($name === '__call' || $name === '__callStatic') { - $name = array_shift($params); - $params = array_shift($params); + $name = array_shift($args); + $args = array_shift($args); } - return static::_stubbedMethod($lsb, $self, $class, $name, $params); + return static::_stubbedMethod($lsb, $self, $class, $name, $args); } /** @@ -50,10 +49,10 @@ public static function before($method, $self, &$params) * @param object|string $self The object instance or a fully-namespaces class name. * @param string $class The class name. * @param string $name The method name. - * @param string $params The passed params. + * @param string $args The passed arguments. * @return boolean Returns `true` if the method has been stubbed. */ - protected static function _stubbedMethod($lsb, $self, $class, $name, $params) + protected static function _stubbedMethod($lsb, $self, $class, $name, $args) { if (is_object($self)) { $list = $lsb === $class ? [$self, $lsb] : [$self, $lsb, $class]; @@ -62,16 +61,11 @@ protected static function _stubbedMethod($lsb, $self, $class, $name, $params) $name = '::' . $name; } - $call = static::$_classes['call']; $stub = static::$_classes['stub']; - $call::log($list, compact('name', 'params')); - - if ($method = $stub::find($list, $name, $params)) { - return $method; - } + $method = $stub::find($list, $name, $args); + Calls::log($list, compact('name', 'args', 'method')); - return false; + return $method ?: false; } - } diff --git a/src/Plugin/Stub.php b/src/Plugin/Stub.php index 5424c388..2240e053 100644 --- a/src/Plugin/Stub.php +++ b/src/Plugin/Stub.php @@ -6,39 +6,35 @@ use ReflectionMethod; use ReflectionClass; use Kahlan\Suite; -use Kahlan\IncompleteException; +use Kahlan\MissingImplementationException; use Kahlan\Analysis\Inspector; use Kahlan\Plugin\Stub\Method; class Stub { /** - * Class dependencies. + * Registered stubbed instance/class methods. * * @var array */ - protected static $_classes = [ - 'parser' => 'Kahlan\Jit\Parser', - 'pointcut' => 'Kahlan\Jit\Patcher\Pointcut', - 'call' => 'Kahlan\Plugin\Call' - ]; + protected static $_registered = []; /** - * The pointcut patcher instance. + * Method chain. * - * @var object + * @var Method[] */ - protected static $_pointcut = null; + protected $_chain = []; /** - * Registered stubbed instance/class methods. + * Stubbed methods. * - * @var array + * @var Method[] */ - protected static $_registered = []; + protected $_methods = []; /** - * Stubbed methods. + * Generic stubs. * * @var Method[] */ @@ -49,7 +45,7 @@ class Stub * * @var integer */ - protected static $_index = 0; + protected $_needToBePatched = false; /** * The Constructor. @@ -58,7 +54,53 @@ class Stub */ public function __construct($reference) { - $this->_reference = $reference; + $reference = $this->_reference($reference); + $isString = is_string($reference); + if ($isString) { + if (!class_exists($reference)) { + throw new InvalidArgumentException("Can't Stub the unexisting class `{$reference}`."); + } + $reference = ltrim($reference, '\\'); + $reflection = Inspector::inspect($reference); + } else { + $reflection = Inspector::inspect(get_class($reference)); + } + + if (!$reflection->isInternal()) { + $this->_reference = $reference; + return; + } + if (!$isString) { + throw new InvalidArgumentException("Can't Stub built-in PHP instances, create a test double using `Double::instance()`."); + } + $this->_needToBePatched = true; + return $this->_reference = $reference; + } + + /** + * Return the actual reference which must be used. + * + * @param mixed $reference An instance or a fully-namespaced class name. + * @param mixed The reference or the monkey patched one if exist. + */ + protected function _reference($reference) + { + if (!is_string($reference)) { + return $reference; + } + + $pos = strrpos($reference, '\\'); + if ($pos !== false) { + $namespace = substr($reference, 0, $pos); + $basename = substr($reference, $pos + 1); + } else { + $namespace = null; + $basename = $reference; + } + $substitute = null; + $reference = Monkey::patched($namespace, $basename, false, $substitute); + + return $substitute ?: $reference; } /** @@ -70,7 +112,7 @@ public function __construct($reference) public function methods($name = []) { if (!func_num_args()) { - return $this->_stubs; + return $this->_methods; } foreach ($name as $method => $returns) { if (is_callable($returns)) { @@ -88,38 +130,94 @@ public function methods($name = []) /** * Stubs a method. * - * @param string $name Method name or array of stubs where key are method names and - * values the stubs. - * @param string $closure The stub implementation. - * @return Method The stubbed method instance. + * @param string $path Method name or array of stubs where key are method names and + * values the stubs. + * @param string $closure The stub implementation. + * @return Method[] The created array of method instances. + * @return Method The stubbed method instance. */ - public function method($name, $closure = null) + public function method($path, $closure = null) { - $static = false; - $reference = $this->_reference; - if (preg_match('/^::.*/', $name)) { - $static = true; - $reference = is_object($reference) ? get_class($reference) : $reference; - $name = substr($name, 2); + if ($this->_needToBePatched) { + $layer = Double::classname(); + Monkey::patch($this->_reference, $layer); + $this->_needToBePatched = false; + $this->_reference = $layer; } - $hash = Suite::hash($reference); - if (!isset(static::$_registered[$hash])) { - static::$_registered[$hash] = new static($reference); + + $reference = $this->_reference; + + if (!$path) { + throw new InvalidArgumentException("Method name can't be empty."); } - $instance = static::$_registered[$hash]; - if (is_object($reference)) { - Suite::register(get_class($reference)); - } else { - Suite::register($reference); + + $names = is_array($path) ? $path : [$path]; + + $this->_chain = []; + $total = count($names); + + foreach ($names as $index => $name) { + if (preg_match('/^::.*/', $name)) { + $reference = is_object($reference) ? get_class($reference) : $reference; + } + + $hash = Suite::hash($reference); + if (!isset(static::$_registered[$hash])) { + static::$_registered[$hash] = new static($reference); + } + + $instance = static::$_registered[$hash]; + if (is_object($reference)) { + Suite::register(get_class($reference)); + } else { + Suite::register($reference); + } + if (!isset($instance->_methods[$name])) { + $instance->_methods[$name] = []; + $instance->_stubs[$name] = Double::instance(); + } + + $method = new Method([ + 'parent' => $this, + 'reference' => $reference, + 'name' => $name + ]); + $this->_chain[$name] = $method; + array_unshift($instance->_methods[$name], $method); + + if ($index < $total - 1) { + $reference = $instance->_stubs[$name]; + $method->andReturn($instance->_stubs[$name]); + } } - if (!isset($instance->_stubs[$name])) { - $instance->_stubs[$name] = []; + + $method = end($this->_chain); + if ($closure) { + $method->andRun($closure); } - $method = new Method(compact('name', 'static', 'closure')); - array_unshift($instance->_stubs[$name], $method); return $method; } + /** + * Set arguments requirement indexed by method name. + * + * @param mixed ... <0,n> Argument(s). + * @return self + */ + public function where($requirements = []) + { + foreach ($requirements as $name => $args) { + if (!isset($this->_chain[$name])) { + throw new InvalidArgumentException("Unexisting `{$name}` as method as part of the chain definition."); + } + if (!is_array($args)) { + throw new InvalidArgumentException("Argument requirements must be an arrays for `{$name}` method."); + } + call_user_func_array([$this->_chain[$name], 'with'], $args); + } + return $this; + } + /** * Stubs class methods. * @@ -141,10 +239,10 @@ public static function on($reference) * @param mixed $references An instance or a fully namespaced class name. * or an array of that. * @param string $method The method name. - * @param array $params The required arguments. + * @param array $args The required arguments. * @return object|null Return the subbed method or `null` if not founded. */ - public static function find($references, $method = null, $params = []) + public static function find($references, $method = null, $args = null) { $references = (array) $references; $stub = null; @@ -155,18 +253,14 @@ public static function find($references, $method = null, $params = []) continue; } $stubs = static::$_registered[$hash]->methods(); - $static = false; - if (preg_match('/^::.*/', $method)) { - $static = true; - $method = substr($method, 2); - } + if (!isset($stubs[$method])) { continue; } + foreach ($stubs[$method] as $stub) { $call['name'] = $method; - $call['static'] = $static; - $call['params'] = $params; + $call['args'] = $args; if ($stub->match($call)) { return $stub; } @@ -175,403 +269,6 @@ public static function find($references, $method = null, $params = []) return false; } - /** - * Creates a polyvalent instance. - * - * @param array $options Array of options. Options are: - * - `'class'` _string_: the fully-namespaced class name. - * - `'extends'` _string_: the fully-namespaced parent class name. - * - `'params'` _array_: params to pass to the constructor. - * - `'methods'` _string_: override the method defined. - * @return object The created instance. - */ - public static function create($options = []) - { - $class = static::classname($options); - - if (isset($options['params'])) { - $refl = new ReflectionClass($class); - $instance = $refl->newInstanceArgs($options['params']); - } else { - $instance = new $class(); - } - $call = static::$_classes['call']; - new $call($instance); - return $instance; - } - - /** - * Creates a polyvalent static class. - * - * @param array $options Array of options. Options are: - * - `'class'` : the fully-namespaced class name. - * - `'extends'` : the fully-namespaced parent class name. - * @return string The created fully-namespaced class name. - */ - public static function classname($options = []) - { - $defaults = ['class' => 'Kahlan\Spec\Plugin\Stub\Stub' . static::$_index++]; - $options += $defaults; - - if (!static::$_pointcut) { - $pointcut = static::$_classes['pointcut']; - static::$_pointcut = new $pointcut(); - } - - if (!class_exists($options['class'], false)) { - $parser = static::$_classes['parser']; - $code = static::generate($options); - $nodes = $parser::parse($code); - $code = $parser::unparse(static::$_pointcut->process($nodes)); - eval('?>' . $code); - } - $call = static::$_classes['call']; - new $call($options['class']); - return $options['class']; - } - - /** - * Creates a class definition. - * - * @param array $options Array of options. Options are: - * - `'class'` _string_ : the fully-namespaced class name. - * - `'extends'` _string_ : the fully-namespaced parent class name. - * - `'implements'` _array_ : the implemented interfaces. - * - `'uses'` _array_ : the used traits. - * - `'methods'` _array_ : the methods to stubs. - * - `'layer'` _boolean_: indicate if public methods should be layered. - * @return string The generated class string content. - */ - public static function generate($options = []) - { - $defaults = [ - 'class' => 'spec\plugin\stub\Stub' . static::$_index++, - 'extends' => '', - 'implements' => [], - 'uses' => [], - 'methods' => [], - 'layer' => false, - 'openTag' => true, - 'closeTag' => true - ]; - $options += $defaults; - - if ($options['extends']) { - $options += ['magicMethods' => false]; - } else { - $options += ['magicMethods' => true]; - } - - $class = $options['class']; - $namespace = ''; - if (($pos = strrpos($class, '\\')) !== false) { - $namespace = substr($class, 0, $pos); - $class = substr($class, $pos + 1); - } - - if ($namespace) { - $namespace = "namespace {$namespace};\n"; - } - - $uses = static::_generateUses($options['uses']); - $extends = static::_generateExtends($options['extends']); - $implements = static::_generateImplements($options['implements']); - - $methods = static::_generateMethodStubs($options['methods'], $options['magicMethods']); - if ($options['extends']) { - $methods += static::_generateClassMethods($options['extends'], $options['layer']); - } - $methods += static::_generateInterfaceMethods($options['implements']); - - $methods = $methods ? ' ' . join("\n ", $methods) : ''; - - $openTag = $options['openTag'] ? "" : ''; - -return $openTag . $namespace . << "public function __construct() {}", - '__destruct' => "public function __destruct() {}", - '__call' => "public function __call(\$name, \$params) { return new static(); }", - '::__callStatic' => "public static function __callStatic(\$name, \$params) {}", - '__get' => "public function __get(\$key){ return new static(); }", - '__set' => "public function __set(\$key, \$value) { \$this->{\$key} = \$value; }", - '__isset' => "public function __isset(\$key) { return isset(\$this->{\$key}); }", - '__unset' => "public function __unset(\$key) { unset(\$this->{\$key}); }", - '__sleep' => "public function __sleep() { return []; }", - '__wakeup' => "public function __wakeup() {}", - '__toString' => "public function __toString() { return get_class(); }", - '__invoke' => "public function __invoke() {}", - '__set_sate' => "public function __set_sate(\$properties) {}", - '__clone' => "public function __clone() {}" - ]; - } - - /** - * Creates a `use` definition. - * - * @param array $uses An array of traits. - * @return string The generated `use` definition. - */ - protected static function _generateUses($uses) - { - if (!$uses) { - return ''; - } - $traits = []; - foreach ((array) $uses as $use) { - if (!trait_exists($use)) { - throw new IncompleteException("Unexisting trait `{$use}`"); - } - $traits[] = '\\' . ltrim($use, '\\'); - } - return ' use ' . join(', ', $traits) . ';'; - } - - /** - * Creates an `extends` definition. - * - * @param string $extends The parent class name. - * @return string The generated `extends` definition. - */ - protected static function _generateExtends($extends) - { - if (!$extends) { - return ''; - } - return ' extends \\' . ltrim($extends, '\\'); - } - - /** - * Creates an `implements` definition. - * - * @param array $uses An array of interfaces. - * @return string The generated `implements` definition. - */ - protected static function _generateImplements($implements) - { - if (!$implements) { - return ''; - } - $classes = []; - foreach ((array) $implements as $implement) { - $classes[] = '\\' . ltrim($implement, '\\'); - } - return ' implements ' . join(', ', $classes); - } - - /** - * Creates method stubs. - * - * @param array $methods An array of method definitions. - * @param boolean $defaults If `true`, Magic Methods will be appended. - * @return string The generated method definitions. - */ - protected static function _generateMethodStubs($methods, $defaults = true) - { - $result = []; - $methods = $methods !== null ? (array) $methods : []; - - if ($defaults) { - $methods = array_merge($methods, array_keys(static::_getMagicMethods())); - } - $methods = array_unique($methods); - - $magicMethods = static::_getMagicMethods(); - - foreach ($methods as $name) { - if (isset($magicMethods[$name])) { - $result[$name] = $magicMethods[$name]; - } else { - $static = $return = ''; - if ($name[0] === '&') { - $return = '$r = null; return $r;'; - } - if (preg_match('/^&?::.*/', $name)) { - $static = 'static '; - $name = substr($name, 2); - } - $result[$name] = "public {$static}function {$name}() {{$return}}"; - } - } - - return $result; - } - - /** - * Creates method definitions from a class name. - * - * @param string $class A class name. - * @param boolean $layer If `true`, all public methods are "overriden". - * @return string The generated methods. - */ - protected static function _generateClassMethods($class, $layer = false) - { - $result = []; - if (!class_exists($class)) { - throw new IncompleteException("Unexisting parent class `{$class}`"); - } - $result = static::_generateAbstractMethods($class); - - if (!$layer) { - return $result; - } - $reflection = Inspector::inspect($class); - $finals = $reflection->getMethods(ReflectionMethod::IS_FINAL); - $methods = array_diff($reflection->getMethods(ReflectionMethod::IS_PUBLIC), $finals); - foreach ($methods as $method) { - $result[$method->getName()] = static::_generateMethod($method, true); - } - return $result; - } - - /** - * Creates method definitions from a class name. - * - * @param string $class A class name. - * @param integer $mask The method mask to filter. - * @return string The generated methods. - */ - protected static function _generateAbstractMethods($class) - { - $result = []; - if (!class_exists($class)) { - throw new IncompleteException("Unexisting parent class `{$class}`"); - } - $reflection = Inspector::inspect($class); - $methods = $reflection->getMethods(ReflectionMethod::IS_ABSTRACT); - foreach ($methods as $method) { - $result[$method->getName()] = static::_generateMethod($method); - } - return $result; - } - - /** - * Creates method definitions from an interface array. - * - * @param array $interfaces A array on interfaces. - * @param integer $mask The method mask to filter. - * @return string The generated methods. - */ - protected static function _generateInterfaceMethods($interfaces, $mask = 255) - { - if (!$interfaces) { - return []; - } - $result = []; - foreach ((array) $interfaces as $interface) { - if (!interface_exists($interface)) { - throw new IncompleteException("Unexisting interface `{$interface}`"); - } - $reflection = Inspector::inspect($interface); - $methods = $reflection->getMethods($mask); - foreach ($methods as $method) { - $result[$method->getName()] = static::_generateMethod($method); - } - } - return $result; - } - - /** - * Creates a method definition from a `ReflectionMethod` instance. - * - * @param objedct $method A instance of `ReflectionMethod`. - * @return string The generated method. - */ - protected static function _generateMethod($method, $callParent = false) - { - $result = join(' ', Reflection::getModifierNames($method->getModifiers())); - $result = preg_replace('/abstract\s*/', '', $result); - $name = $method->getName(); - $parameters = static::_generateSignature($method); - if (PHP_MAJOR_VERSION >= 7) { - $type = $method->getReturnType(); - if ($type && !$type->isBuiltin()) { - $type = '\\' . $type; - } - - $type = $type ? ": {$type} " : ''; - } else { - $type = ''; - } - $body = "{$result} function {$name}({$parameters}) {$type}{"; - if ($callParent) { - $parameters = static::_generateParameters($method); - $body .= "return parent::{$name}({$parameters});"; - } - return $body . "}"; - } - - /** - * Creates a parameters signature of a `ReflectionMethod` instance. - * - * @param object $method A instance of `ReflectionMethod`. - * @return string The parameters definition list. - */ - protected static function _generateSignature($method) - { - $params = []; - $isVariadic = PHP_MAJOR_VERSION >= 7 ? $method->isVariadic() : false; - - foreach ($method->getParameters() as $num => $parameter) { - $typehint = Inspector::typehint($parameter); - $typehint = $typehint ? $typehint . ' ' : $typehint; - $name = $parameter->getName(); - $name = ($name && $name !== '...') ? $name : 'param' . $num; - $reference = $parameter->isPassedByReference() ? '&' : ''; - $default = ''; - if ($parameter->isDefaultValueAvailable()) { - $default = var_export($parameter->getDefaultValue(), true); - $default = ' = ' . preg_replace('/\s+/', '', $default); - } elseif ($parameter->isOptional()) { - if ($isVariadic && $parameter->isVariadic()) { - $reference = '...'; - $default = ''; - } else { - $default = ' = NULL'; - } - } - - $params[] = "{$typehint}{$reference}\${$name}{$default}"; - } - return join(', ', $params); - } - - /** - * Creates a parameters list from a `ReflectionMethod` instance. - * - * @param object $method A instance of `ReflectionMethod`. - * @return string The parameters definition list. - */ - protected static function _generateParameters($method) - { - $params = []; - foreach ($method->getParameters() as $num => $parameter) { - $name = $parameter->getName(); - $name = ($name && $name !== '...') ? $name : 'param' . $num; - $params[] = "\${$name}"; - } - return join(', ', $params); - } - /** * Checks if a stub has been registered for a hash * @@ -580,7 +277,7 @@ protected static function _generateParameters($method) */ public static function registered($hash = null) { - if ($hash === null) { + if (!func_num_args()) { return array_keys(static::$_registered); } return isset(static::$_registered[$hash]); diff --git a/src/Plugin/Stub/Method.php b/src/Plugin/Stub/Method.php index 836f4548..8fef936c 100644 --- a/src/Plugin/Stub/Method.php +++ b/src/Plugin/Stub/Method.php @@ -7,43 +7,65 @@ class Method extends \Kahlan\Plugin\Call\Message { /** - * Index value in the `Method::$_returns` array. + * Index value in the `Method::$_substitutes` array. * - * @var int + * @var integer */ - protected $_index = 0; + protected $_substituteIndex = 0; + + /** + * Return values. + * + * @var array + */ + protected $_substitutes = null; + + /** + * Index value in the `Method::$_returns/Method::$_closures` array. + * + * @var integer + */ + protected $_returnIndex = 0; /** * Stub implementation. * * @var Closure */ - protected $_closure = null; + protected $_closures = null; /** * Return values. * * @var array */ - protected $_returns = []; + protected $_returns = null; + + /** + * The method return value. + * + * @var mixed + */ + protected $_return = null; /** * The Constructor. * * @param array $config The options array, possible options are: * - `'closure'`: the closure to execute for this stub. - * - `'params'`: the params required for exectuting this stub. - * - `'static'`: the type of call required for exectuting this stub. + * - `'args'`: the arguments required for exectuting this stub. + * - `'static'`: the type of call required for exectuting this stub. * - `'returns'`: the returns values for this stub (used only if * the `'closure'` option is missing). */ public function __construct($config = []) { - $defaults = ['closure' => null, 'params' => [], 'returns' => [], 'static' => false]; + $defaults = ['closures' => null, 'args' => [], 'returns' => null, 'static' => false]; $config += $defaults; parent::__construct($config); - $this->_closure = $config['closure']; + $this->_name = ltrim($this->_name, '\\'); + $this->_closures = $config['closures']; $this->_returns = $config['returns']; } @@ -51,54 +73,98 @@ public function __construct($config = []) * Runs the stub. * * @param string $self The context from which the stub need to be executed. - * @param array $params The call parameters array. + * @param array $args The call arguments array. * @return mixed The returned stub result. */ - public function __invoke($self, $params) + public function __invoke($args = [], $self = null) { - if ($this->_closure) { - if (is_string($self)) { - $closure = $this->_closure->bindTo(null, $self); + if ($this->_closures !== null) { + if (isset($this->_closures[$this->_returnIndex])) { + $closure = $this->_closures[$this->_returnIndex++]; } else { - $closure = $this->_closure->bindTo($self, get_class($self)); + $closure = end($this->_closures); } - return call_user_func_array($closure, $params); + if (is_string($self)) { + $closure = $closure->bindTo(null, $self); + } elseif ($self) { + $closure = $closure->bindTo($self, get_class($self)); + } + $this->_return = call_user_func_array($closure, $args); + } elseif ($this->_returns && array_key_exists($this->_returnIndex, $this->_returns)) { + $this->_return = $this->_returns[$this->_returnIndex++]; + } else { + $this->_return = $this->_returns ? end($this->_returns) : null; } - if (isset($this->_returns[$this->_index])) { - return $this->_returns[$this->_index++]; + return $this->_return; + } + + /** + * Set return values. + * + * @param mixed ... <0,n> Return value(s). + */ + public function toBe() + { + if ($this->reference()) { + $this->_substitutes = func_get_args(); + } else { + call_user_func_array([$this, 'andRun'], func_get_args()); } - return $this->_returns ? end($this->_returns) : null; } /** - * Sets the stub logic. + * Set the stub logic. * * @param Closure $closure The logic. */ - public function run($closure) + public function andRun() { - if ($this->_returns) { - throw new Exception("Some return values are already set."); + if ($this->_returns !== null) { + throw new Exception("Some return value(s) has already been set."); } - if (!is_callable($closure)) { - throw new Exception("The passed parameter is not callable."); + $closures = func_get_args(); + foreach ($closures as $closure) { + if (!is_callable($closure)) { + throw new Exception("The passed parameter is not callable."); + } } - $this->_closure = $closure; + $this->_closures = $closures; } /** - * Set. return values. + * Set return values. * * @param mixed ... <0,n> Return value(s). */ public function andReturn() { - if ($this->_closure) { - throw new Exception("Closure already set."); - } - if (func_num_args()) { - $this->_returns = func_get_args(); + if ($this->_closures !== null) { + throw new Exception("Some closure(s) has already been set."); } + $this->_returns = func_get_args(); } + /** + * Get the actual return value. + * + * @return mixed + */ + public function actualReturn() + { + return $this->_return; + } + + + /** + * Get the method substitute. + * + * @return mixed + */ + public function substitute() + { + if (isset($this->_substitutes[$this->_substituteIndex])) { + return $this->_substitutes[$this->_substituteIndex++]; + } + return $this->_substitutes ? end($this->_substitutes) : null; + } } diff --git a/src/Reporter/Bar.php b/src/Reporter/Bar.php index 9caba264..e45d3e49 100644 --- a/src/Reporter/Bar.php +++ b/src/Reporter/Bar.php @@ -51,10 +51,8 @@ public function __construct($config = []) $defaults = [ 'size' => 50, 'preferences' => [ - 'pass' => 'green', - 'fail' => 'red', - 'incomplete' => 'yellow', - 'exception' => 'magenta' + 'passed' => 'green', + 'failed' => 'red' ], 'chars' => [ 'bar' => '=', @@ -71,66 +69,36 @@ public function __construct($config = []) $_key = "_{$key}"; $this->$_key = $value; } - $this->_color = $this->_preferences['pass']; + $this->_color = $this->_preferences['passed']; } /** * Callback called before any specs processing. * - * @param array $params The suite params array. + * @param array $args The suite arguments. */ - public function start($params) + public function start($args) { - parent::start($params); + parent::start($args); $this->write("\n"); - } - - /** - * Callback called on a spec start. - * - * @param object $report The report object of the whole spec. - */ - public function specStart($report = null) - { - parent::specStart($report); $this->_progressBar(); } - - /** - * Callback called on failure. - * - * @param array $report The report array. - */ - public function fail($report = []) - { - $this->_color = $this->_preferences['fail']; - $this->write("\n"); - $this->_report($report); - } - /** - * Callback called when an exception occur. + * Callback called after a spec execution. * - * @param array $report The report array. + * @param object $log The log object of the whole spec. */ - public function exception($report = []) + public function specEnd($log = null) { - $this->_color = $this->_preferences['exception']; - $this->write("\n"); - $this->_report($report); - } - - /** - * Callback called when a `Kahlan\IncompleteException` occur. - * - * @param array $report The report array. - */ - public function incomplete($report = []) - { - $this->_color = $this->_preferences['incomplete']; - $this->write("\n"); - $this->_report($report); + $this->_current++; + switch ($log->type()) { + case 'failed': + case 'errored': + $this->_color = $this->_preferences['failed']; + break; + } + $this->_progressBar(); } /** @@ -161,11 +129,18 @@ protected function _progressBar() /** * Callback called at the end of specs processing. + * + * @param object $summary The execution summary instance. */ - public function end($results = []) + public function end($summary) { $this->write("\n\n"); - $this->_summary($results); - $this->_reportFocused($results); + foreach ($summary->logs() as $log) { + if (!$log->passed()) { + $this->_report($log); + } + } + $this->write("\n\n"); + $this->_reportSummary($summary); } } diff --git a/src/Reporter/Coverage.php b/src/Reporter/Coverage.php index 6ef3e8e2..80a18081 100644 --- a/src/Reporter/Coverage.php +++ b/src/Reporter/Coverage.php @@ -42,6 +42,13 @@ class Coverage extends Terminal */ protected $_enabled = true; + /** + * Store prefix by level for tree rendering. + * + * @var array + */ + protected $_prefixes = []; + /** * The Constructor. * @@ -78,20 +85,20 @@ public function __construct($config = []) /** * Callback called before any specs processing. * - * @param array $params The suite params array. + * @param array $args The suite arguments. */ - public function start($params) + public function start($args) { } /** * Callback called on a spec start. * - * @param object $report The report object of the whole spec. + * @param object $spec The spec object of the whole spec. */ - public function specStart($report = null) + public function specStart($spec = null) { - parent::specStart($report); + parent::specStart($spec); if (!$this->enabled()) { return; } @@ -101,11 +108,11 @@ public function specStart($report = null) /** * Callback called after a spec execution. * - * @param object $report The report object of the whole spec. + * @param object $log The log object of the whole spec. */ - public function specEnd($report = null) + public function specEnd($log = null) { - parent::specEnd($report); + parent::specEnd($log); if (!$this->enabled()) { return; } @@ -125,13 +132,13 @@ public function collector() /** * Delegates the call to the collector instance. * - * @param string $name The function name. - * @param array $params The parameters to pass to the function. + * @param string $name The function name. + * @param array $args The arguments to pass to the function. * @return mixed */ - public function __call($name, $params) + public function __call($name, $args) { - return call_user_func_array([$this->collector(), $name], $params); + return call_user_func_array([$this->collector(), $name], $args); } /** @@ -157,41 +164,130 @@ public function metrics() * - 4 : overall coverage by methods and functions. * - string : coverage for a fully namespaced (class/method/namespace) string. */ - protected function _renderChildMetrics($metrics, $verbosity = 1) + protected function _renderMetrics($metrics, $verbosity) { - $type = $metrics->type(); - if ($verbosity === 2 && ($type === 'class' || $type === 'function')) { - return; - } - if ($verbosity === 3 && ($type === 'function' || $type === 'method')) { - return; - } - // If parent is null, this is the total so do not output as we will do this later - if (!is_null($metrics->parent())) { - $this->_renderMetric($metrics); - } + $maxLabelWidth = null; if ($verbosity === 1) { return; } - foreach ($metrics->childs() as $child) { - $this->_renderChildMetrics($child, $verbosity); + $metricsReport = $this->_getMetricsReport($metrics->children(), $verbosity, 0, 3, $maxLabelWidth); + $name = $metrics->name() ?: '\\'; + $maxLabelWidth = max(strlen($name) + 1, $maxLabelWidth); + $maxLabelWidth += 4; + $stats = $metrics->data(); + $percent = number_format($stats['percent'], 2); + $style = $this->_style($percent); + $maxLineWidth = strlen($stats['lloc']); + + $this->write(str_repeat(' ', $maxLabelWidth)); + $this->write(' '); + $this->write(str_pad('Lines', $maxLineWidth * 2 + 3, ' ', STR_PAD_BOTH)); + $this->write(str_pad('%', 12, ' ', STR_PAD_LEFT)); + $this->write("\n\n"); + $this->write(str_pad(' ' . $name, $maxLabelWidth)); + $this->write(' '); + $this->write(str_pad("{$stats['cloc']}", $maxLineWidth, ' ', STR_PAD_LEFT)); + $this->write(' / '); + $this->write(str_pad("{$stats['lloc']}", $maxLineWidth, ' ', STR_PAD_LEFT)); + $this->write(' '); + $this->write(str_pad("{$percent}%", 7, ' ', STR_PAD_LEFT), $style); + $this->write("\n"); + $this->_renderMetricsReport($metricsReport, $maxLabelWidth, $maxLineWidth, 0); + } + + /** + * Outputs some metrics reports built using `::_getMetricsReport()`. + * + * @param array $metricsReport An array of nested metrics reports extracted according some verbosity. + * @param array $labelWidth The width column of the label column used for padding. + * @param array $lineWidth The width column of the covered lines data used for padding. + * @param array $depth The actual depth in the reporting to build tree prefix. + */ + protected function _renderMetricsReport($metricsReport, $labelWidth, $lineWidth, $depth) + { + $nbChilden = count($metricsReport); + $index = 0; + foreach ($metricsReport as $name => $data) { + $isLast = $index === $nbChilden - 1; + if ($isLast) { + $this->_prefixes[$depth] = '└──'; + } else { + $this->_prefixes[$depth] = '├──'; + } + + $metrics = $data['metrics']; + $stats = $metrics->data(); + $percent = number_format($stats['percent'], 2); + $style = $this->_style($percent); + + $prefix = join('', $this->_prefixes) . ' '; + $diff = strlen($prefix) - strlen(utf8_decode($prefix)); + + $type = $metrics->type(); + $color = $type === 'function' || $type === 'method' ? 'd' : ''; + $this->write($prefix); + $this->write(str_pad($name, $labelWidth + $diff - strlen($prefix)), $color); + $this->write(' '); + $this->write(str_pad("{$stats['cloc']}", $lineWidth, ' ', STR_PAD_LEFT)); + $this->write(' / '); + $this->write(str_pad("{$stats['lloc']}", $lineWidth, ' ', STR_PAD_LEFT)); + $this->write(' '); + $this->write(str_pad("{$percent}%", 7, ' ', STR_PAD_LEFT), $style); + $this->write("\n"); + + if ($isLast) { + $this->_prefixes[$depth] = '   '; + } else { + $this->_prefixes[$depth] = '│  '; + } + $this->_renderMetricsReport($data['children'], $labelWidth, $lineWidth, $depth + 1); + $index++; } + $this->_prefixes[$depth] = ''; } /** - * Outputs some metrics info for a given metric. + * Extract some metrics reports to display according to a verbosity parameter. * - * @param Metrics $metrics A metrics instance. + * @param Metrics[] $children A array of metrics. + * @param array $options The options for the reporter, the options are: + * - `'verbosity`' _integer|string_: The verbosity level: + * - 1 : overall coverage value for the whole code. + * - 2 : overall coverage by namespaces. + * - 3 : overall coverage by classes. + * - 4 : overall coverage by methods and functions. + * - string : coverage for a fully namespaced (class/method/namespace) string. + * @param array $depth The actual depth in the reporting. + * @param array $tab The size of the tab used for lablels. + * @param array $maxWidth Will contain the maximum width obtained for labels. */ - protected function _renderMetric($metrics) + protected function _getMetricsReport($children, $verbosity, $depth = 0, $tab = 3, &$maxWidth = null) { - $name = $metrics->name(); - $stats = $metrics->data(); - $percent = number_format($stats['percent'], 2); - $style = $this->_style($percent); - $this->write(str_pad("Lines: {$percent}%", 15), $style); - $this->write(trim(str_pad("({$stats['cloc']}/{$stats['lloc']})", 20) . "{$name}")); - $this->write("\n"); + $list = []; + foreach ($children as $child) { + $type = $child->type(); + + if ($verbosity === 2 && $type !== 'namespace') { + continue; + } + if ($verbosity === 3 && ($type === 'function' || $type === 'method')) { + continue; + } + + $name = $child->name(); + $pos = strrpos($name, '\\', $type === 'namespace' ? - 2 : 0); + $basename = substr($name, $pos !== false ? $pos + 1 : 0); + + $len = strlen($basename) + ($depth + 1) * $tab; + if ($len > $maxWidth) { + $maxWidth = $len; + } + $list[$basename] = [ + 'metrics' => $child, + 'children' => $this->_getMetricsReport($child->children(), $verbosity, $depth + 1, $tab, $maxWidth) + ]; + } + return $list; } /** @@ -241,7 +337,7 @@ protected function _renderCoverage($metrics) */ protected function _style($percent) { - switch(true) { + switch (true) { case $percent >= 80: return 'n;green'; break; @@ -257,29 +353,45 @@ protected function _style($percent) /** * Callback called at the end of the process. + * + * @param object $summary The execution summary instance. */ - public function stop($results = []) + public function stop($summary) { - $this->write("Coverage Summary\n----------------\n\n"); - if (is_numeric($this->_verbosity)) { - $metrics = $this->metrics(); - $this->_renderChildMetrics($metrics, $this->_verbosity); - } else { - $metrics = $this->metrics()->get($this->_verbosity); - if ($metrics) { - $this->_renderChildMetrics($metrics); - $this->write("\n"); - $this->_renderCoverage($metrics); - } else { - $this->write("\nUnexisting namespace: `{$this->_verbosity}`, coverage can't be generated.\n\n", "n;yellow"); - } + $this->write("Coverage Summary\n----------------\n"); + + $verbosity = $this->_verbosity; + $metrics = is_numeric($this->_verbosity) ? $this->metrics() : $this->metrics()->get($verbosity); + + if (!$metrics) { + $this->write("\nUnexisting namespace: `{$this->_verbosity}`, coverage can't be generated.\n\n", "n;yellow"); + } + + $this->_renderMetrics($metrics, $verbosity); + $this->write("\n"); + + if (is_string($verbosity)) { + $this->_renderCoverage($metrics); + $this->write("\n"); } + // Output the original stored metrics object (the total coverage) - $this->write("Total:\n"); - $this->_renderMetric($metrics); + $name = $metrics->name(); + $stats = $metrics->data(); + $percent = number_format($stats['percent'], 2); + $this->write(str_repeat(' ', substr_count($name, '\\'))); + + $pos = strrpos($name, '\\'); + $basename = substr($name, $pos !== false ? $pos + 1 : 0); + $this->write('Total: '); + $this->write("{$percent}% ", $this->_style($percent)); + $this->write("({$stats['cloc']}/{$stats['lloc']})"); + $this->write("\n"); + // Output the time to collect coverage $time = number_format($this->_time, 3); - $this->write("\nCollected in {$time} seconds\n\n\n"); + $memory = $this->readableSize(memory_get_peak_usage() - $summary->memoryUsage()); + $this->write("\nCoverage collected in {$time} seconds (using an additionnal {$memory}o)\n\n\n"); } /** diff --git a/src/Reporter/Coverage/Collector.php b/src/Reporter/Coverage/Collector.php index da02c3ce..726063d7 100644 --- a/src/Reporter/Coverage/Collector.php +++ b/src/Reporter/Coverage/Collector.php @@ -125,7 +125,8 @@ public function __construct($config = []) * * @return object */ - public function driver() { + public function driver() + { return $this->_driver; } @@ -134,7 +135,8 @@ public function driver() { * * @return string */ - public function base() { + public function base() + { return rtrim($this->_base, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; } @@ -356,24 +358,23 @@ protected function _processTree($file, $nodes, $coverage, $path = '') */ protected function _processNode($file, $node, $coverage, $path) { - if ($node->hasMethods) { - $path = "{$path}" . $node->name; - return $this->_processTree($file, $node->tree, $coverage, $path); - } if ($node->type === 'namespace') { + if ($node->type === 'namespace') { $path = "{$path}" . $node->name . '\\'; - return $this->_processTree($file, $node->tree, $coverage, $path); - } if ($node->type === 'function') { - $prefix = $node->isMethod ? "{$path}::" : "{$path}\\"; + $this->_processTree($file, $node->tree, $coverage, $path); + } elseif ($node->hasMethods) { + if ($node->type === 'interface') { + return; + } + $path = "{$path}" . $node->name; + $this->_processTree($file, $node->tree, $coverage, $path); + } elseif ($node->type === 'function') { + $prefix = $node->isMethod ? "{$path}::" : "{$path}"; $path = $prefix . $node->name . '()'; - $type = $node->type; } else { - $type = $node->parent ? $node->parent->type : 'namespace'; - } - if ($type === 'interface') { - return; + $this->_processTree($file, $node->tree, $coverage, ''); } $metrics = $this->_processMetrics($file, $node, $coverage); - $this->_metrics->add($path, $type, $metrics); + $this->_metrics->add($path, $metrics); } /** @@ -421,7 +422,8 @@ protected function _processMetrics($file, $node, $coverage) * @param integer $increment The increment to perform if the line has not already been processed. * @return integer The metric value. */ - protected function _lineMetric($type, $index, $value, $increment = 1) { + protected function _lineMetric($type, $index, $value, $increment = 1) + { if ($this->_processed[$type] >= $index) { return $value; } @@ -449,7 +451,6 @@ protected function _methodMetrics($node, $metrics) $metrics['line']['start'] = $node->lines['start']; $metrics['line']['stop'] = $node->lines['stop']; - return $metrics; } @@ -466,5 +467,4 @@ public function parse($file) $parser = $this->_classes['parser']; return $this->_tree[$file] = $parser::parse(file_get_contents($file), ['lines' => true]); } - } diff --git a/src/Reporter/Coverage/Driver/HHVM.php b/src/Reporter/Coverage/Driver/HHVM.php index 4cb77a84..4a939cf1 100644 --- a/src/Reporter/Coverage/Driver/HHVM.php +++ b/src/Reporter/Coverage/Driver/HHVM.php @@ -38,7 +38,8 @@ public function start() //@see bug https://github.com/facebook/hhvm/issues/4752 try { fb_enable_code_coverage(); - } catch (Exception $e) {} + } catch (Exception $e) { + } } /** diff --git a/src/Reporter/Coverage/Driver/Xdebug.php b/src/Reporter/Coverage/Driver/Xdebug.php index 5227796c..68da7230 100644 --- a/src/Reporter/Coverage/Driver/Xdebug.php +++ b/src/Reporter/Coverage/Driver/Xdebug.php @@ -45,7 +45,8 @@ public function start() //@see bug https://github.com/facebook/hhvm/issues/4752 try { xdebug_start_code_coverage($this->_config['coverage']); - } catch (Exception $e) {} + } catch (Exception $e) { + } } /** diff --git a/src/Reporter/Coverage/Exporter/CodeClimate.php b/src/Reporter/Coverage/Exporter/CodeClimate.php index e9fd6926..0667c0a3 100644 --- a/src/Reporter/Coverage/Exporter/CodeClimate.php +++ b/src/Reporter/Coverage/Exporter/CodeClimate.php @@ -105,5 +105,4 @@ protected static function _sourceFiles($collector) return $result; } - } diff --git a/src/Reporter/Coverage/Metrics.php b/src/Reporter/Coverage/Metrics.php index 59ddeb9f..b9493257 100644 --- a/src/Reporter/Coverage/Metrics.php +++ b/src/Reporter/Coverage/Metrics.php @@ -53,7 +53,7 @@ class Metrics * * @param array */ - protected $_childs = []; + protected $_children = []; /** * Constructor @@ -82,11 +82,11 @@ public function __construct($options = []) case 'function': case 'trait': case 'class': - $this->_name = $pname ? $pname . '\\' . $options['name'] : $options['name']; - break; + $this->_name = $pname ? $pname . $options['name'] : $options['name']; + break; case 'method': $this->_name = $pname ? $pname . '::' . $options['name'] : $options['name']; - break; + break; } } @@ -145,28 +145,37 @@ public function data($metrics = []) * Adds some metrics to the current metrics. * * @param string $name The name reference of the metrics. - * @param string $type The type of metrics to add. * Possible values are: `'namespace'`, `'class' or 'function'. * @param array The metrics array to add. */ - public function add($name, $type, $metrics) + public function add($name, $metrics) { - if (!$name) { - $this->_merge($metrics, true); - return; - } - list($name, $subname, $nameType) = $this->_parseName($name, $type); - if (!isset($this->_childs[$name])) { - $parent = $this; - $this->_childs[$name] = new Metrics([ - 'name' => $name, - 'parent' => $parent, - 'type' => $nameType - ]); - } - ksort($this->_childs); + $parts = $this->_parseName($name); $this->_merge($metrics); - $this->_childs[$name]->add($subname, $type, $metrics); + + $current = $this; + $length = count($parts); + foreach ($parts as $index => $part) { + list($name, $type) = $part; + if (!isset($current->_children[$name])) { + $current->_children[$name] = new static([ + 'name' => $name, + 'parent' => $current, + 'type' => $type + ]); + } + uksort($current->_children, function ($a, $b) { + $isFunction1 = substr($a, -2) === '()'; + $isFunction2 = substr($b, -2) === '()'; + if ($isFunction1 === $isFunction2) { + return strcmp($a, $b); + } + return $isFunction1 ? -1 : 1; + }); + + $current = $current->_children[$name]; + $current->_merge($metrics, $index === $length - 1); + } } /** @@ -177,34 +186,32 @@ public function add($name, $type, $metrics) */ public function get($name = null) { - if (!$name) { - return $this; - } - list($name, $subname, $type) = $this->_parseName($name); + $parts = $this->_parseName($name); - if (!isset($this->_childs[$name])) { - return; + $child = $this; + foreach ($parts as $part) { + list($name, $type) = $part; + if (!isset($child->_children[$name])) { + return; + } + $child = $child->_children[$name]; } - return $this->_childs[$name]->get($subname); + return $child; } /** - * Gets the childs of the current metrics. + * Gets the children of the current metrics. * * @param string $name The name reference of the metrics. - * @return array The metrics childs. + * @return array The metrics children. */ - public function childs($name = null) + public function children($name = null) { - if (!$name) { - return $this->_childs; + $child = $this->get($name); + if (!$child) { + return []; } - list($name, $subname, $type) = $this->_parseName($name); - - if (!isset($this->_childs[$name])) { - return null; - } - return $this->_childs[$name]->childs($subname); + return $child->_children; } /** @@ -214,19 +221,31 @@ public function childs($name = null) * @param string $type The type to use by default if not auto detected. * @return array The parsed name. */ - protected function _parseName($name, $type = null) + protected function _parseName($name) { - $subname = ''; - if (strpos($name, '\\') !== false) { - $type = 'namespace'; - list($name, $subname) = explode('\\', $name, 2); - } elseif (strpos($name, '::') !== false) { - $type = 'class'; - list($name, $subname) = explode('::', $name, 2); - } elseif (preg_match('~\(\)$~', $name)) { - $type = ($this->_type === 'class' || $this->_type === 'trait') ? 'method' : 'function'; + $result = []; + $parts = preg_split('~([^\\\]*\\\?)~', $name, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + + $last = array_pop($parts); + + if (!$last) { + return []; + } + + foreach ($parts as $name) { + $result[] = [$name, 'namespace']; + } + + if (strpos($last, '::') !== false) { + list($name, $subname) = explode('::', $last, 2); + $result[] = [$name, 'class']; + $result[] = [$subname, 'method']; + } elseif (preg_match('~\(\)$~', $last)) { + $result[] = [$last, 'function']; + } else { + $result[] = [$last, substr($last, -1) === '\\' ? 'namespace' : 'class']; } - return [$name, $subname, $type]; + return $result; } /** diff --git a/src/Reporter/Dot.php b/src/Reporter/Dot.php index 32557533..1fc5f8cf 100644 --- a/src/Reporter/Dot.php +++ b/src/Reporter/Dot.php @@ -5,94 +5,101 @@ class Dot extends Terminal { /** * Store the current number of dots. - *é + * * @var integer */ protected $_counter = 0; /** - * Callback called before any specs processing. - * - * @param array $params The suite params array. - */ - public function start($params) - { - parent::start($params); - $this->write("\n"); - } - - /** - * Callback called on successful expect. + * The max number of columns. * - * @param object $report An expect report object. + * @var integer */ - public function pass($report = null) - { - $this->_write('.'); - } + protected $_columns = 80; /** - * Callback called on failure. + * The dot area with. * - * @param object $report An expect report object. + * @var integer */ - public function fail($report = null) - { - $this->_write('F', 'red'); - } + protected $_dotWidth = 0; /** - * Callback called when an exception occur. + * Constructor * - * @param object $report An expect report object. + * @param array $config The config array. Possible options are: + * -`'columns'`: the max columns width for dot area. */ - public function exception($report = null) + public function __construct($config = []) { - $this->_write('E', 'magenta'); + parent::__construct($config); + $defaults = ['columns' => 80]; + $config += $defaults; + $this->_columns = $config['columns']; } /** - * Callback called on a skipped spec. + * Callback called before any specs processing. * - * @param object $report An expect report object. + * @param array $args The suite arguments array. */ - public function skip($report = null) + public function start($args) { - $this->_write('S', 'cyan'); + parent::start($args); + $this->_dotWidth = max($this->_columns - 11 - strlen($this->_total) * 2, 10); + $this->write("\n"); } /** - * Callback called when a `Kahlan\IncompleteException` occur. + * Callback called after a spec execution. * - * @param object $report An expect report object. + * @param object $log The log object of the whole spec. */ - public function incomplete($report = null) + public function specEnd($log = null) { - $this->_write('I', 'yellow'); + switch ($log->type()) { + case 'passed': + $this->_write('.'); + break; + case 'skipped': + $this->_write('S', 'd'); + break; + case 'pending': + $this->_write('P', 'cyan'); + break; + case 'excluded': + $this->_write('X', 'yellow'); + break; + case 'failed': + $this->_write('F', 'red'); + break; + case 'errored': + $this->_write('E', 'magenta'); + break; + } } /** * Callback called at the end of specs processing. + * + * @param object $summary The execution summary instance. */ - public function end($results = []) + public function end($summary) { do { $this->_write(' '); - } while ($this->_counter % 80 !== 0); + } while ($this->_counter % $this->_dotWidth !== 0); $this->write("\n"); - foreach ($results['specs'] as $type => $reports) { - foreach ($reports as $report) { - if ($report->type() !== 'pass' && $report->type() !== 'skip') { - $this->_report($report); - } + foreach ($summary->logs() as $log) { + if (!$log->passed()) { + $this->_report($log); } } $this->write("\n\n"); - $this->_summary($results); - $this->_reportFocused($results); + $this->_reportSummary($summary); } /** @@ -105,8 +112,13 @@ protected function _write($string, $options = null) { $this->write($string, $options); $this->_counter++; - if ($this->_counter % 80 === 0) { - $this->write(' ' . floor(($this->_current * 100) / $this->_total) . "%\n"); + + if ($this->_counter % $this->_dotWidth === 0) { + $counter = min($this->_counter, $this->_total); + $percent = min(floor(($counter * 100) / $this->_total), 100) . '%'; + $this->write(str_pad($counter, strlen($this->_total) + 1, ' ', STR_PAD_LEFT)); + $this->write(' / ' . $this->_total); + $this->write(' (' . str_pad($percent, 4, ' ', STR_PAD_LEFT) . ")\n"); } } } diff --git a/src/Reporter/Json.php b/src/Reporter/Json.php index 6897806e..0eaef906 100644 --- a/src/Reporter/Json.php +++ b/src/Reporter/Json.php @@ -1,6 +1,8 @@ [], - 'summary' => [ - 'success' => 0, - 'failed' => 0, - 'skipped' => 0, - 'error' => 0, - 'passed' => 0, - 'incomplete' => 0 - ] + 'errors' => [] ]; /** * Callback called before any specs processing. * - * @param array $params The suite params array. + * @param array $args The suite arguments. */ - public function start($params) + public function start($args) { $this->_header = false; - parent::start($params); - } - - /** - * Callback called on successful expect. - * - * @param object $report An expect report object. - */ - public function pass($report = null) - { - $this->_json['summary']['passed'] += 1; - } - - /** - * Callback called on failure. - * - * @param object $report An expect report object. - */ - public function fail($report = null) - { - $this->_json['summary']['failed'] += 1; - } - - /** - * Callback called when an exception occur. - * - * @param object $report An expect report object. - */ - public function exception($report = null) - { - $this->_json['summary']['failed'] += 1; - } - - /** - * Callback called on a skipped spec. - * - * @param object $report An expect report object. - */ - public function skip($report = null) - { - $this->_json['summary']['skipped'] += 1; - } - - /** - * Callback called when a `Kahlan\IncompleteException` occur. - * - * @param object $report An expect report object. - */ - public function incomplete($report = null) - { - $this->_json['summary']['incomplete'] += 1; + parent::start($args); } /** * Callback called at the end of specs processing. * - * @param array $results The results array of the execution. + * @param object $summary The execution summary instance. */ - public function end($results = []) + public function end($summary) { - foreach ($results['specs'] as $type => $reports) { - foreach ($reports as $report) { - if ($report->type() !== 'pass' && $report->type() !== 'skip') { - switch ($report->type()) { - case 'fail': - $this->_json['errors'][] = [ - 'spec' => trim(implode(' ', $report->messages())), - 'suite' => $report->file(), - 'actual' => $report->params()['actual'], - 'expected' => $report->params()['expected'] - ]; - break; - case 'exception': - $exception = $report->exception(); + $toString = function ($instance) { + return 'an instance of `' . get_class($instance) . '`'; + }; + + foreach ($summary->logs() as $log) { + if ($log->passed()) { + continue; + } + switch ($log->type()) { + case 'failed': + foreach ($log->children() as $log) { + if ($log->passed()) { + continue; + } + $data = []; + foreach ($log->data() as $key => $value) { + $data[$key] = Text::toString($value, ['object' => ['method' => $toString]]); + } - $this->_json['errors'][] = [ - 'spec' => trim(implode(' ', $report->messages())), - 'suite' => $report->file(), - 'exception' => '`' . get_class($exception) .'` Code(' . $exception->getCode() . ')', - 'trace' => $exception->getMessage() - ]; - break; + $this->_json['errors'][] = [ + 'spec' => trim(implode(' ', $log->messages())), + 'suite' => $log->file(), + 'data' => $data + ]; } + break; + case 'errored': + $exception = $log->exception(); - } + $this->_json['errors'][] = [ + 'spec' => trim(implode(' ', $log->messages())), + 'suite' => $log->file(), + 'exception' => '`' . get_class($exception) .'` Code(' . $exception->getCode() . ')', + 'trace' => $exception->getMessage() + ]; + break; } } + $this->_json['summary'] = [ + 'total' => $summary->total(), + 'passed' => $summary->passed(), + 'pending' => $summary->pending(), + 'skipped' => $summary->skipped(), + 'excluded' => $summary->excluded(), + 'failed' => $summary->failed(), + 'errored' => $summary->errored(), + ]; + $this->write(json_encode($this->_json)); } -} \ No newline at end of file +} diff --git a/src/Reporter/Reporter.php b/src/Reporter/Reporter.php index 9fe190d4..9baba21a 100644 --- a/src/Reporter/Reporter.php +++ b/src/Reporter/Reporter.php @@ -40,107 +40,84 @@ public function __construct($config = []) /** * Callback called before any specs processing. * - * @param array $params The suite params array. + * @param array $args The suite arguments. */ - public function start($params) + public function start($args) { $this->_start = $this->_start ?: microtime(true); - $this->_total = max(1, $params['total']); + $this->_total = max(1, $args['total']); } /** * Callback called on a suite start. * - * @param object $report The report object of the whole spec. + * @param object $suite The suite instance. */ - public function suiteStart($report = null) - { - } - - /** - * Callback called after a suite execution. - * - * @param object $report The report object of the whole spec. - */ - public function suiteEnd($report = null) + public function suiteStart($suite = null) { } /** * Callback called on a spec start. * - * @param object $report The report object of the whole spec. - */ - public function specStart($report = null) - { - } - - /** - * Callback called after a spec execution. - * - * @param object $report The report object of the whole spec. + * @param object $spec The spec object of the whole spec. */ - public function specEnd($report = null) + public function specStart($spec = null) { - $this->_current++; } /** * Callback called on successful expect. * - * @param object $report An expect report object. + * @param object $log An expect log object. */ - public function pass($report = null) + public function passed($log = null) { } /** * Callback called on failure. * - * @param object $report An expect report object. + * @param object $log An expect log object. */ - public function fail($report = null) + public function failed($log = null) { } /** - * Callback called when an exception occur. - * - * @param object $report An expect report object. - */ - public function exception($report = null) - { - } - - /** - * Callback called on a skipped spec. + * Callback called after a spec execution. * - * @param object $report An expect report object. + * @param object $log The log object of the whole spec. */ - public function skip($report = null) + public function specEnd($log = null) { + $this->_current++; } /** - * Callback called when a `Kahlan\IncompleteException` occur. + * Callback called after a suite execution. * - * @param object $report An expect report object. + * @param object $suite The suite instance. */ - public function incomplete($report = null) + public function suiteEnd($suite = null) { } /** * Callback called at the end of specs processing. + * + * @param object $summary The execution summary instance. */ - public function end($results = []) + public function end($summary) { } /** * Callback called at the end of the process. + * + * @param object $summary The execution summary instance. */ - public function stop($results = []) + public function stop($summary) { } } diff --git a/src/Reporter/Tap.php b/src/Reporter/Tap.php index c330c17a..6714e0f1 100644 --- a/src/Reporter/Tap.php +++ b/src/Reporter/Tap.php @@ -1,142 +1,86 @@ 0, - 'failed' => 0, - 'skipped' => 0, - 'total' => 0 - ]; - - protected $_lines = []; - /** * Callback called before any specs processing. * - * @param array $params The suite params array. + * @param array $args The suite arguments. */ - public function start($params) + public function start($args) { $this->_header = false; - parent::start($params); - $this->write("\n"); - $this->write("# Building report it can take some time, please be patient"); - } - - /** - * Callback called on successful expect. - * - * @param object $report An expect report object. - */ - public function pass($report = null) - { - $this->_counters['success'] += 1; - $this->_counters['total'] += 1; - - $this->_formatTap(true, $report); - } - - /** - * Callback called on failure. - * - * @param object $report An expect report object. - */ - public function fail($report = null) - { - $this->_counters['failed'] += 1; - $this->_counters['total'] += 1; - - $this->_formatTap(false, $report); - $this->_lines[] = "# Actual: {$report->params()["actual"]}"; - $this->_lines[] = "# Expected: {$report->params()["expected"]}"; - } - - /** - * Callback called when an exception occur. - * - * @param object $report An expect report object. - */ - public function exception($report = null) - { - $this->_counters['failed'] += 1; - $this->_counters['total'] += 1; - - $this->_formatTap(true, $report); - $exception = $report->exception(); - $this->_lines[] = '# Exception: `' . get_class($exception) .'` Code(' . $exception->getCode() . '):'; - $this->_lines[] = '# Message: ' . $exception->getMessage(); - } - - /** - * Callback called on a skipped spec. - * - * @param object $report An expect report object. - */ - public function skip($report = null) - { - $this->_counters['skipped'] += 1; - $this->_counters['total'] += 1; - - $this->_formatTap(true, $report); + parent::start($args); + $this->write("\n1..{$args['total']}\n"); } /** - * Callback called when a `Kahlan\IncompleteException` occur. + * Callback called after a spec execution. * - * @param object $report An expect report object. + * @param object $log The log object of the whole spec. */ - public function incomplete($report = null) + public function specEnd($log = null) { - $this->_counters['skipped'] += 1; - $this->_counters['total'] += 1; - - $this->_formatTap(true, $report); - } - - /** - * Callback called at the end of specs processing. - * - * @param array $results The results array of the execution. - */ - public function end($results = []) - { - $this->write("1..{$this->_counters['total']}\n"); - foreach($this->_lines as $line) { - $this->write($line . "\n"); + $isOk = $log->passed() ? "ok" : "not ok"; + + switch ($log->type()) { + case 'skipped': + case 'pending': + case 'excluded': + $prefix = "# {$log->type()} "; + break; + default: + $prefix = '- '; + break; + } + $message = $prefix . trim(implode(" ", $log->messages())); + $this->_counter++; + + $this->write("{$isOk} {$this->_counter} {$message}\n"); + + if ($exception = $log->exception()) { + $this->write('# Exception: `' . get_class($exception) .'` Code(' . $exception->getCode() . '):' . "\n"); + $this->write('# Message: ' . $exception->getMessage() . "\n"); + } else { + foreach ($log->children() as $log) { + if ($log->passed()) { + continue; + } + $toString = function ($instance) { + return 'an instance of `' . get_class($instance) . '`'; + }; + foreach ($log->data() as $key => $value) { + $key = ucfirst($key); + $value = Text::toString($value, ['object' => ['method' => $toString]]); + $this->write("# {$key}: {$value}\n"); + } + } } - - $this->write("# total {$this->_counters['total']}\n"); - $this->write("# pass {$this->_counters['success']}\n"); - $this->write("# fail {$this->_counters['failed']}\n"); - $this->write("# skip {$this->_counters['skipped']}\n"); } /** - * Export a report to its TAP representation. + * Callback called at the end of specs processing. * - * @param boolean $success The success value. - * @param object $report The report to export. - * @return The TAP string representation of the report. + * @param object $summary The execution summary instance. */ - protected function _formatTap($success, $report) + public function end($summary) { - $isOk = ($success) ? "ok" : "not ok"; - $message = $report->file() . ": " .trim(implode(" ", $report->messages())); - $this->_lines[] = "{$isOk} {$this->_counters['total']} {$message}"; + $this->write("# total {$summary->total()}\n"); + $this->write("# passed {$summary->passed()}\n"); + $this->write("# pending {$summary->pending()}\n"); + $this->write("# skipped {$summary->skipped()}\n"); + $this->write("# excluded {$summary->excluded()}\n"); + $this->write("# failed {$summary->failed()}\n"); + $this->write("# errored {$summary->errored()}\n"); } - } diff --git a/src/Reporter/Terminal.php b/src/Reporter/Terminal.php index 575014c3..05bc5044 100644 --- a/src/Reporter/Terminal.php +++ b/src/Reporter/Terminal.php @@ -81,11 +81,11 @@ public function __construct($config = []) /** * Callback called before any specs processing. * - * @param array $params The suite params array. + * @param array $args The suite arguments. */ - public function start($params) + public function start($args) { - parent::start($params); + parent::start($args); if (!$this->_header) { return; } @@ -96,7 +96,7 @@ public function start($params) } /** - * Returns the Kahlan ascii art string. + * Return the Kahlan ascii art string. * * @return string */ @@ -112,7 +112,7 @@ public function kahlan() } /** - * Returns the Kahlan baseline string. + * Return the Kahlan baseline string. * * @return string */ @@ -122,28 +122,40 @@ public function kahlanBaseline() } /** - * Prints a spec report with its parents messages. + * Print a spec report with its parents messages. * - * @param object $report A spec report instance. + * @param object $log A spec log instance. */ - protected function _report($report) + protected function _report($log) { - $this->_reportSuiteMessages($report); - $this->_reportSpecMessage($report); - $this->_reportExpect($report); - $this->indent(0); + $type = $log->type(); + $this->_reportSuiteMessages($log); + $this->_reportSpecMessage($log); + $this->_reportFailure($log); + $this->_indent = 0; + } + + /** + * Print a spec report. + * + * @param object $log A spec log instance. + */ + protected function _reportSpec($log) + { + $this->_reportSpecMessage($log); + $this->_reportFailure($log); } /** - * Prints an array of description messages to STDOUT + * Print an array of description messages to STDOUT * * @param array $messages An array of description message. * @return integer The final message indentation. */ - protected function _reportSuiteMessages($report) + protected function _reportSuiteMessages($log) { $this->_indent = 0; - $messages = array_values(array_filter($report->messages())); + $messages = array_values(array_filter($log->messages())); array_pop($messages); foreach ($messages as $message) { $this->write($message); @@ -153,124 +165,124 @@ protected function _reportSuiteMessages($report) } /** - * Prints a spec report. + * Print a spec message report. * - * @param object $report A spec report instance. + * @param object $log A spec log instance. */ - protected function _reportSpec($report) - { - $this->_reportSpecMessage($report); - foreach($report->childs() as $child) { - $this->_reportExpect($child); - } - } - - protected function _reportSpecMessage($report) + protected function _reportSpecMessage($log) { - $messages = $report->messages(); + $messages = $log->messages(); $message = end($messages); - switch($report->type()) { - case "pass": - $this->write("✔", 'green'); - $this->write(" "); + switch ($log->type()) { + case 'passed': + $this->write('✔', 'green'); + $this->write(' '); + $this->write("{$message}\n", 'd'); + break; + case 'skipped': + $this->write('✔', 'd'); + $this->write(' '); $this->write("{$message}\n", 'd'); - break; - case "skip": - $this->write("↩", 'cyan'); - $this->write(" "); + break; + case 'pending': + $this->write('✔', 'cyan'); + $this->write(' '); $this->write("{$message}\n", 'cyan'); - break; - case "fail": - $this->write("✘", 'red'); - $this->write(" "); + break; + case 'excluded': + $this->write('✔', 'yellow'); + $this->write(' '); + $this->write("{$message}\n", 'yellow'); + break; + case 'failed': + $this->write('✘', 'red'); + $this->write(' '); $this->write("{$message}\n", 'red'); - break; - case "exception": - $this->write("✘", 'red'); - $this->write(" "); + break; + case 'errored': + $this->write('✘', 'red'); + $this->write(' '); $this->write("{$message}\n", 'red'); - break; - case "incomplete": - $this->write("✘", 'red'); - $this->write(" "); - $this->write("{$message}\n", 'red'); - break; + break; } } /** - * Prints an expectation report. + * Print an expectation report. * - * @param object $report An expectation report. + * @param object $log An specification log. */ - protected function _reportExpect($report) + protected function _reportFailure($log) { $this->_indent++; - switch($report->type()) { - case "skip": - $this->write("specification skipped in ", 'cyan'); - $this->write("`{$report->file()}` "); - $this->write("line {$report->line()}", 'cyan'); - $this->write("\n\n"); - break; - case "fail": - $this->write("expect->{$report->matcherName()}() failed in ", 'red'); - $this->write("`{$report->file()}` "); - $this->write("line {$report->line()}", 'red'); - $this->write("\n\n"); - $this->_reportDiff($report); - break; - case "exception": - $this->write("an uncaught exception has been thrown in ", 'magenta'); - $this->write("`{$report->file()}` "); - $this->write("line {$report->line()}", 'magenta'); - $this->write("\n\n"); + $type = $log->type(); + switch ($type) { + case "failed": + foreach ($log->children() as $expectation) { + if ($expectation->type() !== 'failed') { + continue; + } + $this->write("expect->{$expectation->matcherName()}() failed in ", 'red'); + $this->write("`{$expectation->file()}` "); + $this->write("line {$expectation->line()}", 'red'); + $this->write("\n\n"); + $this->_reportDiff($expectation); + } + break; + case "errored": + $backtrace = Debugger::backtrace(['trace' => $log->exception()]); + $trace = reset($backtrace); + $file = preg_replace('~' . preg_quote(getcwd(), '~') . '/~', '', $trace['file']); + $line = $trace['line']; - $this->write('message:', 'yellow'); - $this->_reportException($report->exception()); - $this->prefix($this->format(' ', 'n;;magenta') . ' '); - $this->write(Debugger::trace(['trace' => $report->backtrace()])); - $this->prefix(''); - $this->write("\n\n"); - break; - case "incomplete": - $this->write("an unexisting class has been used in ", 'yellow'); - $this->write("`{$report->file()}` "); - $this->write("line {$report->line()}", 'yellow'); + $this->write("an uncaught exception has been thrown in ", 'magenta'); + $this->write("`{$file}` "); + $this->write("line {$line}", 'magenta'); $this->write("\n\n"); $this->write('message:', 'yellow'); - $this->_reportException($report->exception()); + $this->_reportException($log->exception()); $this->prefix($this->format(' ', 'n;;magenta') . ' '); - $this->write(Debugger::trace(['trace' => $report->backtrace()])); + $this->write(Debugger::trace(['trace' => $backtrace])); $this->prefix(''); $this->write("\n\n"); - break; + break; } $this->_indent--; } /** - * Prints diff of spec's params. + * Print diff of spec's data. * - * @param array $report A report array. + * @param array $log A log array. */ - protected function _reportDiff($report) + protected function _reportDiff($log) { - $params = $report->params(); - foreach ($params as $key => $value) { + $data = $log->data(); + + $this->write("It expect actual "); + + if ($log->not()) { + $this->write('NOT ', 'cyan'); + $not = 'not '; + } else { + $not = ''; + } + $this->write("to {$log->description()}\n\n"); + + foreach ($data as $key => $value) { if (preg_match('~actual~', $key)) { $this->write("{$key}:\n", 'yellow'); $this->prefix($this->format(' ', 'n;;91') . ' '); } elseif (preg_match('~expected~', $key)) { - $this->write("{$key}:\n", 'yellow'); + $this->write("{$not}{$key}:\n", 'yellow'); $this->prefix($this->format(' ', 'n;;92') . ' '); } else { $this->write("{$key}:\n", 'yellow'); } $type = gettype($value); - $toString = function($instance) { + $toString = function ($instance) { return 'an instance of `' . get_class($instance) . '`'; }; $this->write("({$type}) " . Text::toString($value, ['object' => ['method' => $toString]])); @@ -280,6 +292,11 @@ protected function _reportDiff($report) $this->write("\n"); } + /** + * Print an exception to the outpout. + * + * @param object $exception An exception. + */ protected function _reportException($exception) { $msg = '`' . get_class($exception) .'` Code(' . $exception->getCode() . ') with '; @@ -293,7 +310,7 @@ protected function _reportException($exception) } /** - * Prints a string to output. + * Print a string to output. * * @param string $string The string to print. * @param string|array $options The possible values for an array are: @@ -324,7 +341,7 @@ public function write($string, $options = null) } /** - * Gets/sets the console indentation. + * Get/set the console indentation. * * @param integer $indent The indent number. * @return integer Returns the indent value. @@ -338,7 +355,7 @@ public function indent($indent = null) } /** - * Gets/sets the console prefix to use for writing. + * Get/set the console prefix to use for writing. * * @param string $prefix The prefix. * @return string Returns the prefix value. @@ -372,76 +389,135 @@ public function format($string, $options = null) } /** - * Prints a summary of specs execution to STDOUT + * Humanizes values using an appropriate unit. * - * @param array $results The results array of the execution. + * @return integer $value The value. + * @return integer $precision The required precision. + * @return integer $base The unit base. + * @return string The Humanized string value. */ - public function _summary($report) + public function readableSize($value, $precision = 0, $base = 1024) { - $results = $report['specs']; - - $passed = count($results['passed']) + count($results['skipped']); - $failed = 0; - foreach ([ - 'exceptions' => 'exception', - 'incomplete' => 'incomplete', - 'failed' => 'fail' - ] as $key => $value) { - ${$value} = count($results[$key]); - $failed += ${$value}; + $i = 0; + if ($value < 1) { + return '0'; } - $total = $passed + $failed; - $this->write('Executed ' . $passed . " of {$total} "); + $units = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; + while (($value / $base) >= 1) { + $value = $value / $base; + $i++; + } + $unit = isset($units[$i]) ? $units[$i] : '?'; + return round($value, $precision) . $unit; + } - if ($failed) { + /** + * Print a summary of specs execution to STDOUT + * + * @param object $summary The execution summary instance. + */ + public function _reportSummary($summary) + { + $this->_summarizeSkipped($summary); + + $passed = $summary->passed(); + $skipped = $summary->skipped(); + $pending = $summary->pending(); + $excluded = $summary->excluded(); + $failed = $summary->failed(); + $errored = $summary->errored(); + $expectation = $summary->expectation(); + $total = $summary->executable(); + + $this->write("Expectations : "); + $this->write("{$expectation} Executed"); + $this->write("\n"); + $this->write("Specifications : "); + $this->write("{$pending} Pending", 'cyan'); + $this->write(", "); + $this->write("{$excluded} Excluded", 'yellow'); + $this->write(", "); + $this->write("{$skipped} Skipped", 'd'); + $this->write("\n\n"); + $this->write('Passed ' . ($passed), 'green'); + $this->write(" of {$total} "); + + if ($failed + $errored) { $this->write('FAIL ', 'red'); $this->write('('); $comma = false; - if ($fail) { - $this->write('FAILURE: ' . $fail , 'red'); - $comma = true; - } - if ($incomplete) { - if ($comma) { - $this->write(', '); - } - $this->write('INCOMPLETE: ' . $incomplete , 'yellow'); + if ($failed) { + $this->write('FAILURE: ' . $failed, 'red'); $comma = true; } - if ($exception) { + if ($errored) { if ($comma) { $this->write(', '); } - $this->write('EXCEPTION: ' . $exception , 'magenta'); + $this->write('EXCEPTION: ' . $errored, 'magenta'); } $this->write(')'); } else { $this->write('PASS', 'green'); } $time = number_format(microtime(true) - $this->_start, 3); - $this->write(" in {$time} seconds\n\n\n"); + $memory = $this->readableSize($summary->memoryUsage()); + $this->write(" in {$time} seconds (using {$memory}o)"); + $this->write("\n\n"); + + $this->_summarizeFocused($summary); } /** - * Prints focused report to STDOUT + * Print focused report to STDOUT * - * @param array $report A report array. + * @param object $summary The execution summary instance. */ - protected function _reportFocused($report) + protected function _summarizeFocused($summary) { - if (!$backtraces = $report['focuses']) { + if (!$focused = $summary->get('focused')) { return; } $this->write("Focus Mode Detected in the following files:\n", 'b;yellow;'); - foreach ($backtraces as $backtrace) { + foreach ($focused as $scope) { + $backtrace = $scope->backtrace(); $this->write(Debugger::trace(['trace' => $backtrace, 'depth' => 1]), 'n;yellow'); $this->write("\n"); } $this->write("exit(-1)\n\n", 'red'); } + /** + * Print focused report to STDOUT + * + * @param object $summary The execution summary instance. + */ + protected function _summarizeSkipped($summary) + { + foreach ([ + 'pending' => 'cyan', + 'excluded' => 'yellow', + 'skipped' => '90' + ] as $type => $color) { + if (!$logs = $summary->logs($type)) { + continue; + } + $count = count($logs); + if ($this->_colors) { + $this->prefix($this->format(' ', "n;;{$color}") . ' '); + } + $this->write(ucfirst($type) . " specification" . ($count > 1 ? 's' : '') . ": {$count}\n"); + + foreach ($logs as $log) { + $this->write("{$log->file()}, line {$log->line()}\n", 'd'); + } + $this->prefix(''); + $this->write("\n"); + } + } + /** * Destructor */ diff --git a/src/Reporter/Verbose.php b/src/Reporter/Verbose.php index b1ab558c..d21f91a1 100644 --- a/src/Reporter/Verbose.php +++ b/src/Reporter/Verbose.php @@ -1,28 +1,27 @@ write("\n"); } /** * Callback called on a suite start. * - * @param object $report The report object of the whole spec. + * @param object $suite The suite instance. */ - public function suiteStart($report = null) + public function suiteStart($suite = null) { - $messages = $report->messages(); + $messages = $suite->messages(); $message = end($messages); $this->write("{$message}\n", "b;"); $this->_indent++; @@ -31,9 +30,9 @@ public function suiteStart($report = null) /** * Callback called after a suite execution. * - * @param object $report The report object of the whole spec. + * @param object $suite The suite instance. */ - public function suiteEnd($report = null) + public function suiteEnd($suite = null) { $this->_indent--; } @@ -41,20 +40,29 @@ public function suiteEnd($report = null) /** * Callback called after a spec execution. * - * @param object $report The report object of the whole spec. + * @param object $log The log object of the whole spec. */ - public function specEnd($report = null) + public function specEnd($log = null) { - $this->_reportSpec($report); + $this->_reportSpec($log); } /** * Callback called at the end of specs processing. + * + * @param object $summary The execution summary instance. */ - public function end($results = []) + public function end($summary) { + $this->write("\n"); + + foreach ($summary->logs() as $log) { + if (!$log->passed()) { + $this->_report($log); + } + } + $this->write("\n\n"); - $this->_summary($results); - $this->_reportFocused($results); + $this->_reportSummary($summary); } } diff --git a/src/Reporters.php b/src/Reporters.php index 6b546f5c..7e0a2190 100644 --- a/src/Reporters.php +++ b/src/Reporters.php @@ -80,7 +80,7 @@ public function clear() * @param string $type The name of the report. * @param array $data The data to report. */ - public function process($type, $data = null) + public function dispatch($type, $data = null) { foreach ($this->_reporters as $reporter) { $reporter->$type($data); diff --git a/src/Scope.php b/src/Scope.php index 8dcee469..327aa284 100644 --- a/src/Scope.php +++ b/src/Scope.php @@ -8,6 +8,20 @@ class Scope { + /** + * Indicates whether the scope has been runned or not. + * + * @var boolean + */ + protected $_runned = false; + + /** + * Stores the success value. + * + * @var boolean + */ + protected $_passed = true; + /** * Instances stack. * @@ -32,15 +46,14 @@ class Scope 'context' => true, 'current' => true, 'describe' => true, - 'dispatch' => true, - 'emitReport' => true, + 'excluded' => true, 'expect' => true, - 'focus' => true, 'focused' => true, 'failfast' => true, 'given' => true, 'hash' => true, 'it' => true, + 'log' => true, 'logs' => true, 'matcher' => true, 'message' => true, @@ -51,11 +64,12 @@ class Scope 'registered' => true, 'report' => true, 'reset' => true, - 'results' => true, 'run' => true, 'skipIf' => true, 'status' => true, + 'summary' => true, 'timeout' => true, + 'type' => true, 'wait' => true, 'fdescribe' => true, 'fcontext' => true, @@ -66,14 +80,11 @@ class Scope ]; /** - * Class dependencies. + * The scope type. * - * @var array + * @var object */ - protected $_classes = [ - 'expectation' => 'Kahlan\Expectation', - 'given' => 'Kahlan\Given' - ]; + protected $_type = null; /** * The root instance. @@ -118,44 +129,18 @@ class Scope protected $_given = []; /** - * The report result of executed spec. + * The report log of executed spec. * * @var object */ - protected $_report = null; - - /** - * The results array. - * - * @var array - */ - protected $_results = [ - 'passed' => [], - 'failed' => [], - 'skipped' => [], - 'exceptions' => [], - 'incomplete' => [] - ]; + protected $_log = null; /** - * The matching beetween events name & result types. + * The execution summary instance. * - * @var array - */ - protected $_resultTypes = [ - 'pass' => 'passed', - 'fail' => 'failed', - 'skip' => 'skipped', - 'exception' => 'exceptions', - 'incomplete' => 'incomplete' - ]; - - /** - * Focused scope detected. - * - * @var boolean + * @var object */ - protected $_focused = false; + protected $_summary = null; /** * Count the number of failure or exception. @@ -163,7 +148,7 @@ class Scope * @see ::failfast() * @var integer */ - protected $_failure = 0; + protected $_failures = 0; /** * The reporters container. @@ -203,34 +188,45 @@ class Scope * The Constructor. * * @param array $config The Suite config array. Options are: + * -`'type'` _string_ : supported type are `'normal'` & `'focus'`. * -`'message'` _string_ : the description message. * -`'parent'` _object_ : the parent scope. * -`'root'` _object_ : the root scope. + * -`'log'` _object_ : the log instance. + * -`'timeout'` _integer_: the timeout. */ public function __construct($config = []) { $defaults = [ + 'type' => 'normal', 'message' => '', 'parent' => null, 'root' => null, + 'log' => null, 'timeout' => 0, - 'classes' => [] + 'summary' => null ]; $config += $defaults; - $this->_classes += $config['classes']; - /** - * @var Message $message - * @var Scope $parent - * @var integer $timeout - */ - extract($config); - - $this->_message = $message; - $this->_parent = $parent; - $this->_root = $parent ? $parent->_root : $this; - $this->_report = new Report(['scope' => $this]); - $this->_timeout = $timeout; + + $this->_type = $config['type']; + $this->_message = $config['message']; + $this->_parent = $config['parent']; + $this->_root = $this->_parent ? $this->_parent->_root : $this; + $this->_timeout = $config['timeout']; $this->_backtrace = Debugger::focus($this->backtraceFocus(), Debugger::backtrace(), 1); + $this->_log = $config['log'] ?: new Log([ + 'scope' => $this, + 'backtrace' => $this->_backtrace + ]); + $this->_summary = $config['summary']; + if ($this->_summary) { + return; + } + if ($this->_root->summary()) { + $this->_summary = $this->_root->summary(); + } else { + $this->_summary = new Summary(); + } } /** @@ -253,7 +249,7 @@ public function &__get($key) return $this->_parent->__get($key); } if (in_array($key, static::$blacklist)) { - if ($key == 'expect') { + if ($key === 'expect') { throw new Exception("You can't use expect() inside of describe()"); } } @@ -278,18 +274,18 @@ public function __set($key, $value) /** * Allow closures assigned to the scope property to be inkovable. * - * @param string $name Name of the method being called. - * @param array $params Enumerated array containing the passed parameters. + * @param string $name Name of the method being called. + * @param array $args Enumerated array containing the passed arguments. * @return mixed * @throws Throw an Exception if the property doesn't exists / is not callable. */ - public function __call($name, $params) + public function __call($name, $args) { $property = null; $property = $this->__get($name); if (is_callable($property)) { - return call_user_func_array($property, $params); + return call_user_func_array($property, $args); } throw new Exception("Uncallable variable `{$name}`."); } @@ -306,8 +302,8 @@ public function given($name, $closure) if (isset(static::$blacklist[$name])) { throw new Exception("Sorry `{$name}` is a reserved keyword, it can't be used as a scope variable."); } - $class = $this->_classes['given']; - $given = new $class($closure); + + $given = new Given($closure); if (array_key_exists($name, $this->_given)) { $given->{$name} = $this->_given[$name](static::current()); } @@ -350,6 +346,16 @@ public function messages() return $messages; } + /** + * Gets the backtrace array. + * + * @return array + */ + public function backtrace() + { + return $this->_backtrace; + } + /** * Skips specs(s) if the condition is `true`. * @@ -366,24 +372,29 @@ public function skipIf($condition) } /** - * Skips childs specs(s). + * Skips children specs(s). * * @param object $exception The exception at the origin of the skip. * @param boolean $emit Indicated if report events should be generated. */ - protected function _skipChilds($exception, $emit = false) + protected function _skipChildren($exception, $emit = false) { - $report = $this->report(); + $log = $this->log(); if ($this instanceof Suite) { - foreach ($this->_childs as $child) { - $child->_skipChilds($exception, true); + foreach ($this->children() as $child) { + $child->_skipChildren($exception, true); } } elseif ($emit) { - $this->emitReport('specStart', $report); - $report->add('skip', ['exception' => $exception]); - $this->emitReport('specEnd', $report); + if (!$this->_root->focused() || $this->focused()) { + $this->report('specStart', $this); + $this->_passed = true; + $this->log()->type('skipped'); + $this->summary()->log($this->log()); + $this->report('specEnd', $log); + } } else { - $report->add('skip', ['exception' => $exception]); + $this->_passed = true; + $this->log()->type('skipped'); } } @@ -395,21 +406,19 @@ protected function _skipChilds($exception, $emit = false) */ protected function _exception($exception, $inEachHook = false) { - $data = compact('exception'); - switch(get_class($exception)) { + switch (get_class($exception)) { case 'Kahlan\SkipException': if ($inEachHook) { - $this->report()->add('skip', $data); + $this->log()->type('skipped'); } else { - $this->_skipChilds($exception); + $this->_skipChildren($exception); } - break; - case 'Kahlan\IncompleteException': - $this->report()->add('incomplete', $data); - break; + break; default: - $this->report()->add('exception', $data); - break; + $this->_passed = false; + $this->log()->type('errored'); + $this->log()->exception($exception); + break; } } @@ -460,29 +469,47 @@ public function backtraceFocus($pattern = null) if ($pattern === null) { return $this->_root->_backtraceFocus; } - return $this->_root->_backtraceFocus = strtr(preg_quote($pattern, '~'), ['\*' => '.*', '\?' => '.']); + $patterns = is_array($pattern) ? $pattern : [$pattern]; + foreach ($patterns as $key => $value) { + $patterns[$key] = preg_quote($value, '~'); + } + $pattern = join('|', $patterns); + return $this->_root->_backtraceFocus = strtr($pattern, ['\*' => '.*', '\?' => '.']); + } + + /** + * Set/get the scope type. + * + * @param string The type mode. + * @return mixed + */ + public function type($type = null) + { + if (!func_num_args()) { + return $this->_type; + } + $this->_type = $type; + return $this; } /** - * Sets focused mode. + * Check for excluded mode. * - * @param boolean The focus mode. * @return boolean */ - public function focus($state = true) + public function excluded() { - return $this->_focused = $state; + return $this->_type === 'exclude'; } /** - * Gets focused mode. + * Check for focused mode. * - * @param boolean|null For the setter behavior. * @return boolean */ public function focused() { - return $this->_focused; + return $this->_type === 'focus'; } /** @@ -490,11 +517,11 @@ public function focused() */ protected function _emitFocus() { - $this->_root->_focuses[] = Debugger::focus($this->backtraceFocus(), Debugger::backtrace()); + $this->_root->summary()->add('focused', $this); $instances = $this->_parents(true); foreach ($instances as $instance) { - $instance->focus(); + $instance->type('focus'); } } @@ -503,17 +530,9 @@ protected function _emitFocus() * * @return array */ - public function results() + public function summary() { - return $this->_results; - } - - /** - * Notifies a failure occurs. - */ - public function failure() - { - $this->_root->_failure++; + return $this->_root->_summary; } /** @@ -530,38 +549,31 @@ public static function current() * Dispatches a report up to the root scope. * It only logs expectations report. * - * @param object $report The report object to log. + * @param object $log The report object to log. */ - public function dispatch($report) + public function log($type = null, $data = []) { - $resultType = $this->_resultTypes[$report->type()]; - $this->_root->_results[$resultType][] = $report; - - $this->emitReport($report->type(), $report); + if (!func_num_args()) { + return $this->_log; + } + $this->report($type, $this->log()->add($type, $data)); } /** - * Emit a report even up to reporters. + * Send some data to reporters. * - * @param string $type The name of the report. - * @param array $data The data to report. + * @param string $type The message type. + * @param mixed $data The message data. */ - public function emitReport($type, $data = null) + public function report($type, $data, $byPassFocuses = false) { if (!$this->_root->_reporters) { return; } - $this->_root->_reporters->process($type, $data); - } - - /** - * Gets the report instance. - * - * @return object The report instance. - */ - public function report() - { - return $this->_report; + if (!$byPassFocuses && $this->_root->focused() && !$this->focused()) { + return; + } + $this->_root->_reporters->dispatch($type, $data); } /** @@ -586,5 +598,4 @@ public function timeout($timeout = null) } return $this->_timeout; } - } diff --git a/src/Specification.php b/src/Specification.php index 1d692c58..72927eeb 100644 --- a/src/Specification.php +++ b/src/Specification.php @@ -2,50 +2,47 @@ namespace Kahlan; use Closure; +use Throwable; use Exception; class Specification extends Scope { - /** - * Stores the success value. - * - * @var boolean - */ - protected $_passed = true; - /** * List of expectations. * @var Expectation[] */ protected $_expectations = []; + /** + * Store the return value of the spec closure. + * + * @var mixed + */ + protected $_return = null; + /** * Constructor. * * @param array $config The Suite config array. Options are: * -`'closure'` _Closure_ : the closure of the test. + * -`'message'` _string_ : the spec message. * -`'scope'` _string_ : supported scope are `'normal'` & `'focus'`. - * -`'matcher'` _object_ : the matcher instance. */ public function __construct($config = []) { $defaults = [ 'closure' => null, - 'message' => 'passes', - 'scope' => 'normal' + 'message' => 'passes' ]; $config += $defaults; $config['message'] = 'it ' . $config['message']; parent::__construct($config); - /** - * @var Closure $closure - * @var string $scope - */ - extract($config); + $config['closure'] = $config['closure'] ?: function () { + }; + $this->_closure = $this->_bind($config['closure'], 'it'); - $this->_closure = $this->_bind($closure, 'it'); - if ($scope === 'focus') { + if ($this->_type === 'focus') { $this->_emitFocus(); } } @@ -59,9 +56,7 @@ public function __construct($config = []) */ public function expect($actual, $timeout = -1) { - $expectation = $this->_classes['expectation']; - - return $this->_expectations[] = new $expectation(compact('actual', 'timeout')); + return $this->_expectations[] = new Expectation(compact('actual', 'timeout')); } /** @@ -74,10 +69,10 @@ public function expect($actual, $timeout = -1) public function waitsFor($actual, $timeout = 0) { $timeout = $timeout ?: $this->timeout(); - $actual = $actual instanceof Closure ? $actual : function () { + $closure = $actual instanceof Closure ? $actual : function () use ($actual) { return $actual; }; - $spec = new static(['closure' => $actual]); + $spec = new static(['closure' => $closure]); return $this->expect($spec, $timeout); } @@ -87,30 +82,86 @@ public function waitsFor($actual, $timeout = 0) * * @see Kahlan\Suite::process() */ - public function process() + protected function _process() { if ($this->_root->focused() && !$this->focused()) { return; } + if ($this->excluded()) { + $this->log()->type('excluded'); + $this->summary()->log($this->log()); + $this->report('specEnd', $this->log()); + return; + } $result = null; - try { - $this->_specStart(); + if (Suite::$PHP >= 7) { try { - $result = $this->run(); - } catch (Exception $exception) { - $this->_exception($exception); - } - foreach ($this->logs() as $log) { - $this->report()->add($log['type'], $log); + $this->_specStart(); + try { + $result = $this->_execute(); + } catch (Throwable $exception) { + $this->_exception($exception); + } + $this->_specEnd(); + } catch (Throwable $exception) { + $this->_exception($exception, true); + $this->_specEnd(!$exception instanceof SkipException); } - $this->_specEnd(); - } catch (Exception $exception) { - $this->_exception($exception, true); + } else { try { + $this->_specStart(); + try { + $result = $this->_execute(); + } catch (Exception $exception) { + $this->_exception($exception); + } $this->_specEnd(); } catch (Exception $exception) { + $this->_exception($exception, true); + $this->_specEnd(!$exception instanceof SkipException); + } + } + + return $this->_return = $result; + } + + /** + * Processes the spec. + */ + protected function _execute() + { + static::$_instances[] = $this; + + $result = null; + + $spec = function () { + $this->_expectations = []; + $closure = $this->_closure; + $result = $closure($this); + foreach ($this->_expectations as $expectation) { + $this->_passed = $expectation->passed() && $this->_passed; + } + array_pop(static::$_instances); + return $result; + }; + + if (Suite::$PHP >= 7) { + try { + $result = $spec(); + } catch (Throwable $e) { + $this->_passed = false; + array_pop(static::$_instances); + throw $e; + } + } else { + try { + $result = $spec(); + } catch (Exception $e) { + $this->_passed = false; + array_pop(static::$_instances); + throw $e; } } @@ -122,7 +173,7 @@ public function process() */ protected function _specStart() { - $this->emitReport('specStart', $this->report()); + $this->report('specStart', $this); if ($this->_parent) { $this->_parent->runCallbacks('beforeEach'); } @@ -131,53 +182,38 @@ protected function _specStart() /** * Spec end helper. */ - protected function _specEnd() + protected function _specEnd($runAfterEach = true) { - if (!$this->_parent) { - $this->emitReport('specEnd', $this->report()); - - return; + foreach ($this->_expectations as $expectation) { + foreach ($expectation->logs() as $log) { + $this->log($log['type'], $log); + } } - $this->_parent->runCallbacks('afterEach'); - $this->emitReport('specEnd', $this->report()); - $this->_parent->autoclear(); - } - /** - * Processes the spec. - */ - public function run() - { - if ($this->_locked) { - throw new Exception('Method not allowed in this context.'); + if ($this->log()->type() === 'passed' && !count($this->_expectations)) { + $this->log()->type('pending'); } + $type = $this->log()->type(); - $this->_locked = true; - static::$_instances[] = $this; - - $result = null; - $closure = $this->_closure; - $this->_expectations = []; + if ($type === 'failed' || $type === 'errored') { + $this->_root->_failures++; + } - try { - $this->_expectations = []; - $result = $closure($this); - foreach ($this->_expectations as $expectation) { - if (!$expectation->runned()) { - $expectation->run(); - } - $this->_passed = $this->_passed && $expectation->passed(); + if ($this->_parent && $runAfterEach) { + try { + $this->_parent->runCallbacks('afterEach'); + } catch (Exception $exception) { + $this->_exception($exception, true); } - array_pop(static::$_instances); - $this->_locked = false; - } catch (Exception $e) { - $this->_passed = false; - array_pop(static::$_instances); - $this->_locked = false; - throw $e; } - return $result; + $this->summary()->log($this->log()); + + $this->report('specEnd', $this->log()); + + if ($this->_parent) { + $this->_parent->autoclear(); + } } /** @@ -185,8 +221,13 @@ public function run() * * @return boolean Returns `true` if no error occurred, `false` otherwise. */ - public function passed() + public function passed(&$return = null) { + if (!$this->_runned) { + $this->_process(); + } + $this->_runned = true; + $return = $this->_return; return $this->_passed; } @@ -203,8 +244,6 @@ public function logs() $logs[] = $log; } } - return $logs; } - } diff --git a/src/Suite.php b/src/Suite.php index c913934c..b994f5ad 100644 --- a/src/Suite.php +++ b/src/Suite.php @@ -2,12 +2,15 @@ namespace Kahlan; use Closure; +use Throwable; use Exception; use InvalidArgumentException; use Kahlan\Analysis\Debugger; class Suite extends Scope { + public static $PHP = PHP_MAJOR_VERSION; + /** * Store all hashed references. * @@ -23,11 +26,11 @@ class Suite extends Scope protected $_status = null; /** - * The childs array. + * The children array. * * @var Suite[]|Specification[] */ - protected $_childs = []; + protected $_children = []; /** * Suite statistics. @@ -42,8 +45,8 @@ class Suite extends Scope * @var array */ protected $_callbacks = [ - 'before' => [], - 'after' => [], + 'beforeAll' => [], + 'afterAll' => [], 'beforeEach' => [], 'afterEach' => [], ]; @@ -55,13 +58,6 @@ class Suite extends Scope */ protected $_autoclear = []; - /** - * Saved backtrace of focused specs. - * - * @var array - */ - protected $_focuses = []; - /** * Set the number of fails allowed before aborting. `0` mean no fast fail. * @@ -76,31 +72,22 @@ class Suite extends Scope * @param array $config The Suite config array. Options are: * -`'closure'` _Closure_: the closure of the test. * -`'name'` _string_ : the type of the suite. - * -`'scope'` _string_ : supported scope are `'normal'` & `'focus'`. */ public function __construct($config = []) { $defaults = [ 'closure' => null, - 'name' => 'describe', - 'scope' => 'normal' + 'name' => 'describe' ]; $config += $defaults; parent::__construct($config); - /** - * @var Closure $closure - * @var string $name - * @var string $scope - */ - extract($config); - if ($this->_root === $this) { return; } - $closure = $this->_bind($closure, $name); + $closure = $this->_bind($config['closure'], $config['name']); $this->_closure = $closure; - if ($scope === 'focus') { + if ($this->_type === 'focus') { $this->_emitFocus(); } } @@ -113,14 +100,14 @@ public function __construct($config = []) * * @return Suite */ - public function describe($message, $closure, $timeout = null, $scope = 'normal') + public function describe($message, $closure, $timeout = null, $type = 'normal') { $parent = $this; $name = 'describe'; $timeout = $timeout !== null ? $timeout : $this->timeout(); - $suite = new Suite(compact('message', 'closure', 'parent', 'name', 'timeout', 'scope')); + $suite = new Suite(compact('message', 'closure', 'parent', 'name', 'timeout', 'type')); - return $this->_childs[] = $suite; + return $this->_children[] = $suite; } /** @@ -129,18 +116,18 @@ public function describe($message, $closure, $timeout = null, $scope = 'normal') * @param string $message Description message. * @param Closure $closure A test case closure. * @param null $timeout - * @param string $scope + * @param string $type * * @return Suite */ - public function context($message, $closure, $timeout = null, $scope = 'normal') + public function context($message, $closure, $timeout = null, $type = 'normal') { $parent = $this; $name = 'context'; $timeout = $timeout !== null ? $timeout : $this->timeout(); - $suite = new Suite(compact('message', 'closure', 'parent', 'name', 'timeout', 'scope')); + $suite = new Suite(compact('message', 'closure', 'parent', 'name', 'timeout', 'type')); - return $this->_childs[] = $suite; + return $this->_children[] = $suite; } /** @@ -148,15 +135,15 @@ public function context($message, $closure, $timeout = null, $scope = 'normal') * * @param string|Closure $message Description message or a test closure. * @param Closure $closure A test case closure. - * @param string $scope The scope. + * @param string $type The type. * * @return Specification */ - public function it($message, $closure = null, $timeout = null, $scope = 'normal') + public function it($message, $closure = null, $timeout = null, $type = 'normal') { static $inc = 1; if ($message instanceof Closure) { - $scope = $timeout; + $type = $timeout; $timeout = $closure; $closure = $message; $message = "spec #" . $inc++; @@ -164,8 +151,8 @@ public function it($message, $closure = null, $timeout = null, $scope = 'normal' $parent = $this; $root = $this->_root; $timeout = $timeout !== null ? $timeout : $this->timeout(); - $spec = new Specification(compact('message', 'closure', 'parent', 'root', 'timeout', 'scope')); - $this->_childs[] = $spec; + $spec = new Specification(compact('message', 'closure', 'parent', 'root', 'timeout', 'type')); + $this->_children[] = $spec; return $this; } @@ -178,8 +165,9 @@ public function it($message, $closure = null, $timeout = null, $scope = 'normal' * * @return */ - public function xdescribe($message, $closure) + public function xdescribe($message, $closure, $timeout = null) { + return $this->describe($message, $closure, $timeout, 'exclude'); } /** @@ -190,8 +178,9 @@ public function xdescribe($message, $closure) * * @return */ - public function xcontext($message, $closure) + public function xcontext($message, $closure, $timeout = null) { + return $this->context($message, $closure, $timeout, 'exclude'); } /** @@ -202,8 +191,9 @@ public function xcontext($message, $closure) * * @return */ - public function xit($message, $closure = null) + public function xit($message, $closure = null, $timeout = null) { + return $this->it($message, $closure, $timeout, 'exclude'); } /** @@ -252,10 +242,10 @@ public function fit($message, $closure = null, $timeout = null) * * @return self */ - public function before($closure) + public function beforeAll($closure) { - $this->_bind($closure, 'before'); - $this->_callbacks['before'][] = $closure; + $this->_bind($closure, 'beforeAll'); + $this->_callbacks['beforeAll'][] = $closure; return $this; } @@ -267,10 +257,10 @@ public function before($closure) * * @return self */ - public function after($closure) + public function afterAll($closure) { - $this->_bind($closure, 'after'); - $this->_callbacks['after'][] = $closure; + $this->_bind($closure, 'afterAll'); + $this->_callbacks['afterAll'][] = $closure; return $this; } @@ -306,32 +296,39 @@ public function afterEach($closure) } /** - * Suite run. + * Suite processing. * * @param array $options Process options. */ - protected function process($options = []) + protected function _process($options = []) { - if ($this->_root->focused() && !$this->focused()) { - return; - } static::$_instances[] = $this; $this->_errorHandler(true, $options); - try { + $suite = function () { $this->_suiteStart(); - foreach ($this->_childs as $child) { + foreach ($this->_children as $child) { if ($this->failfast()) { break; } - $child->process(); + $this->_passed = $child->passed() && $this->_passed; } $this->_suiteEnd(); - } catch (Exception $exception) { - $this->_exception($exception); + }; + + if (Suite::$PHP >= 7) { try { + $suite(); + } catch (Throwable $exception) { + $this->_exception($exception); $this->_suiteEnd(); + } + } else { + try { + $suite(); } catch (Exception $exception) { + $this->_exception($exception); + $this->_suiteEnd(); } } @@ -345,9 +342,9 @@ protected function process($options = []) protected function _suiteStart() { if ($this->message()) { - $this->emitReport('suiteStart', $this->report()); + $this->report('suiteStart', $this); } - $this->runCallbacks('before', false); + $this->runCallbacks('beforeAll', false); } /** @@ -355,9 +352,9 @@ protected function _suiteStart() */ protected function _suiteEnd() { - $this->runCallbacks('after', false); + $this->runCallbacks('afterAll', false); if ($this->message()) { - $this->emitReport('suiteEnd', $this->report()); + $this->report('suiteEnd', $this); } } @@ -368,7 +365,7 @@ protected function _suiteEnd() */ public function failfast() { - return $this->_root->_ff && $this->_root->_failure >= $this->_root->_ff; + return $this->_root->_ff && $this->_root->_failures >= $this->_root->_ff; } /** @@ -440,16 +437,16 @@ public function run($options = []) $this->_autoclear = (array)$options['autoclear']; $this->_ff = $options['ff']; - $this->emitReport('start', ['total' => $this->enabled()]); - $this->process(); - $this->emitReport('end', [ - 'specs' => $this->_results, - 'focuses' => $this->_focuses, - ]); + $this->report('start', ['total' => $this->enabled()], true); + + $success = $this->passed(); + $this->summary()->memoryUsage(memory_get_peak_usage()); + + $this->report('end', $this->summary(), true); $this->_locked = false; - return $this->passed(); + return $success; } /** @@ -459,11 +456,11 @@ public function run($options = []) */ public function passed() { - if (empty($this->_results['failed']) && empty($this->_results['exceptions']) && empty($this->_results['incomplete'])) { - return true; + if (!$this->_runned) { + $this->_process(); } - - return false; + $this->_runned = true; + return $this->_passed; } /** @@ -476,8 +473,7 @@ public function total() if ($this->_stats === null) { $this->stats(); } - - return $this->_stats['focused'] + $this->_stats['normal']; + return $this->_stats['normal'] + $this->_stats['focused'] + $this->_stats['excluded']; } /** @@ -490,7 +486,6 @@ public function enabled() if ($this->_stats === null) { $this->stats(); } - return $this->focused() ? $this->_stats['focused'] : $this->_stats['normal']; } @@ -499,10 +494,7 @@ public function enabled() */ public function stop() { - $this->emitReport('stop', [ - 'specs' => $this->_results, - 'focuses' => $this->_focuses, - ]); + $this->report('stop', $this->summary(), true); } /** @@ -513,29 +505,76 @@ public function stop() protected function stats() { static::$_instances[] = $this; + if (Suite::$PHP >= 7) { + try { + $this->_stats = $this->_stats(); + } catch (Throwable $exception) { + $this->_exception($exception); + $this->summary()->log($this->log()); + + $this->_stats = [ + 'normal' => 0, + 'focused' => 0, + 'excluded' => 0 + ]; + } + } else { + try { + $this->_stats = $this->_stats(); + } catch (Exception $exception) { + $this->_passed = false; + array_pop(static::$_instances); + throw $exception; + } + } + array_pop(static::$_instances); + return $this->_stats; + } + + /** + * Builds the suite. + * + * @return array The suite stats. + */ + protected function _stats() + { if ($closure = $this->_closure) { $closure($this); } $normal = 0; $focused = 0; - foreach ($this->childs() as $child) { + $excluded = 0; + foreach ($this->children() as $child) { + if ($this->excluded()) { + $child->type('exclude'); + } if ($child instanceof Suite) { $result = $child->stats(); if ($child->focused() && !$result['focused']) { $focused += $result['normal']; + $excluded += $result['excluded']; $child->_broadcastFocus(); } else { - $focused += $result['focused']; $normal += $result['normal']; + $focused += $result['focused']; + $excluded += $result['excluded']; } } else { - $child->focused() ? $focused++ : $normal++; + switch ($child->type()) { + case 'exclude': + $excluded++; + break; + case 'focus': + $focused++; + break; + default: + $normal++; + break; + } } } - array_pop(static::$_instances); - - return $this->_stats = compact('normal', 'focused'); + return compact('normal', 'focused', 'excluded'); } /** @@ -547,29 +586,30 @@ protected function stats() */ public function status($status = null) { - if ($status !== null) { + if (func_num_args()) { $this->_status = $status; - } - - if ($this->_status !== null) { - return $this->_status; + return $this; } if ($this->focused()) { return -1; } + if ($this->_status !== null) { + return $this->_status; + } + return $this->passed() ? 0 : -1; } /** - * Gets childs. + * Gets children. * - * @return array The array of childs instances. + * @return array The array of children instances. */ - public function childs() + public function children() { - return $this->_childs; + return $this->_children; } /** @@ -584,16 +624,6 @@ public function callbacks($type) return isset($this->_callbacks[$type]) ? $this->_callbacks[$type] : []; } - /** - * Gets references of focused specs. - * - * @return array - */ - public function focuses() - { - return $this->_focuses; - } - /** * Autoclears plugins. */ @@ -615,8 +645,8 @@ public function autoclear() */ protected function _broadcastFocus() { - foreach ($this->_childs as $child) { - $child->focus(); + foreach ($this->_children as $child) { + $child->type('focus'); if ($child instanceof Suite) { $child->_broadcastFocus(); } @@ -655,13 +685,13 @@ public static function register($hash) /** * Gets registered hashes. [Mainly used for optimizations] * - * @param string $hash The hash to look up. If `null` return all registered hashes. + * @param string $hash The hash to look up. If none return all registered hashes. * * @return array|bool */ public static function registered($hash = null) { - if (!$hash) { + if (!func_num_args()) { return static::$_registered; } @@ -675,5 +705,4 @@ public static function reset() { static::$_registered = []; } - } diff --git a/src/Summary.php b/src/Summary.php new file mode 100644 index 00000000..24a25ca8 --- /dev/null +++ b/src/Summary.php @@ -0,0 +1,152 @@ +_logs as $key => $value) { + $total += count($value); + } + return $total; + } + + /** + * Return the total number of expectations. + * + * @return integer + */ + public function expectation() + { + $total = 0; + foreach ($this->_logs as $key => $value) { + foreach ($value as $log) { + $total += count($log->children()); + } + } + return $total; + } + + /** + * Return the number of executable specs. + * + * @return integer + */ + public function executable() + { + return $this->passed() + $this->failed() + $this->errored(); + } + + /** + * Return the number of specs of a certain type. + * + * @return integer + */ + public function __call($name, $args) + { + return isset($this->_logs[$name]) ? count($this->_logs[$name]) : 0; + } + + /** + * Add a data to a specific key. + * + * @param string $type The type of data. + * @param mixed $value The value to add. + * @return self + */ + public function add($type, $value) + { + if (!isset($this->_data[$type])) { + $this->_data[$type] = []; + } + $this->_data[$type][] = $value; + return $this; + } + + /** + * Get a data of a specific key. + * + * @param string $type The type of data. + * @return array + */ + public function get($type) + { + return isset($this->_data[$type]) ? $this->_data[$type] : []; + } + + /** + * Ingest a log. + * + * @param array $log The log report. + * @return self + */ + public function log($log) + { + $type = $log->type(); + if (!isset($this->_logs[$type])) { + $this->_logs[$type] = []; + } + $this->_logs[$type][] = $log; + return $this; + } + + /** + * Get log report + * + * @param string $type The type of data. + * @return array + */ + public function logs($type = null) + { + if (func_num_args()) { + return isset($this->_logs[$type]) ? $this->_logs[$type] : []; + } + $logs = []; + foreach ($this->_logs as $key => $value) { + $logs = array_merge($logs, $value); + } + return $logs; + } + + + /** + * Return the total number of specs. + * + * @return integer + */ + public function memoryUsage($memoryUsage = null) + { + if (!func_get_args()) { + return $this->_memoryUsage; + } + $this->_memoryUsage = $memoryUsage; + return $this; + } +} diff --git a/src/Util/Text.php b/src/Util/Text.php index c87c9c63..bb8d8193 100644 --- a/src/Util/Text.php +++ b/src/Util/Text.php @@ -82,7 +82,7 @@ public static function clean($str, $options = []) $begin = $escape ? '(? $value) { + foreach ($datas as $key => $value) { if ($comma) { $string .= ",\n"; } @@ -186,7 +186,8 @@ protected static function _arrayToString($datas, $options) * @param array $value The object. * @return string The dumped string. */ - protected static function _objectToString($value, $options) { + protected static function _objectToString($value, $options) + { if ($value instanceof Exception) { $msg = '`' . get_class($value) .'` Code(' . $value->getCode() . ') with '; $message = $value->getMessage(); @@ -213,7 +214,8 @@ protected static function _objectToString($value, $options) { * @param mixed $value The scalar data to dump * @return string The dumped string. */ - public static function dump($value, $quote = '"') { + public static function dump($value, $quote = '"') + { if (is_bool($value)) { return $value ? 'true' : 'false'; } @@ -255,5 +257,4 @@ protected static function _dump($string) } return $unescaped; } - } diff --git a/src/functions.php b/src/functions.php new file mode 100644 index 00000000..3a563f54 --- /dev/null +++ b/src/functions.php @@ -0,0 +1,142 @@ +beforeAll($closure); +} + +function afterAll($closure) +{ + return Suite::current()->afterAll($closure); +} + +function beforeEach($closure) +{ + return Suite::current()->beforeEach($closure); +} + +function afterEach($closure) +{ + return Suite::current()->afterEach($closure); +} + +function describe($message, $closure, $timeout = null, $type = 'normal') +{ + if (!Suite::current()) { + $suite = box('kahlan')->get('suite.global'); + return $suite->describe($message, $closure, $timeout, $type); + } + return Suite::current()->describe($message, $closure, $timeout, $type); +} + +function context($message, $closure, $timeout = null, $type = 'normal') +{ + return Suite::current()->context($message, $closure, $timeout, $type); +} + +function given($name, $value) +{ + return Suite::current()->given($name, $value); +} + +function it($message, $closure = null, $timeout = null, $type = 'normal') +{ + return Suite::current()->it($message, $closure, $timeout, $type); +} + +function fdescribe($message, $closure, $timeout = null) +{ + return describe($message, $closure, $timeout, 'focus'); +} + +function fcontext($message, $closure, $timeout = null) +{ + return context($message, $closure, $timeout, 'focus'); +} + +function fit($message, $closure = null, $timeout = null) +{ + return it($message, $closure, $timeout, 'focus'); +} + +function xdescribe($message, $closure, $timeout = null) +{ + return describe($message, $closure, $timeout, 'exclude'); +} + +function xcontext($message, $closure, $timeout = null) +{ + return context($message, $closure, $timeout, 'exclude'); +} + +function xit($message, $closure = null, $timeout = null) +{ + return it($message, $closure, $timeout, 'exclude'); +} + +function waitsFor($actual, $timeout = null) +{ + return Specification::current()->waitsFor($actual, $timeout); +} + +function skipIf($condition) +{ + $current = Specification::current() ?: Suite::current(); + return $current->skipIf($condition); +} + +/** + * @param $actual + * + * @return Expectation + */ +function expect($actual) +{ + return Specification::current()->expect($actual); +} + +/** + * @param $actual + * + * @return Stubber + */ +function allow($actual) +{ + return new Allow($actual); +} + +function box($name = '', $box = null) +{ + static $boxes = []; + + if (func_num_args() === 1) { + if ($name === false) { + $boxes = []; + return; + } + if (is_object($name)) { + return $boxes[''] = $name; + } + if (isset($boxes[$name])) { + return $boxes[$name]; + } + throw new BoxException("Unexisting box `'{$name}'`."); + } + if (func_num_args() === 2) { + if ($box === false) { + unset($boxes[$name]); + return; + } + return $boxes[$name] = $box; + } + if (!isset($boxes[''])) { + $boxes[''] = new Box(); + } + return $boxes['']; +} diff --git a/src/init.php b/src/init.php index 5522cff1..0fe5a01b 100644 --- a/src/init.php +++ b/src/init.php @@ -2,6 +2,7 @@ use Kahlan\Expectation; use Kahlan\Suite; use Kahlan\Specification; +use Kahlan\Allow; use Kahlan\Box\BoxException; use Kahlan\Box\Box; @@ -36,72 +37,91 @@ !function_exists('xcontext') && !function_exists('waitsFor') && !function_exists('skipIf') && - !function_exists('expect')) { - + !function_exists('expect') && + !function_exists('allow')) { define('KAHLAN_FUNCTIONS_EXIST', true); - function before($closure) { - return Suite::current()->before($closure); + function beforeAll($closure) + { + return Suite::current()->beforeAll($closure); } - function after($closure) { - return Suite::current()->after($closure); + function afterAll($closure) + { + return Suite::current()->afterAll($closure); } - function beforeEach($closure) { + function beforeEach($closure) + { return Suite::current()->beforeEach($closure); } - function afterEach($closure) { + function afterEach($closure) + { return Suite::current()->afterEach($closure); } - function describe($message, $closure, $timeout = null, $scope = 'normal') { + function describe($message, $closure, $timeout = null, $type = 'normal') + { if (!Suite::current()) { $suite = box('kahlan')->get('suite.global'); - return $suite->describe($message, $closure, $timeout, $scope); + return $suite->describe($message, $closure, $timeout, $type); } - return Suite::current()->describe($message, $closure, $timeout, $scope); + return Suite::current()->describe($message, $closure, $timeout, $type); } - function context($message, $closure, $timeout = null, $scope = 'normal') { - return Suite::current()->context($message, $closure, $timeout, $scope); + function context($message, $closure, $timeout = null, $type = 'normal') + { + return Suite::current()->context($message, $closure, $timeout, $type); } - function given($name, $value) { + function given($name, $value) + { return Suite::current()->given($name, $value); } - function it($message, $closure, $timeout = null, $scope = 'normal') { - return Suite::current()->it($message, $closure, $timeout, $scope); + function it($message, $closure = null, $timeout = null, $type = 'normal') + { + return Suite::current()->it($message, $closure, $timeout, $type); } - function fdescribe($message, $closure, $timeout = null) { + function fdescribe($message, $closure, $timeout = null) + { return describe($message, $closure, $timeout, 'focus'); } - function fcontext($message, $closure, $timeout = null) { + function fcontext($message, $closure, $timeout = null) + { return context($message, $closure, $timeout, 'focus'); } - function fit($message, $closure = null, $timeout = null) { + function fit($message, $closure = null, $timeout = null) + { return it($message, $closure, $timeout, 'focus'); } - function xdescribe($message, $closure) { + function xdescribe($message, $closure, $timeout = null) + { + return describe($message, $closure, $timeout, 'exclude'); } - function xcontext($message, $closure) { + function xcontext($message, $closure, $timeout = null) + { + return context($message, $closure, $timeout, 'exclude'); } - function xit($message, $closure = null) { + function xit($message, $closure = null, $timeout = null) + { + return it($message, $closure, $timeout, 'exclude'); } - function waitsFor($actual, $timeout = null) { + function waitsFor($actual, $timeout = null) + { return Specification::current()->waitsFor($actual, $timeout); } - function skipIf($condition) { + function skipIf($condition) + { $current = Specification::current() ?: Suite::current(); return $current->skipIf($condition); } @@ -111,9 +131,20 @@ function skipIf($condition) { * * @return Expectation */ - function expect($actual) { + function expect($actual) + { return Specification::current()->expect($actual); } + + /** + * @param $actual + * + * @return Stubber + */ + function allow($actual) + { + return new Allow($actual); + } } $boxFuctions = true; @@ -129,7 +160,8 @@ function expect($actual) { if ($boxFuctions && !function_exists('box')) { define('BOX_FUNCTIONS_EXIST', true); - function box($name = '', $box = null) { + function box($name = '', $box = null) + { static $boxes = []; if (func_num_args() === 1) { @@ -157,5 +189,4 @@ function box($name = '', $box = null) { } return $boxes['']; } - }