├── .editorconfig ├── .github └── workflows │ └── test.yaml ├── .gitignore ├── LICENSE ├── README.md ├── bin └── quickcheck ├── composer.json ├── dev-env ├── Dockerfile └── toggle-ext ├── doc ├── annotations.md ├── cli-reference.md ├── cli-writing-tests.md ├── generators.md ├── introduction.md └── phpunit.md ├── examples ├── brokenSort.php ├── intArrayElementsLessThan1000.php ├── integerAddition.php └── strings │ ├── lengthLessThan200.php │ └── neverNumeric.php ├── phpunit.xml.dist ├── src └── QuickCheck │ ├── Annotation.php │ ├── Arrays.php │ ├── Check.php │ ├── CheckResult.php │ ├── CheckSuite.php │ ├── Exceptions │ ├── AmbiguousTypeAnnotationException.php │ ├── AnnotationException.php │ ├── DuplicateGeneratorException.php │ ├── MissingTypeAnnotationException.php │ └── NoGeneratorAnnotationException.php │ ├── Failure.php │ ├── Generator.php │ ├── Lazy.php │ ├── PHPUnit │ └── PropertyConstraint.php │ ├── Property.php │ ├── PropertyTest.php │ ├── Random.php │ ├── ShrinkResult.php │ ├── ShrinkTreeNode.php │ ├── Success.php │ ├── Test.php │ └── TestRunner.php └── test └── QuickCheck ├── AnnotationTest.php ├── LazyTest.php ├── PHPUnitIntegrationTest.php └── QuicknDirtyTest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | trim_trailing_whitespace = true 3 | indent_style = space 4 | indent_size = 4 -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | run: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | php-version: 13 | - '7.3' 14 | - '7.4' 15 | - '8.0' 16 | - '8.1' 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Setup PHP 21 | uses: shivammathur/setup-php@v2 22 | with: 23 | php-version: ${{ matrix.php-version }} 24 | tools: phpunit:${{ matrix.phpunit-versions }} 25 | 26 | - name: Install dependencies 27 | run: composer install 28 | 29 | - name: Run tests 30 | run: composer test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /phpunit.xml 2 | /vendor/ 3 | /build/ 4 | /composer.lock 5 | /.phpunit.result.cache 6 | /.idea/ 7 | .vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, Stefan Oestreicher and contributors 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | 1. Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 20 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 21 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 22 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 25 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 26 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 27 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 28 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHPQuickCheck 2 | 3 | PHPQuickCheck is a generative testing library for PHP based on 4 | clojure.test.check. 5 | 6 | > Don't write tests. Generate them. - John Hughes 7 | 8 | ## Huh? 9 | 10 | Generative testing, also called property-based testing, is about 11 | describing the behaviour of your system in terms of properties that 12 | should hold true for all possible input. 13 | 14 | ### Quickstart 15 | 16 | Install PHPQuickCheck: 17 | 18 | ``` 19 | composer require steos/quickcheck --dev 20 | ``` 21 | 22 | Create a property test `test/stringsAreNeverNumeric.php`: 23 | 24 | ```php 25 | vendor/bin/quickcheck test/stringsAreNeverNumeric.php -t 1000 41 | ``` 42 | 43 | ``` 44 | PHPQuickCheck 2.0.2. Don't write tests. Generate them. 45 | 46 | 834/1000 [=========================================>--------] 83% 47 | 48 | Time: 454 ms, Memory: 4.00 MB, Seed: 1578763578270, maxSize: 200 49 | 50 | Failing inputs: array ( 51 | 0 => '9E70', 52 | ) 53 | 54 | Shrinking inputs...done. (0.00 s) 55 | Smallest failing inputs: array ( 56 | 0 => '0', 57 | ) 58 | 59 | QED. (834 tests) 60 | ``` 61 | 62 | ### Documentation 63 | 64 | - [CLI Reference](doc/cli-reference.md) 65 | - [Writing CLI Tests](doc/cli-writing-tests.md) 66 | 67 | #### API 68 | 69 | - [Introduction](doc/introduction.md) 70 | - [PHPUnit Support](doc/phpunit.md) 71 | - [Using Annotations](doc/annotations.md) 72 | - [Generator Examples](doc/generators.md) 73 | 74 | #### Other Resources 75 | 76 | - [A QuickCheck Primer for PHP Developers](https://medium.com/@thinkfunctional/a-quickcheck-primer-for-php-developers-5ffbe20c16c8) 77 | - [Testing the hard stuff and staying sane (John Hughes)](https://www.youtube.com/watch?v=zi0rHwfiX1Q) 78 | 79 | ### xdebug 80 | 81 | PHPQuickCheck uses a lot of functional programming techniques which leads to a lot of nested functions. 82 | With xdebug default settings it can quickly lead to this error: 83 | 84 | ``` 85 | Error: Maximum function nesting level of '256' reached, aborting! 86 | ``` 87 | 88 | This happens due to the infinite recursion protection setting `xdebug.max_nesting_level`. 89 | Best is to disable this or set it to a high value. 90 | The phpunit config sets it to `9999`. 91 | 92 | ### Performance 93 | 94 | - Disable xdebug to get tests to run faster. It has a huge impact on the runtime performance. 95 | 96 | - Use the GMP extension. The RNG will use the gmp functions if available. Otherwise it falls back to very slow bit-fiddling in php userland. 97 | 98 | ## Project Status 99 | 100 | PHPQuickCheck is somewhat experimental. The core functionality of clojure.test.check (v0.5.9, August 2014) has been implemented. 101 | There have been a number of improvements to clojure.test.check since the initial port which have not been implemented yet. 102 | 103 | ### Contributing 104 | 105 | All contributions are welcome. 106 | 107 | Feel free to fork and send a pull request. If you intend to make 108 | major changes please get in touch so we can coordinate our efforts. 109 | 110 | #### Dev Setup 111 | 112 | The repository contains a Dockerfile to quickly set up a dev environment. 113 | It is based on the `php:7.3.18-cli` image and adds xdebug, gmp and composer. 114 | 115 | ``` 116 | $ docker build -t php-quickcheck-dev dev-env 117 | $ docker run --rm -it --mount src=$(pwd),target=/quickcheck,type=bind php-quickcheck-dev bash 118 | # cd /quickcheck 119 | # composer install 120 | # vendor/bin/phpunit 121 | # bin/quickcheck examples 122 | ``` 123 | 124 | The image also contains a small script `toggle-ext` to toggle php extensions on and off: 125 | 126 | ``` 127 | root@c871096e2c92:/quickcheck# toggle-ext xdebug 128 | xdebug is now disabled 129 | root@c871096e2c92:/quickcheck# 130 | ``` 131 | 132 | ## Credits 133 | 134 | All credit goes to clojure.test.check, this project is mostly just a port. 135 | 136 | ## Requirements 137 | 138 | Requires PHP 7.3.x with 64 bit integers. The gmp extension is recommended but not required. 139 | 140 | ## License 141 | 142 | Copyright © 2022, Stefan Oestreicher and contributors. 143 | 144 | Distributed under the terms of the BSD (3-Clause) license. 145 | -------------------------------------------------------------------------------- /bin/quickcheck: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | ')) { 5 | fwrite(STDERR, sprintf( 6 | 'This version of PHPQuickCheck requires PHP 7.3.x' . PHP_EOL . 7 | 'You are using PHP %s (%s).' . PHP_EOL, 8 | PHP_VERSION, 9 | PHP_BINARY 10 | )); 11 | exit(1); 12 | } 13 | 14 | function searchFiles($autoloadSearchPaths) { 15 | foreach ($autoloadSearchPaths as $path) { 16 | $file = __DIR__ . $path; 17 | if (file_exists($file)) { 18 | return $file; 19 | } 20 | } 21 | return null; 22 | } 23 | 24 | $autoload = searchFiles([ 25 | '/../../../autoload.php', 26 | '/../vendor/autoload.php', 27 | ]); 28 | 29 | if ($autoload === null) { 30 | fwrite(STDERR, 'Could not find autoload.php' . PHP_EOL); 31 | exit(1); 32 | } 33 | 34 | require_once $autoload; 35 | 36 | \QuickCheck\Test::main($argv); 37 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "steos/quickcheck", 3 | "description": "a generative testing library", 4 | "license": "BSD-3-Clause", 5 | "require": { 6 | "php-64bit": ">=7.3.0", 7 | "symfony/console": "^5.3 || ^6.0" 8 | }, 9 | "require-dev": { 10 | "phpunit/phpunit": "^9.3" 11 | }, 12 | "autoload": { 13 | "psr-4": {"QuickCheck\\": "src/QuickCheck/"} 14 | }, 15 | "autoload-dev": { 16 | "psr-4": {"QuickCheck\\": "test/QuickCheck/"} 17 | }, 18 | "suggest": { 19 | "ext-gmp": "*" 20 | }, 21 | "bin": [ 22 | "bin/quickcheck" 23 | ], 24 | "scripts": { 25 | "test": "vendor/bin/phpunit" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /dev-env/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.3.18-cli 2 | RUN apt-get update && apt-get install -y less git libgmp10-dev unzip 3 | RUN docker-php-ext-configure gmp && docker-php-ext-install -j$(nproc) gmp 4 | RUN pecl install xdebug && docker-php-ext-enable xdebug 5 | RUN curl https://getcomposer.org/download/1.9.1/composer.phar -s --output /usr/local/bin/composer \ 6 | && chmod +x /usr/local/bin/composer 7 | RUN cp /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini 8 | COPY toggle-ext /usr/local/bin/toggle-ext 9 | RUN chmod +x /usr/local/bin/toggle-ext && toggle-ext xdebug 10 | -------------------------------------------------------------------------------- /dev-env/toggle-ext: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | basedir="/usr/local/etc/php/conf.d" 4 | name="$1" 5 | path_name="$basedir/docker-php-ext-$name" 6 | 7 | if [ "$name" = "" ]; then 8 | echo "usage: $(basename $0) " 9 | exit 1 10 | fi 11 | 12 | if [ -f "$path_name.ini" ]; then 13 | mv "$path_name.ini" "$path_name.off" 14 | echo "$name is now disabled" 15 | exit 0 16 | fi 17 | 18 | if [ -f "$path_name.off" ]; then 19 | mv "$path_name.off" "$path_name.ini" 20 | echo "$name is now enabled" 21 | exit 0 22 | fi 23 | 24 | echo "$name has no config in $basedir" 25 | exit 1 26 | -------------------------------------------------------------------------------- /doc/annotations.md: -------------------------------------------------------------------------------- 1 | ## Automatic detection for Generators 2 | 3 | The `Annotation` class provide a helper to automatically use a generator based 4 | on the documented types for the function: 5 | 6 | ``` 7 | /** 8 | * @param string $s 9 | * @return bool 10 | */ 11 | function my_function($s) { 12 | return is_string($s); 13 | } 14 | 15 | Annotation::check('my_function'); 16 | ``` 17 | 18 | This will test `my_function` using the `Generator::strings()` generator. 19 | 20 | You can also register your own Generators using the 21 | `Annotation::register($type, $generator)` method. 22 | -------------------------------------------------------------------------------- /doc/cli-reference.md: -------------------------------------------------------------------------------- 1 | # CLI Reference 2 | 3 | ### Usage 4 | ``` 5 | quickcheck [ FILE | DIR ] [OPTIONS] 6 | ``` 7 | 8 | The first argument must be a file or directory. 9 | 10 | ### Examples 11 | 12 | ``` 13 | quickcheck test 14 | quickcheck test/example.php 15 | quickcheck test/example.php -t 1000 -s 123 16 | ``` 17 | 18 | ### Options 19 | 20 | ``` 21 | -t number of tests to run 22 | -x maximum size 23 | -s RNG seed 24 | ``` 25 | 26 | -------------------------------------------------------------------------------- /doc/cli-writing-tests.md: -------------------------------------------------------------------------------- 1 | # Writing CLI Tests 2 | 3 | You can write tests that can be run by the CLI by using the `QuickCheck\Test` functions. 4 | This class is only meant to be used to set up tests that will be run by the CLI. 5 | To run tests programmatically use the [`QuickCheck\Property`](./introduction.md) API. 6 | 7 | ## `Test::forAll` 8 | 9 | ```php 10 | static Test::forAll ( array $generators, callable $predicate [, int $numTests = 100 [, int $maxSize = 200 ]] ) : void 11 | ``` 12 | 13 | A single test per file can be defined with `QuickCheck\Test::forAll`. 14 | 15 | ```php 16 | use QuickCheck\Generator as Gen; 17 | use QuickCheck\Test; 18 | 19 | Test::forAll( 20 | [Gen::asciiStrings()], 21 | function($s) { 22 | return !is_numeric($s); 23 | }, 24 | 1000 25 | ); 26 | ``` 27 | 28 | ## `Test::check` 29 | 30 | ```php 31 | static Test::check ([ string $name = null ]) : Check 32 | ``` 33 | 34 | You can write multiple tests per file by using the `QuickCheck\Test::check` function. 35 | It returns a `QuickCheck\Check` instance. 36 | 37 | ### `QuickCheck\Check` 38 | 39 | The `Check` instance is a simple builder that allows you to fluently configure the test: 40 | 41 | ```php 42 | Check::times ( int $n ) : Check 43 | Check::maxSize ( int $n ) : Check 44 | Check::numTests ( int $n ) : Check 45 | Check::forAll ( array $generators, callable $predicate ) : void 46 | ``` 47 | 48 | Note that `Check::forAll` will register the test in the suite and thus returns `void`. 49 | 50 | ### Examples 51 | 52 | ```php 53 | use QuickCheck\Generator as Gen; 54 | use QuickCheck\Test; 55 | 56 | Test::check('is commutative') 57 | ->forAll( 58 | [Gen::ints(), Gen::ints()], 59 | function($a, $b) { 60 | return $a + $b === $b + $a; 61 | }); 62 | 63 | Test::check('has zero as identity') 64 | ->forAll( 65 | [Gen::ints()], 66 | function($x) { 67 | return $x + 0 === $x; 68 | }); 69 | 70 | Test::check('is associative') 71 | ->times(1000) 72 | ->maxSize(1337) 73 | ->forAll( 74 | [Gen::ints(), Gen::ints(), Gen::ints()], 75 | function($a, $b, $c) { 76 | return ($a + $b) + $c == $a + ($b + $c); 77 | }); 78 | 79 | ``` 80 | -------------------------------------------------------------------------------- /doc/generators.md: -------------------------------------------------------------------------------- 1 | # Generators 2 | 3 | ```php 4 | // integers 5 | Gen::ints()->takeSamples(); 6 | # => [0,1,-1,3,-2,0,3,3,-8,9] 7 | ``` 8 | 9 | ```php 10 | // ascii strings 11 | Gen::asciiStrings()->takeSamples(); 12 | # => ["","O","@","&5h","QSuC","","c[,","[Q"," ){#o{Z","\"!F=n"] 13 | ``` 14 | 15 | ```php 16 | // arrays 17 | Gen::ints()->intoArrays()->takeSamples(); 18 | # => [[],[],[-2],[0,0],[2],[4,-4,-1,2],[],[-5,-7,3,-2,2,0,-2], ...] 19 | ``` 20 | 21 | ```php 22 | // randomly choose between multiple generators 23 | Gen::oneOf(Gen::booleans(), Gen::ints())->takeSamples(); 24 | # => [false,true,false,false,2,true,true,3,3,2] 25 | ``` 26 | 27 | ```php 28 | // randomly choose element from array 29 | Gen::elements('foo', 'bar', 'baz')->takeSamples(); 30 | # => ["foo","foo","bar","bar","foo","foo","bar","baz","foo","baz"] 31 | ``` 32 | 33 | ```php 34 | // tuples of generators 35 | Gen::tuples(Gen::posInts(), Gen::alphaNumStrings())->takeSamples(); 36 | # => [[0,""],[1,""],[0,"86"],[3,"NPG"],[4,"1q"],[4,"6"],[1,"Eu60MN"],[1,"6q9D8wm"], ...] 37 | ``` 38 | 39 | ```php 40 | // notEmpty - generated value must not be empty 41 | Gen::arraysOf(Gen::ints())->notEmpty()->takeSamples(); 42 | # => [[0],[1],[1,1],[4,-1],[1],[0,0,1,1,-2],[-6,5,4,-2,-1],[3,7,-2],[-6,5],[-1,1,-4]] 43 | ``` 44 | 45 | ```php 46 | // suchThat - generated value must pass given predicate function 47 | // Note: Only do this if you can't generate the value deterministically. SuchThat may fail 48 | // if the generator doesn't return an acceptable value after 10 times. 49 | // (you can pass $maxTries as second argument if necessary). 50 | $oddInts = Gen::ints()->suchThat(function($i) { 51 | return $i % 2 != 0; 52 | }); 53 | $oddInts->takeSamples(); 54 | # => [1,1,-1,-3,5,5,-3,-7,1,7] 55 | ``` 56 | 57 | ```php 58 | // frequency - 1/3 of the time generate posInts, 2/3 of the time booleans 59 | Gen::frequency(1, Gen::posInts(), 2, Gen::booleans())->takeSamples(); 60 | # => [0,1,false,true,true,false,false,true,5,false] 61 | ``` 62 | 63 | ```php 64 | // map - transform the value generated 65 | Gen::posInts()->map(function($i) { return $i * 2; })->takeSamples(); 66 | # => [0,2,4,2,4,8,10,8,0,6] 67 | ``` 68 | 69 | ```php 70 | // use map to generate day times 71 | $daytimes = Gen::tuples(Gen::choose(0, 23), Gen::choose(0, 59)) 72 | ->map(function($daytime) { 73 | list($h, $m) = $daytime; 74 | return sprintf('%02d:%02d', $h, $m); 75 | }); 76 | 77 | $daytimes->takeSamples(); 78 | # => ["03:35","17:03","23:31","17:50","08:41","23:07","17:59","05:10","03:47","09:36"] 79 | ``` 80 | 81 | ```php 82 | // chain - create new generator based on generated value 83 | $chainSample = Gen::ints()->intoArrays()->notEmpty() 84 | ->chain(function($ints) { 85 | // choose one random element from the int array 86 | return Gen::elements($ints) 87 | // and return a tuple of the whole array and the chosen value 88 | ->map(function($i) use ($ints) { 89 | return [$ints, $i]; 90 | }); 91 | }); 92 | $chainSample->takeSamples(); 93 | # => [..., [[4,-2,5,3,5],4],[[-3,5,5,6,2],5],[[1,-3,1,5,2],-3], ...] 94 | ``` 95 | 96 | ```php 97 | // maps from string keys to daytimes 98 | $daytimes->mapsFrom(Gen::alphaStrings()->notEmpty())->notEmpty()->takeSamples(); 99 | # => [{"v":"08:36"},{"i":"03:43","E":"14:38"},{"gUU":"03:08","UIc":"19:24"}, ...] 100 | ``` 101 | 102 | ```php 103 | // maps from strings to booleans 104 | Gen::alphaStrings()->notEmpty()->mapsTo(Gen::booleans())->notEmpty()->takeSamples(); 105 | # => [{"J":true},{"pt":true,"TgQ":false},{"M":true,"jL":false},{"rm":true}, ...] 106 | ``` 107 | 108 | ```php 109 | // maps with fixed keys 110 | Gen::mapsWith([ 111 | 'age' => Gen::choose(18, 99), 112 | 'name' => Gen::tuples( 113 | Gen::elements('Ada', 'Grace', 'Hedy'), 114 | Gen::elements('Lovelace', 'Hopper', 'Lamarr') 115 | )->map(function($name) { 116 | return "$name[0] $name[1]"; 117 | }) 118 | ])->takeSamples(); 119 | # => [{"age":50,"name":"Ada Lamarr"},{"age":97,"name":"Ada Hopper"}, 120 | # {"age":81,"name":"Ada Lovelace"},{"age":55,"name":"Hedy Lamarr"}, ...] 121 | ``` 122 | 123 | ```php 124 | // recursive nested structures 125 | Gen::recursive( 126 | function (Gen $innerTypes) { 127 | return Gen::arraysOf($innerTypes); 128 | }, 129 | Gen::posInts() 130 | )->takeSamples(); 131 | # => "[[],[],[[[]]],[[[[2,3,1],[],[2,0,1]]],[[[3,1],[2,3,1],[]], 132 | # [[],[3,1],[]]],[]],[0,1,1,3],[1],[[[[],[[5,6,3,0,2],[2,4],[3,4,0,2,4,0]],..." 133 | ``` 134 | -------------------------------------------------------------------------------- /doc/introduction.md: -------------------------------------------------------------------------------- 1 | # API Introduction 2 | 3 | Property-based testing is all about defining properties that should hold true for all possible input 4 | and then trying to falsify them. 5 | In practice that means we need to define a predicate function and its possible inputs. 6 | Quickcheck will then run this predicate function for any number of randomly generated inputs. 7 | 8 | ## `QuickCheck\Generator` 9 | 10 | Inputs are defined using so-called generators (not to be confused with PHP generators). 11 | A generator is a function that produces random values of a certain type. 12 | In PHPQuickCheck the `QuickCheck\Generator` class is a wrapper around this function 13 | and provides a number of factory functions to create basic generators and some useful combinators 14 | to combine and transform generators. 15 | 16 | [Generator Examples](./generators.md) 17 | 18 | ## `QuickCheck\Property` 19 | 20 | Properties can be defined with the `Property::forAll` function and checked with `Property::check`: 21 | 22 | ```php 23 | static Property::forAll ( array $generators, callable $predicate [, int $maxSize = 200 ]) : Property 24 | static Property::check ( Property $prop, int $numTests [, int $seed = null ] ) : CheckResult 25 | ``` 26 | 27 | Here is a failing example: 28 | 29 | ```php 30 | $stringsAreNeverNumeric = Property::forAll( 31 | [Gen::asciiStrings()], 32 | function($str) { 33 | return !is_numeric($str); 34 | } 35 | ); 36 | 37 | $result = Property::check($stringsAreNeverNumeric, 10000); 38 | ``` 39 | 40 | `Property::check` returns a `QuickCheck\CheckResult` which has a convenience function to 41 | dump the result to stdout: 42 | 43 | ```php 44 | $result->dump('json_encode'); 45 | ``` 46 | 47 | This will produce something like the following output: 48 | 49 | ``` 50 | Failed after 834 tests 51 | Seed: 1578763578270 52 | Failing input: 53 | ["9E70"] 54 | Smallest failing input: 55 | ["0"] 56 | ``` 57 | 58 | This tells us that after 834 random tests the property was successfully falsified. 59 | The exact argument that caused the failure was "9E70" which then got shrunk to "0". 60 | So our minimal failing case is the string "0". 61 | 62 | Here's another example: 63 | 64 | ```php 65 | // predicate function that checks if the given 66 | // array elements are in ascending order 67 | function isAscending(array $xs) { 68 | $last = count($xs) - 1; 69 | for ($i = 0; $i < $last; ++$i) { 70 | if ($xs[$i] > $xs[$i + 1]) { 71 | return false; 72 | } 73 | } 74 | return true; 75 | } 76 | 77 | // sort function that is obviously broken 78 | function myBrokenSort(array $xs) { 79 | return $xs; 80 | } 81 | 82 | // so let's test our sort function, it should work on all possible int arrays 83 | $brokenSort = Property::forAll( 84 | [Gen::ints()->intoArrays()], 85 | function(array $xs) { 86 | return isAscending(myBrokenSort($xs)); 87 | } 88 | ); 89 | 90 | $result = Property::check($brokenSort, 100); 91 | $result->dump('json_encode'); 92 | ``` 93 | 94 | This will result in output similar to: 95 | 96 | ``` 97 | Failed after 7 tests 98 | Seed: 1578763516428 99 | Failing input: 100 | [[3,-5,-1,-1,-6]] 101 | Smallest failing input: 102 | [[0,-1]] 103 | ``` 104 | 105 | As you can see the list that failed was `[3,-5,-1,-1,-6]` which got shrunk to `[0,-1]`. For each run 106 | the exact failing values are different but it will always shrink down to `[1,0]` or `[0,-1]`. 107 | 108 | The result also contains the seed so you can run the exact same test by passing it as an argument 109 | to the check function: 110 | 111 | ```php 112 | Property::check($brokenSort, 100, 1411398418957) 113 | ->dump('json_encode'); 114 | ``` 115 | 116 | This always fails with the array `[-3,6,5,-5,1]` after 7 tests and shrinks to `[1,0]`. 117 | -------------------------------------------------------------------------------- /doc/phpunit.md: -------------------------------------------------------------------------------- 1 | ## PHPUnit 2 | 3 | To use PHPQuickCheck with PHPUnit, the assertion `\QuickCheck\PHPUnit\PropertyConstraint` is provided. 4 | It provides a static constructor method `PropertyConstraint::check`. 5 | Similar to `Property::check`, the method takes the size and allows also passing the seed if needed. 6 | 7 | ```php 8 | public function testStringsAreLessThanTenChars() 9 | { 10 | $property = Property::forAll( 11 | [Gen::strings()], 12 | fn ($s): bool => 10 > strlen($s) 13 | ); 14 | 15 | $this->assertThat($property, PropertyConstraint::check(50)); // will fail 16 | } 17 | ``` 18 | 19 | The assertion will delegate to `Property::check($size, $seed)`, and if the function returns anything but `true`, it will display a formatted failure description. 20 | 21 | ```txt 22 | Failed asserting that property is true. 23 | Tests runs: 16, failing size: 15, seed: 1578486446175, smallest shrunk value(s): 24 | array ( 25 | 0 => , 26 | ) 27 | ``` 28 | 29 | If an exception is thrown or a PHPUnit assertion fails, the message will be included in the output. 30 | 31 | To reproduce a test result the displayed seed can be passed via `PropertyConstraint::check($size, 1578486446175)`. 32 | -------------------------------------------------------------------------------- /examples/brokenSort.php: -------------------------------------------------------------------------------- 1 | $xs[$i + 1]) { 12 | return false; 13 | } 14 | } 15 | return true; 16 | } 17 | 18 | function myBrokenSort(array $xs): array { 19 | return $xs; 20 | } 21 | 22 | Test::forAll( 23 | [Gen::ints()->intoArrays()], 24 | function(array $xs): bool { 25 | return isAscending(myBrokenSort($xs)); 26 | } 27 | ); 28 | -------------------------------------------------------------------------------- /examples/intArrayElementsLessThan1000.php: -------------------------------------------------------------------------------- 1 | intoArrays()], 10 | function($xs): bool { 11 | $n = count(array_filter($xs, function($n): bool { 12 | return $n > 1000; 13 | })); 14 | return $n === 0; 15 | }); 16 | -------------------------------------------------------------------------------- /examples/integerAddition.php: -------------------------------------------------------------------------------- 1 | times(1000) 10 | ->maxSize(500) 11 | ->forAll( 12 | [Gen::ints(), Gen::ints()], 13 | function($a, $b): bool { 14 | return $a + $b === $b + $a; 15 | }); 16 | 17 | Test::check('has zero as identity') 18 | ->forAll( 19 | [Gen::ints()], 20 | function($x): bool { 21 | return $x + 0 === $x; 22 | }); 23 | 24 | Test::check('is associative') 25 | ->times(1000) 26 | ->maxSize(1337) 27 | ->forAll( 28 | [Gen::ints(), Gen::ints(), Gen::ints()], 29 | function($a, $b, $c): bool { 30 | return ($a + $b) + $c === $a + ($b + $c); 31 | }); 32 | -------------------------------------------------------------------------------- /examples/strings/lengthLessThan200.php: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | src/ 17 | 18 | 19 | test 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | test 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/QuickCheck/Annotation.php: -------------------------------------------------------------------------------- 1 | 'booleans', 24 | ); 25 | 26 | /** 27 | * Return the correct reflection class for the given callable. 28 | * 29 | * @param callable $f 30 | * @throws AnnotationException 31 | * @return \ReflectionFunction|\ReflectionMethod 32 | */ 33 | public static function getReflection(callable $f) 34 | { 35 | if (is_string($f)) { 36 | if (strpos($f, '::', 1) !== false) { 37 | return new \ReflectionMethod($f); 38 | } else { 39 | return new \ReflectionFunction($f); 40 | } 41 | } elseif (is_array($f) && count($f) == 2) { 42 | return new \ReflectionMethod($f[0], $f[1]); 43 | } elseif ($f instanceof \Closure) { 44 | return new \ReflectionFunction($f); 45 | } elseif (is_object($f) && method_exists($f, '__invoke')) { 46 | return new \ReflectionMethod($f, '__invoke'); 47 | } 48 | // if the tests above are exhaustive, we should never hit the next line. 49 | throw new AnnotationException("Unable to determine callable type."); 50 | } 51 | 52 | /** 53 | * Return the types for the given callable. 54 | * 55 | * @param callable $f 56 | * @throws AnnotationException 57 | * @return array 58 | */ 59 | public static function types(callable $f) 60 | { 61 | $ref = self::getReflection($f); 62 | 63 | $docs = $ref->getDocComment(); 64 | $proto = $ref->getParameters(); 65 | 66 | preg_match_all('/@param\s+(?P.*?)\s+\$(?P.*?)\s+/', $docs, $docs, PREG_SET_ORDER); 67 | 68 | $params = array(); 69 | foreach ($proto as $p) { 70 | $name = $p->getName(); 71 | $type = null; 72 | foreach ($docs as $k => $d) { 73 | if ($d['name'] === $name) { 74 | $type = $d['type']; 75 | unset($docs[$k]); 76 | break; 77 | } 78 | } 79 | if (is_null($type)) { 80 | throw new MissingTypeAnnotationException("Cannot determine type for $name."); 81 | } 82 | if (count(explode('|', $type)) > 1) { 83 | throw new AmbiguousTypeAnnotationException("Ambiguous type for $name : $type"); 84 | } 85 | $params[$name] = $type; 86 | } 87 | 88 | return $params; 89 | } 90 | 91 | /** 92 | * Associate a generator to a given type. 93 | * 94 | * @param string $type 95 | * @param Generator $generator Tĥe generator associated with the type 96 | * @throws DuplicateGeneratorException 97 | */ 98 | public static function register($type, Generator $generator) 99 | { 100 | if (array_key_exists($type, self::$generators)) { 101 | throw new DuplicateGeneratorException("A generator is already registred for $type."); 102 | } 103 | 104 | self::$generators[$type] = $generator; 105 | } 106 | 107 | /** 108 | * Determine the generators needed to test the function $f and then 109 | * use the predicate $p to assert correctness. 110 | * 111 | * If $p is omitted, will simply check that $f returns true for 112 | * each generated values. 113 | * 114 | * @param callable $f The function to test 115 | * @param callable $p The predicate 116 | * @param int $n number of iteration 117 | * @throws NoGeneratorAnnotationException 118 | * @return CheckResult 119 | */ 120 | public static function check(callable $f, callable $p = null, $n = 10) 121 | { 122 | if (is_null($p)) { 123 | $p = function ($result) { 124 | return $result === true; 125 | 126 | }; 127 | } 128 | 129 | $types = self::types($f); 130 | 131 | $args = array(); 132 | foreach ($types as $t) { 133 | $array = false; 134 | if (substr($t, -2) == '[]') { 135 | $t = substr($t, 0, -2); 136 | $array = true; 137 | } 138 | 139 | if (array_key_exists($t, self::$generators)) { 140 | $generator = self::$generators[$t]; 141 | } elseif (method_exists('QuickCheck\Generator', $t.'s')) { 142 | $generator = $t.'s'; 143 | } else { 144 | throw new NoGeneratorAnnotationException("Unable to find a generator for $t"); 145 | } 146 | 147 | if (! $generator instanceof Generator) { 148 | $generator = call_user_func(array('QuickCheck\Generator', $generator)); 149 | } 150 | 151 | if ($array) { 152 | $generator = $generator->intoArrays(); 153 | } 154 | $args[] = $generator; 155 | } 156 | 157 | $check = function () use ($f, $p) { 158 | $result = call_user_func_array($f, func_get_args()); 159 | return $p($result); 160 | }; 161 | 162 | return Property::check(Property::forAll($args, $check), $n); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/QuickCheck/Arrays.php: -------------------------------------------------------------------------------- 1 | name = $name; 15 | $this->suite = $suite; 16 | } 17 | 18 | function forAll(array $args, callable $pred) { 19 | $this->property = Property::forAll($args, $pred, $this->maxSize); 20 | $this->suite->register($this); 21 | } 22 | 23 | function times(int $numTests) { 24 | $this->numTests = $numTests; 25 | return $this; 26 | } 27 | 28 | function maxSize(int $maxSize) { 29 | $this->maxSize = $maxSize; 30 | return $this; 31 | } 32 | 33 | function numTests() { 34 | return $this->numTests; 35 | } 36 | 37 | function name() { 38 | return $this->name; 39 | } 40 | 41 | function property(): Property { 42 | return $this->property; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/QuickCheck/CheckResult.php: -------------------------------------------------------------------------------- 1 | file = $file; 12 | } 13 | 14 | function register(Check $check) { 15 | $this->checks[] = $check; 16 | } 17 | 18 | function empty() { 19 | return empty($this->checks); 20 | } 21 | 22 | /** 23 | * @return Check[] 24 | */ 25 | function checks() { 26 | return $this->checks; 27 | } 28 | 29 | function file() { 30 | return $this->file; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/QuickCheck/Exceptions/AmbiguousTypeAnnotationException.php: -------------------------------------------------------------------------------- 1 | numTests = $numTests; 14 | $this->seed = $seed; 15 | $this->failed = $failed; 16 | $this->shrunk = $shrunk; 17 | } 18 | 19 | public function numTests(): int 20 | { 21 | return $this->numTests; 22 | } 23 | 24 | public function seed(): int 25 | { 26 | return $this->seed; 27 | } 28 | 29 | public function shrunk(): ShrinkResult 30 | { 31 | return $this->shrunk; 32 | } 33 | 34 | function test(): PropertyTest { 35 | return $this->failed; 36 | } 37 | 38 | function isSuccess(): bool { 39 | return false; 40 | } 41 | 42 | function isFailure(): bool { 43 | return true; 44 | } 45 | 46 | function dump(callable $encode): void 47 | { 48 | echo 'Failed after ', $this->numTests(), ' tests', PHP_EOL; 49 | echo 'Seed: ', $this->seed(), PHP_EOL; 50 | echo 'Failing input:', PHP_EOL; 51 | echo call_user_func($encode, $this->test()->arguments()), PHP_EOL; 52 | echo 'Smallest failing input:', PHP_EOL; 53 | echo call_user_func($encode, $this->shrunk()->test()->arguments()), PHP_EOL; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/QuickCheck/Generator.php: -------------------------------------------------------------------------------- 1 | gen = $gen; 18 | } 19 | 20 | /** 21 | * invokes the generator with the given RNG and size 22 | * 23 | * @param Random $rng 24 | * @param int $size 25 | * @return ShrinkTreeNode 26 | */ 27 | public function call(Random $rng, $size) 28 | { 29 | return call_user_func($this->gen, $rng, $size); 30 | } 31 | 32 | public static function pure($value) 33 | { 34 | return new self(function ($rng, $size) use ($value) { 35 | return $value; 36 | }); 37 | } 38 | 39 | public function fmap(callable $k) 40 | { 41 | return new self(function ($rng, $size) use ($k) { 42 | return call_user_func($k, call_user_func($this->gen, $rng, $size)); 43 | }); 44 | } 45 | 46 | /** 47 | * Creates a new generator by mapping $k over the value of this generator 48 | * 49 | * @param callable $k must return a new Generator 50 | * @return Generator 51 | */ 52 | public function flatMap(callable $k) 53 | { 54 | return new self(function ($rng, $size) use ($k) { 55 | return call_user_func($k, $this->call($rng, $size))->call($rng, $size); 56 | }); 57 | } 58 | 59 | /** 60 | * turns a list of generators into a generator of a list 61 | * 62 | * @param Generator[]|\Iterator $xs 63 | * @return Generator 64 | */ 65 | private static function sequence($xs) 66 | { 67 | return Lazy::reduce( 68 | function (Generator $acc, Generator $elem) { 69 | return $acc->flatMap(function ($xs) use ($elem) { 70 | return $elem->flatMap(function ($y) use ($xs) { 71 | return self::pure(Arrays::append($xs, $y)); 72 | }); 73 | }); 74 | }, 75 | $xs, 76 | self::pure([]) 77 | ); 78 | } 79 | 80 | /** 81 | * maps function f over the values produced by this generator 82 | * 83 | * @param callable $f 84 | * @return Generator 85 | */ 86 | public function map(callable $f) 87 | { 88 | return $this->fmap(function (ShrinkTreeNode $node) use ($f) { 89 | return $node->map($f); 90 | }); 91 | } 92 | 93 | /** 94 | * creates a new generator that passes the result of this generator 95 | * to the callable $k which should return a new generator. 96 | * 97 | * @param callable $k 98 | * @return Generator 99 | */ 100 | public function chain(callable $k) 101 | { 102 | return $this->flatMap( 103 | function (ShrinkTreeNode $rose) use ($k) { 104 | $gen = new self(function ($rng, $size) use ($rose, $k) { 105 | return $rose->map($k)->map( 106 | function (self $g) use ($rng, $size) { 107 | return $g->call($rng, $size); 108 | } 109 | ); 110 | }); 111 | return $gen->fmap(function (ShrinkTreeNode $node) { 112 | return $node->join(); 113 | }); 114 | } 115 | ); 116 | } 117 | 118 | /** 119 | * creates a generator that always returns $value and never shrinks 120 | * 121 | * @param mixed $value 122 | * @return Generator 123 | */ 124 | public static function unit($value) 125 | { 126 | return self::pure(ShrinkTreeNode::pure($value)); 127 | } 128 | 129 | // helpers 130 | // -------------------------------------------------- 131 | 132 | public static function sizes($maxSize) 133 | { 134 | return Lazy::cycle(function () use ($maxSize) { 135 | return Lazy::range(0, $maxSize); 136 | }); 137 | } 138 | 139 | /** 140 | * returns an infinite sequence of random samples from this generator bounded by $maxSize 141 | * 142 | * @param int $maxSize 143 | * @return \Generator 144 | */ 145 | public function samples($maxSize = 100) 146 | { 147 | $rng = new Random(); 148 | return Lazy::map( 149 | function ($size) use ($rng) { 150 | return $this->call($rng, $size)->getValue(); 151 | }, 152 | self::sizes($maxSize) 153 | ); 154 | } 155 | 156 | /** 157 | * returns an array of $num random samples from this generator 158 | * 159 | * @param int $num 160 | * @return array 161 | */ 162 | public function takeSamples($num = 10) 163 | { 164 | return Lazy::realize(Lazy::take($num, $this->samples())); 165 | } 166 | 167 | 168 | // internal helpers 169 | // -------------------------------------------------- 170 | 171 | private static function halfs($n) 172 | { 173 | while (0 != $n) { 174 | yield $n; 175 | $n = intval($n / 2); 176 | } 177 | } 178 | 179 | private static function shrinkInt($i) 180 | { 181 | return Lazy::map( 182 | function ($val) use ($i) { 183 | return $i - $val; 184 | }, 185 | self::halfs($i) 186 | ); 187 | } 188 | 189 | private static function intRoseTree($val) 190 | { 191 | return new ShrinkTreeNode($val, function () use ($val) { 192 | return Lazy::map( 193 | function ($x) { 194 | return self::intRoseTree($x); 195 | }, 196 | self::shrinkInt($val) 197 | ); 198 | }); 199 | } 200 | 201 | private static function sized(callable $sizedGen) 202 | { 203 | return new self(function ($rng, $size) use ($sizedGen) { 204 | $gen = call_user_func($sizedGen, $size); 205 | return $gen->call($rng, $size); 206 | }); 207 | } 208 | 209 | private static function randRange(Random $rng, $lower, $upper) 210 | { 211 | if ($lower > $upper) { 212 | throw new \InvalidArgumentException(); 213 | } 214 | $factor = $rng->nextDouble(); 215 | return (int)floor($lower + ($factor * (1.0 + $upper) - $factor * $lower)); 216 | } 217 | 218 | private static function getArgs(array $args) 219 | { 220 | if (count($args) == 1 && is_array($args[0])) { 221 | return $args[0]; 222 | } 223 | return $args; 224 | } 225 | 226 | // combinators and helpers 227 | // -------------------------------------------------- 228 | 229 | /** 230 | * creates a generator that returns numbers in the range $lower to $upper, inclusive 231 | * 232 | * @param int $lower 233 | * @param int $upper 234 | * @return Generator 235 | */ 236 | public static function choose($lower, $upper) 237 | { 238 | return new self(function (Random $rng, $size) use ($lower, $upper) { 239 | $val = self::randRange($rng, $lower, $upper); 240 | $tree = self::intRoseTree($val); 241 | return $tree->filter(function ($x) use ($lower, $upper) { 242 | return $x >= $lower && $x <= $upper; 243 | }); 244 | }); 245 | } 246 | 247 | /** 248 | * creates a new generator based on this generator with size always bound to $n 249 | * 250 | * @param int $n 251 | * @return Generator 252 | */ 253 | public function resize($n) 254 | { 255 | return new self(function ($rng, $size) use ($n) { 256 | return $this->call($rng, $n); 257 | }); 258 | } 259 | 260 | /** 261 | * creates a new generator that returns an array whose elements are chosen 262 | * from the list of given generators. Individual elements shrink according to 263 | * their generator but the array will never shrink in count. 264 | * 265 | * Accepts either a variadic number of args or a single array of generators. 266 | * 267 | * Example: 268 | * Gen::tuples(Gen::booleans(), Gen::ints()) 269 | * Gen::tuples([Gen::booleans(), Gen::ints()]) 270 | * 271 | * @return Generator 272 | */ 273 | public static function tuples() 274 | { 275 | $seq = self::sequence(self::getArgs(func_get_args())); 276 | return $seq->flatMap(function ($roses) { 277 | return self::pure(ShrinkTreeNode::zip( 278 | function (...$xs) { 279 | return $xs; 280 | }, 281 | $roses 282 | )); 283 | }); 284 | } 285 | 286 | /** 287 | * creates a generator that returns a positive or negative integer bounded 288 | * by the generators size parameter. 289 | * 290 | * @return Generator 291 | */ 292 | public static function ints() 293 | { 294 | return self::sized(function ($size) { 295 | return self::choose(-$size, $size); 296 | }); 297 | } 298 | 299 | /** 300 | * creates a generator that produces arrays whose elements 301 | * are chosen from $gen. 302 | * 303 | * If no $min or $max are specified the array size will be bounded by the 304 | * generators maxSize argument and shrink to the empty array. 305 | * 306 | * If $min and $max is specified the array size will be bounded by $min and $max (inclusive) 307 | * and will not shrink below $min. 308 | * 309 | * If $min is specified without $max the generated arrays will always be of size $min 310 | * and don't shrink in size at all (equivalent to tuple). 311 | * 312 | * @param Generator $gen 313 | * @param int $min 314 | * @param int $max 315 | * @return Generator 316 | */ 317 | public static function arraysOf(self $gen, int $min = null, int $max = null) 318 | { 319 | if ($min !== null && $max === null) { 320 | return self::tuples(Lazy::realize(Lazy::repeat($min, $gen))); 321 | } else if ($min !== null && $max !== null) { 322 | return self::choose($min, $max)->flatMap(function(ShrinkTreeNode $num) use ($gen, $min, $max) { 323 | $seq = self::sequence(Lazy::repeat($num->getValue(), $gen)); 324 | return $seq->flatMap(function($roses) use ($min, $max) { 325 | return self::pure(ShrinkTreeNode::shrink(function (...$xs) { 326 | return $xs; 327 | }, $roses))->flatMap(function(ShrinkTreeNode $rose) use ($min, $max) { 328 | return self::pure($rose->filter(function($x) use ($min, $max) { 329 | return count($x) >= $min && count($x) <= $max; 330 | })); 331 | }); 332 | }); 333 | }); 334 | } else { 335 | $sized = self::sized(function ($s) { 336 | return self::choose(0, $s); 337 | }); 338 | return $sized->flatMap(function (ShrinkTreeNode $numRose) use ($gen) { 339 | $seq = self::sequence(Lazy::repeat($numRose->getValue(), $gen)); 340 | return $seq->flatMap(function ($roses) { 341 | return self::pure(ShrinkTreeNode::shrink(function (...$xs) { 342 | return $xs; 343 | }, $roses)); 344 | }); 345 | }); 346 | } 347 | } 348 | 349 | /** 350 | * creates a generator that produces arrays whose elements 351 | * are chosen from this generator. 352 | * 353 | * @return Generator 354 | */ 355 | public function intoArrays(int $min = null, int $max = null) 356 | { 357 | return self::arraysOf($this, $min, $max); 358 | } 359 | 360 | /** 361 | * creates a generator that produces characters from 0-255 362 | * 363 | * @return Generator 364 | */ 365 | public static function chars() 366 | { 367 | return self::choose(0, 255)->map('chr'); 368 | } 369 | 370 | /** 371 | * creates a generator that produces only ASCII characters 372 | * 373 | * @return Generator 374 | */ 375 | public static function asciiChars() 376 | { 377 | return self::choose(32, 126)->map('chr'); 378 | } 379 | 380 | /** 381 | * creates a generator that produces alphanumeric characters 382 | * 383 | * @return Generator 384 | */ 385 | public static function alphaNumChars() 386 | { 387 | return self::oneOf( 388 | self::choose(48, 57), 389 | self::choose(65, 90), 390 | self::choose(97, 122) 391 | )->map('chr'); 392 | } 393 | 394 | /** 395 | * creates a generator that produces only alphabetic characters 396 | * 397 | * @return Generator 398 | */ 399 | public static function alphaChars() 400 | { 401 | return self::oneOf( 402 | self::choose(65, 90), 403 | self::choose(97, 122) 404 | )->map('chr'); 405 | } 406 | 407 | public function dontShrink() 408 | { 409 | return $this->flatMap(function (ShrinkTreeNode $x) { 410 | return self::unit($x->getRoot()); 411 | }); 412 | } 413 | 414 | public function toStrings() 415 | { 416 | return $this->map(function ($x) { 417 | return is_array($x) ? implode('', $x) : (string)$x; 418 | }); 419 | } 420 | 421 | /** 422 | * creates a generator that produces strings; may contain unprintable chars 423 | * 424 | * @return Generator 425 | */ 426 | public static function strings() 427 | { 428 | return self::chars()->intoArrays()->toStrings(); 429 | } 430 | 431 | /** 432 | * creates a generator that produces ASCII strings 433 | * 434 | * @return Generator 435 | */ 436 | public static function asciiStrings() 437 | { 438 | return self::asciiChars()->intoArrays()->toStrings(); 439 | } 440 | 441 | /** 442 | * creates a generator that produces alphanumeric strings 443 | * 444 | * @return Generator 445 | */ 446 | public static function alphaNumStrings() 447 | { 448 | return self::alphaNumChars()->intoArrays()->toStrings(); 449 | } 450 | 451 | /** 452 | * creates a generator that produces alphabetic strings 453 | * 454 | * @return Generator 455 | */ 456 | public static function alphaStrings() 457 | { 458 | return self::alphaChars()->intoArrays()->toStrings(); 459 | } 460 | 461 | /** 462 | * creates a generator that produces arrays with keys chosen from $keygen 463 | * and values chosen from $valgen. 464 | * 465 | * @param Generator $keygen 466 | * @param Generator $valgen 467 | * 468 | * @return Generator 469 | */ 470 | public static function maps(self $keygen, self $valgen) 471 | { 472 | return self::tuples($keygen, $valgen) 473 | ->intoArrays() 474 | ->map(function ($tuples) { 475 | $map = []; 476 | foreach ($tuples as $tuple) { 477 | [$key, $val] = $tuple; 478 | $map[$key] = $val; 479 | } 480 | return $map; 481 | }); 482 | } 483 | 484 | /** 485 | * creates a generator that produces arrays with keys chosen from 486 | * this generator and values chosen from $valgen. 487 | * 488 | * @param Generator $valgen 489 | * @return Generator 490 | */ 491 | public function mapsTo(self $valgen) 492 | { 493 | return self::maps($this, $valgen); 494 | } 495 | 496 | /** 497 | * creates a generator that produces arrays with keys chosen from 498 | * $keygen and values chosen from this generator. 499 | * 500 | * @param Generator $keygen 501 | * @return Generator 502 | */ 503 | public function mapsFrom(self $keygen) 504 | { 505 | return self::maps($keygen, $this); 506 | } 507 | 508 | /** 509 | * creates a generator that produces arrays with the same keys as in $map 510 | * where each corresponding value is chosen from a specified generator. 511 | * 512 | * Example: 513 | * Gen::mapsWith( 514 | * 'foo' => Gen::booleans(), 515 | * 'bar' => Gen::ints() 516 | * ) 517 | * 518 | * @param array $map 519 | * @return Generator 520 | */ 521 | public static function mapsWith(array $map) 522 | { 523 | return self::tuples($map)->map(function ($vals) use ($map) { 524 | return array_combine(array_keys($map), $vals); 525 | }); 526 | } 527 | 528 | /** 529 | * creates a new generator that randomly chooses a value from the list 530 | * of provided generators. Shrinks toward earlier generators as well as shrinking 531 | * the generated value itself. 532 | * 533 | * Accepts either a variadic number of args or a single array of generators. 534 | * 535 | * Example: 536 | * Gen::oneOf(Gen::booleans(), Gen::ints()) 537 | * Gen::oneOf([Gen::booleans(), Gen::ints()]) 538 | * 539 | * @return Generator 540 | */ 541 | public static function oneOf() 542 | { 543 | $generators = self::getArgs(func_get_args()); 544 | $num = count($generators); 545 | if ($num < 2) { 546 | throw new \InvalidArgumentException(); 547 | } 548 | return self::choose(0, $num - 1) 549 | ->chain(function ($index) use ($generators) { 550 | return $generators[$index]; 551 | }); 552 | } 553 | 554 | /** 555 | * creates a generator that randomly chooses from the specified values 556 | * 557 | * Accepts either a variadic number of args or a single array of values. 558 | * 559 | * Example: 560 | * Gen::elements('foo', 'bar', 'baz') 561 | * Gen::elements(['foo', 'bar', 'baz']) 562 | * 563 | * @return Generator 564 | */ 565 | public static function elements() 566 | { 567 | $coll = self::getArgs(func_get_args()); 568 | if (empty($coll)) { 569 | throw new \InvalidArgumentException(); 570 | } 571 | return self::choose(0, count($coll) - 1) 572 | ->flatMap(function (ShrinkTreeNode $rose) use ($coll) { 573 | return self::pure($rose->map( 574 | function ($index) use ($coll) { 575 | return $coll[$index]; 576 | } 577 | )); 578 | }); 579 | } 580 | 581 | /** 582 | * creates a new generator that generates values from this generator such that they 583 | * satisfy callable $pred. 584 | * At most $maxTries attempts will be made to generate a value that satisfies the 585 | * predicate. At every retry the size parameter will be increased. In case of failure 586 | * an exception will be thrown. 587 | * 588 | * @param callable $pred 589 | * @param int $maxTries 590 | * @return Generator 591 | * @throws \RuntimeException 592 | */ 593 | public function suchThat(callable $pred, $maxTries = 10) 594 | { 595 | return new self(function ($rng, $size) use ($pred, $maxTries) { 596 | for ($i = 0; $i < $maxTries; ++$i) { 597 | $value = $this->call($rng, $size); 598 | if (call_user_func($pred, $value->getValue())) { 599 | return $value->filter($pred); 600 | } 601 | $size++; 602 | } 603 | throw new \RuntimeException( 604 | "couldn't satisfy such-that predicate after $maxTries tries." 605 | ); 606 | }); 607 | } 608 | 609 | public function notEmpty($maxTries = 10) 610 | { 611 | return $this->suchThat(function ($x) { 612 | return !empty($x); 613 | }, $maxTries); 614 | } 615 | 616 | /** 617 | * creates a generator that produces true or false. Shrinks to false. 618 | * 619 | * @return Generator 620 | */ 621 | public static function booleans() 622 | { 623 | return self::elements(false, true); 624 | } 625 | 626 | /** 627 | * creates a generator that produces positive integers bounded by 628 | * the generators size parameter. 629 | * 630 | * @return Generator 631 | */ 632 | public static function posInts() 633 | { 634 | return self::ints()->map(function ($x) { 635 | return abs($x); 636 | }); 637 | } 638 | 639 | /** 640 | * creates a generator that produces negative integers bounded by 641 | * the generators size parameter. 642 | * 643 | * @return Generator 644 | */ 645 | public static function negInts() 646 | { 647 | return self::ints()->map(function ($x) { 648 | return -abs($x); 649 | }); 650 | } 651 | 652 | /** 653 | * creates a generator that produces strictly positive integers bounded by 654 | * the generators size parameter. 655 | * 656 | * @return Generator 657 | */ 658 | public static function strictlyPosInts() 659 | { 660 | return self::ints()->map(function ($x) { 661 | return abs($x)+1; 662 | }); 663 | } 664 | 665 | /** 666 | * creates a generator that produces strictly negative integers bounded by 667 | * the generators size parameter. 668 | * 669 | * @return Generator 670 | */ 671 | public static function strictlyNegInts() 672 | { 673 | return self::ints()->map(function ($x) { 674 | return -abs($x)-1; 675 | }); 676 | } 677 | 678 | /** 679 | * creates a generator that produces values from specified generators based on 680 | * likelihoods. The likelihood of a generator being chosen is its likelihood divided 681 | * by the sum of all likelihoods. 682 | * 683 | * Example: 684 | * Gen::frequency( 685 | * 5, Gen::ints(), 686 | * 3, Gen::booleans(), 687 | * 2, Gen::alphaStrings() 688 | * ) 689 | * 690 | * @return Generator 691 | */ 692 | public static function frequency() 693 | { 694 | $args = func_get_args(); 695 | $argc = count($args); 696 | if ($argc < 2 || $argc % 2 != 0) { 697 | throw new \InvalidArgumentException(); 698 | } 699 | $total = array_sum(Lazy::realize(Lazy::takeNth(2, $args))); 700 | $pairs = Lazy::realize(Lazy::partition(2, $args)); 701 | return self::choose(1, $total)->flatMap( 702 | function (ShrinkTreeNode $rose) use ($pairs) { 703 | $n = $rose->getValue(); 704 | foreach ($pairs as $pair) { 705 | [$chance, $gen] = $pair; 706 | if ($n <= $chance) { 707 | return $gen; 708 | } 709 | $n = $n - $chance; 710 | } 711 | } 712 | ); 713 | } 714 | 715 | public static function simpleTypes() 716 | { 717 | return self::oneOf( 718 | self::ints(), 719 | self::chars(), 720 | self::strings(), 721 | self::booleans() 722 | ); 723 | } 724 | 725 | public static function simplePrintableTypes() 726 | { 727 | return self::oneOf( 728 | self::ints(), 729 | self::asciiChars(), 730 | self::asciiStrings(), 731 | self::booleans() 732 | ); 733 | } 734 | 735 | public static function containerTypes(self $innerType) 736 | { 737 | return self::oneOf( 738 | $innerType->intoArrays(), 739 | $innerType->mapsFrom(self::oneOf(self::ints(), self::strings())) 740 | ); 741 | } 742 | 743 | private static function recursiveHelper($container, $scalar, $scalarSize, $childrenSize, $height) 744 | { 745 | if ($height == 0) { 746 | return $scalar->resize($scalarSize); 747 | } else { 748 | return call_user_func( 749 | $container, 750 | self::recursiveHelper( 751 | $container, 752 | $scalar, 753 | $scalarSize, 754 | $childrenSize, 755 | $height - 1 756 | ) 757 | ); 758 | } 759 | } 760 | 761 | public static function recursive(callable $container, self $scalar) 762 | { 763 | return self::sized(function ($size) use ($container, $scalar) { 764 | return self::choose(1, 5)->chain( 765 | function ($height) use ($container, $scalar, $size) { 766 | $childrenSize = pow($size, 1 / $height); 767 | return self::recursiveHelper( 768 | $container, 769 | $scalar, 770 | $size, 771 | $childrenSize, 772 | $height 773 | ); 774 | } 775 | ); 776 | }); 777 | } 778 | 779 | public static function any() 780 | { 781 | return self::recursive( 782 | [__CLASS__, 'containerTypes'], 783 | self::simpleTypes() 784 | ); 785 | } 786 | 787 | public static function anyPrintable() 788 | { 789 | return self::recursive( 790 | [__CLASS__, 'containerTypes'], 791 | self::simplePrintableTypes() 792 | ); 793 | } 794 | } 795 | -------------------------------------------------------------------------------- /src/QuickCheck/Lazy.php: -------------------------------------------------------------------------------- 1 | rewind(); 30 | if (!$xs->valid()) { 31 | return $initial; 32 | } 33 | $acc = $initial; 34 | if ($acc === null) { 35 | $acc = $xs->current(); 36 | $xs->next(); 37 | } 38 | for (; $xs->valid(); $xs->next()) { 39 | $acc = call_user_func($f, $acc, $xs->current()); 40 | } 41 | return $acc; 42 | } 43 | throw new \InvalidArgumentException(); 44 | } 45 | 46 | /** 47 | * Maps a function over an iterable collection in a lazy way 48 | * using generators 49 | * 50 | * @param callable $f 51 | * @param \Iterable $coll 52 | * @return \Generator 53 | */ 54 | public static function map(callable $f, iterable $coll) 55 | { 56 | foreach ($coll as $x) { 57 | yield call_user_func($f, $x); 58 | } 59 | } 60 | 61 | /** 62 | * Filter the given iterable collection using the callback in a 63 | * lazy way. 64 | * 65 | * @param callable $f 66 | * @param $coll 67 | * @return \Generator 68 | */ 69 | public static function filter(callable $f, iterable $coll) 70 | { 71 | foreach ($coll as $x) { 72 | if (call_user_func($f, $x)) { 73 | yield $x; 74 | } 75 | } 76 | } 77 | 78 | /** 79 | * Transform the given iterable collection to a "real" array. 80 | * 81 | * @param array|\Iterator $it 82 | * @return array 83 | */ 84 | public static function realize($it) 85 | { 86 | if ($it instanceof \Iterator) { 87 | return iterator_to_array($it); 88 | } 89 | return $it; 90 | } 91 | 92 | /** 93 | * Iterate infinitely on the result of the given callable. 94 | * 95 | * @param callable $f 96 | * @return \Generator 97 | */ 98 | public static function cycle(callable $f) 99 | { 100 | while (true) { 101 | foreach ($f() as $x) { 102 | yield $x; 103 | } 104 | } 105 | } 106 | 107 | /** 108 | * Produce a range starting at $min and finishing with $max. 109 | * If $max is negative, the range will be infinite. 110 | * 111 | * @param int $min 112 | * @param int $max 113 | * @return \Generator 114 | */ 115 | public static function range($min = 0, $max = -1) 116 | { 117 | for ($i = $min; $max < 0 || $i < $max; ++$i) { 118 | yield $i; 119 | } 120 | } 121 | 122 | /** 123 | * Return at most $n elements from the given iterable collection in 124 | * a lazy fashion. 125 | * 126 | * @param int $n 127 | * @param \Iterator $it 128 | * @return \Generator 129 | */ 130 | public static function take($n, \Iterator $it) 131 | { 132 | for ($i = 0, $it->rewind(); $i < $n && $it->valid(); ++$i, $it->next()) { 133 | yield $it->current(); 134 | } 135 | } 136 | 137 | /** 138 | * Lazily iterate over all iterable collection given as parameters 139 | * in order. 140 | * 141 | * @return \Generator 142 | */ 143 | public static function concat() 144 | { 145 | foreach (func_get_args() as $xs) { 146 | foreach ($xs as $x) { 147 | yield $x; 148 | } 149 | } 150 | } 151 | 152 | /** 153 | * repeat the value n times in a lazy fashion. 154 | * 155 | * @param int $n 156 | * @param mixed $val 157 | * @return \Generator 158 | */ 159 | public static function repeat($n, $val) 160 | { 161 | for ($i = 0; $i < $n; ++$i) { 162 | yield $val; 163 | } 164 | } 165 | 166 | /** 167 | * Return an iterator for the given collection if 168 | * possible. 169 | * 170 | * @param $xs 171 | * @return \Iterator 172 | * @throws \InvalidArgumentException 173 | */ 174 | public static function iterator($xs) 175 | { 176 | if (is_array($xs)) { 177 | return new \ArrayIterator($xs); 178 | } 179 | if (!$xs instanceof \Iterator) { 180 | throw new \InvalidArgumentException(); 181 | } 182 | return $xs; 183 | } 184 | 185 | /** 186 | * Return every Nth elements in the given collection in a lazy 187 | * fashion. 188 | * 189 | * @param int $n 190 | * @param $coll 191 | * @return \Generator 192 | */ 193 | public static function takeNth($n, $coll) 194 | { 195 | $coll = self::iterator($coll); 196 | for ($coll->rewind(); $coll->valid(); $coll->valid() && $coll->next()) { 197 | yield $coll->current(); 198 | for ($i = 0; $i < $n - 1 && $coll->valid(); ++$i) { 199 | $coll->next(); 200 | } 201 | } 202 | } 203 | 204 | /** 205 | * Return the given collection partitioned in n sized segments 206 | * in a lazy fashion. 207 | * 208 | * @param int $n 209 | * @param $coll 210 | * @return \Generator 211 | */ 212 | public static function partition($n, $coll) 213 | { 214 | $coll = self::iterator($coll); 215 | for ($coll->rewind(); $coll->valid();) { 216 | $partition = []; 217 | for ($i = 0; $i < $n && $coll->valid(); ++$i, $coll->next()) { 218 | $partition[] = $coll->current(); 219 | } 220 | yield $partition; 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/QuickCheck/PHPUnit/PropertyConstraint.php: -------------------------------------------------------------------------------- 1 | numTests = $n; 26 | $this->seed = $seed; 27 | } 28 | 29 | public static function check($n = 100, int $seed = null): self 30 | { 31 | return new self($n, $seed); 32 | } 33 | 34 | public function withExceptionStacktrace(): self 35 | { 36 | $this->showExceptionStacktrace = true; 37 | return $this; 38 | } 39 | 40 | public function evaluate($prop, string $description = '', bool $returnResult = false): ?bool 41 | { 42 | $result = Property::check($prop, $this->numTests, $this->seed); 43 | return parent::evaluate($result, $description, $returnResult); 44 | } 45 | 46 | protected function matches($other): bool 47 | { 48 | return $other->isSuccess(); 49 | } 50 | 51 | public function toString(): string 52 | { 53 | return 'property is true'; 54 | } 55 | 56 | protected function failureDescription($other): string 57 | { 58 | return $this->toString(); 59 | } 60 | 61 | /** 62 | * @param Failure $failure 63 | * @return string 64 | */ 65 | protected function additionalFailureDescription($failure): string 66 | { 67 | return sprintf( 68 | "%s%sTest runs: %d, seed: %s, smallest shrunk value(s):\n%s", 69 | $this->extractExceptionMessage($failure->test()->result()), 70 | $this->extractExceptionStacktrace($failure->test()->result()), 71 | $failure->numTests(), 72 | $failure->seed(), 73 | var_export($failure->shrunk()->test()->arguments(), true) 74 | ); 75 | } 76 | 77 | private function extractExceptionMessage($result): string 78 | { 79 | return $result instanceof \Exception ? $result->getMessage() . "\n" : ''; 80 | } 81 | 82 | private function extractExceptionStacktrace($result): string 83 | { 84 | return $this->showExceptionStacktrace && $result instanceof \Exception ? 85 | $result->getTraceAsString() . "\n" : 86 | ''; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/QuickCheck/Property.php: -------------------------------------------------------------------------------- 1 | generator = $generator; 13 | $this->maxSize = $maxSize; 14 | } 15 | 16 | public static function forAll(array $args, callable $f, int $maxSize = 200): self 17 | { 18 | return new self(Generator::tuples($args)->map(function ($args) use ($f) { 19 | try { 20 | $result = call_user_func_array($f, $args); 21 | } catch (\Exception $e) { 22 | $result = $e; 23 | } 24 | return new PropertyTest($result, $f, $args); 25 | }), $maxSize); 26 | } 27 | 28 | function withMaxSize(int $maxSize) { 29 | return new Property($this->generator, $maxSize); 30 | } 31 | 32 | function randomTests(Random $rng) { 33 | return Lazy::map( 34 | function($size) use ($rng) { 35 | return $this->generator->call($rng, $size); 36 | }, 37 | Generator::sizes($this->maxSize) 38 | ); 39 | } 40 | 41 | static function check(self $prop, int $numTests, int $seed = null): CheckResult { 42 | $seed = $seed ?? intval(microtime(true) * 1000); 43 | $rng = new Random($seed); 44 | /** @var ShrinkTreeNode[] $tests */ 45 | $tests = Lazy::take($numTests, $prop->randomTests($rng)); 46 | $testCount = 0; 47 | foreach ($tests as $node) { 48 | $testCount++; 49 | /** @var PropertyTest $test */ 50 | $test = $node->getValue(); 51 | if (PropertyTest::isFailure($test)) { 52 | $failed = $test; 53 | $shrunk = new ShrinkResult(0, 0, $test); 54 | foreach (ShrinkResult::searchSmallest($node) as $result) { 55 | $shrunk = $result; 56 | } 57 | return new Failure($testCount, $seed, $failed, $shrunk); 58 | } 59 | } 60 | return new Success($testCount, $seed); 61 | } 62 | 63 | function maxSize() { 64 | return $this->maxSize; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/QuickCheck/PropertyTest.php: -------------------------------------------------------------------------------- 1 | result = $result; 14 | $this->predicate = $predicate; 15 | $this->arguments = $arguments; 16 | } 17 | 18 | static function isFailure(self $test) 19 | { 20 | return !$test->result || $test->result instanceof \Exception; 21 | } 22 | 23 | public function result() 24 | { 25 | return $this->result; 26 | } 27 | 28 | public function predicate(): callable 29 | { 30 | return $this->predicate; 31 | } 32 | 33 | public function arguments(): array 34 | { 35 | return $this->arguments; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/QuickCheck/Random.php: -------------------------------------------------------------------------------- 1 | > 32; 18 | } 19 | public static function rshiftu($a, $b) 20 | { 21 | if ($b == 0) { 22 | return $a; 23 | } 24 | 25 | return ($a >> $b) & ~(1 << 63 >> ($b - 1)); 26 | } 27 | public static function mask($val) 28 | { 29 | return $val & ((1 << 48) - 1); 30 | } 31 | public function __construct($seed = null) 32 | { 33 | if (PHP_INT_SIZE < 8) { 34 | throw new \RuntimeException('64 bit integer precision required'); 35 | } 36 | $this->setSeed($seed !== null ? $seed : intval(1000 * microtime(true))); 37 | } 38 | private function setSeed($seed) 39 | { 40 | $this->seed = self::mask($seed ^ self::MULTIPLIER); 41 | } 42 | protected function next($bits) 43 | { 44 | $temp = function_exists('gmp_mul') ? 45 | gmp_intval(gmp_mul($this->seed, self::MULTIPLIER)) : 46 | self::int_mul($this->seed, self::MULTIPLIER); 47 | $this->seed = self::mask($temp + self::ADDEND); 48 | 49 | return self::i32(self::rshiftu($this->seed, (48 - $bits))); 50 | } 51 | public function nextDouble() 52 | { 53 | return (($this->next(26) << 27) + $this->next(27)) 54 | / (double) (1 << 53); 55 | } 56 | public function nextInt() 57 | { 58 | return $this->next(32); 59 | } 60 | 61 | // does Java/C/Go-like overflow 62 | // http://stackoverflow.com/questions/4456442/multiplication-of-two-integers-using-bitwise-operators 63 | public static function int_mul($a, $b) 64 | { 65 | $result = 0; 66 | while ($b != 0) { 67 | // Iterate the loop till b==0 68 | if ($b & 01) { // is $b odd? 69 | $result = self::int_add($result, $a); // int_add result 70 | } 71 | $a <<= 1; // Left shifting the value contained in 'a' by 1 72 | // multiplies a by 2 for each loop 73 | $b >>= 1; // Right shifting the value contained in 'b' by 1. 74 | } 75 | 76 | return $result; 77 | } 78 | 79 | // addition with only shifts and ands 80 | public static function int_add($x, $y) 81 | { 82 | if ($y == 0) { 83 | return $x; 84 | } else { 85 | return self::int_add($x ^ $y, ($x & $y) << 1); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/QuickCheck/ShrinkResult.php: -------------------------------------------------------------------------------- 1 | visited = $visited; 14 | $this->depth = $depth; 15 | $this->test = $test; 16 | } 17 | 18 | function visited() { 19 | return $this->visited; 20 | } 21 | 22 | function depth() { 23 | return $this->depth; 24 | } 25 | 26 | function test(): PropertyTest { 27 | return $this->test; 28 | } 29 | 30 | /** 31 | * @param ShrinkTreeNode $tree 32 | * @return \Generator|ShrinkResult[] 33 | */ 34 | static function searchSmallest(ShrinkTreeNode $tree) 35 | { 36 | $nodes = $tree->getChildren(); 37 | $visited = 0; 38 | $depth = 0; 39 | for (; $nodes->valid(); $nodes->next(), $visited++) { 40 | /** @var ShrinkTreeNode $head */ 41 | $head = $nodes->current(); 42 | /** @var PropertyTest $result */ 43 | $result = $head->getValue(); 44 | if (PropertyTest::isFailure($result)) { 45 | $children = $head->getChildren(); 46 | if (!empty($children)) { 47 | $nodes = $children; 48 | $depth++; 49 | } 50 | yield new ShrinkResult($visited, $depth, $result); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/QuickCheck/ShrinkTreeNode.php: -------------------------------------------------------------------------------- 1 | value = $value; 19 | $this->children = $children; 20 | } 21 | 22 | public static function pure($val) 23 | { 24 | return new self($val, function () { 25 | return []; 26 | }); 27 | } 28 | 29 | public function getValue() 30 | { 31 | return $this->value; 32 | } 33 | 34 | public function getChildren() 35 | { 36 | return call_user_func($this->children); 37 | } 38 | 39 | public function map(callable $f) 40 | { 41 | return new self( 42 | call_user_func($f, $this->value), 43 | function () use ($f) { 44 | return Lazy::map( 45 | function (ShrinkTreeNode $x) use ($f) { 46 | return $x->map($f); 47 | }, 48 | $this->getChildren() 49 | ); 50 | } 51 | ); 52 | } 53 | 54 | public function join() 55 | { 56 | $innerRoot = $this->value->getValue(); 57 | return new self($innerRoot, function() { 58 | return Lazy::concat( 59 | Lazy::map( 60 | function (self $x) { 61 | return $x->join(); 62 | }, 63 | $this->getChildren() 64 | ), 65 | $this->value->getChildren() 66 | ); 67 | }); 68 | } 69 | 70 | public function chain(callable $f) 71 | { 72 | return $this->map($f)->join(); 73 | } 74 | 75 | public function filter(callable $pred) 76 | { 77 | return new self( 78 | $this->value, 79 | function () use ($pred) { 80 | return Lazy::map( 81 | function (self $child) use ($pred) { 82 | return $child->filter($pred); 83 | }, 84 | Lazy::filter( 85 | function (self $child) use ($pred) { 86 | return call_user_func($pred, $child->getValue()); 87 | }, 88 | $this->getChildren() 89 | ) 90 | ); 91 | } 92 | ); 93 | } 94 | 95 | private static function permutations(array $nodes) 96 | { 97 | foreach ($nodes as $index => $node) { 98 | foreach ($node->getChildren() as $child) { 99 | yield Arrays::assoc($nodes, $index, $child); 100 | } 101 | } 102 | } 103 | 104 | public static function zip(callable $f, $nodes) 105 | { 106 | return new self( 107 | call_user_func_array($f, Lazy::realize(Lazy::map( 108 | function (self $node) { 109 | return $node->getValue(); 110 | }, 111 | $nodes 112 | ))), 113 | function () use ($f, $nodes) { 114 | return Lazy::map( 115 | function ($xs) use ($f) { 116 | return self::zip($f, $xs); 117 | }, 118 | self::permutations($nodes) 119 | ); 120 | } 121 | ); 122 | } 123 | 124 | private static function remove(array $nodes) 125 | { 126 | return Lazy::concat( 127 | Lazy::map( 128 | function ($index) use ($nodes) { 129 | return Arrays::excludeNth($index, $nodes); 130 | }, 131 | array_keys($nodes) 132 | ), 133 | self::permutations($nodes) 134 | ); 135 | } 136 | 137 | public static function shrink(callable $f, $nodes) 138 | { 139 | $nodes = Lazy::realize($nodes); 140 | if (empty($nodes)) { 141 | return self::pure(call_user_func($f)); 142 | } else { 143 | return new self( 144 | call_user_func_array($f, Lazy::realize(Lazy::map( 145 | function (self $node) { 146 | return $node->getValue(); 147 | }, 148 | $nodes 149 | ))), 150 | function () use ($f, $nodes) { 151 | return Lazy::map( 152 | function ($x) use ($f) { 153 | return self::shrink($f, $x); 154 | }, 155 | self::remove($nodes) 156 | ); 157 | } 158 | ); 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/QuickCheck/Success.php: -------------------------------------------------------------------------------- 1 | numTests = $numTests; 12 | $this->seed = $seed; 13 | } 14 | 15 | function numTests(): int 16 | { 17 | return $this->numTests; 18 | } 19 | 20 | function seed(): int 21 | { 22 | return $this->seed; 23 | } 24 | 25 | function isSuccess(): bool 26 | { 27 | return true; 28 | } 29 | 30 | function isFailure(): bool 31 | { 32 | return false; 33 | } 34 | 35 | function dump(callable $encode): void 36 | { 37 | echo $this->numTests(), ' tests were successful', PHP_EOL; 38 | echo 'Seed: ', $this->seed(), PHP_EOL; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/QuickCheck/Test.php: -------------------------------------------------------------------------------- 1 | times($times) 18 | ->maxSize($maxSize) 19 | ->forAll($args, $pred); 20 | } 21 | 22 | static function check(string $name = null) { 23 | self::ensureSuite(); 24 | return new Check(self::$currentSuite, $name); 25 | } 26 | 27 | static private function ensureSuite() { 28 | if (self::$currentSuite === null) { 29 | throw new \RuntimeException('no active suite'); 30 | } 31 | } 32 | 33 | static private function readOpts(array $argv) { 34 | $n = count($argv); 35 | $args = []; 36 | $opts = []; 37 | $current = &$args; 38 | for ($i = 0; $i < $n; ++$i) { 39 | if ($argv[$i][0] === '-') { 40 | $name = substr($argv[$i], 1); 41 | $opts[$name] = []; 42 | $current = &$opts[$name]; 43 | } else { 44 | $current[] = $argv[$i]; 45 | } 46 | } 47 | return [$args, array_map(function($opt) { 48 | return @$opt[0] ?? true; 49 | }, $opts)]; 50 | } 51 | 52 | static private function printUsage() { 53 | echo "Usage: quickcheck [ FILE | DIRECTORY ] [OPTIONS]", PHP_EOL; 54 | } 55 | 56 | static private function requireSuiteFile($name, $file) { 57 | self::$currentSuite = new CheckSuite($name); 58 | require_once $file; 59 | if (!self::$currentSuite->empty()) { 60 | self::$suites[] = self::$currentSuite; 61 | } 62 | } 63 | 64 | private static function loadSuites($args) { 65 | self::$suites = []; 66 | self::$currentSuite = null; 67 | 68 | if (is_dir($args[0])) { 69 | $it = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($args[0])); 70 | $real = realpath($args[0]); 71 | foreach ($it as $file) { 72 | if ($file->getExtension() !== 'php') continue; 73 | $name = substr($file->getRealPath(), strlen($real) + 1, -4); 74 | self::requireSuiteFile($name, $file->getRealPath()); 75 | } 76 | } elseif (is_file($args[0])) { 77 | self::requireSuiteFile(basename($args[0], '.php'), $args[0]); 78 | } else { 79 | echo "Error: \"$args[0]\" is not a valid directory or file.", PHP_EOL; 80 | self::printUsage(); 81 | exit(1); 82 | } 83 | } 84 | 85 | static function main(array $argv) { 86 | if (count($argv) < 2) { 87 | self::printUsage(); 88 | exit(0); 89 | } 90 | [$args, $opts] = self::readOpts(array_slice($argv, 1)); 91 | self::loadSuites($args); 92 | $seed = intval(@$opts['s'] ?? 1000 * microtime(true)); 93 | $testRunner = new TestRunner( 94 | @$opts['t'] ? intval($opts['t']) : null, 95 | @$opts['x'] ? intval($opts['x']) : null 96 | ); 97 | $testRunner->execute(self::$suites, $seed); 98 | } 99 | } -------------------------------------------------------------------------------- /src/QuickCheck/TestRunner.php: -------------------------------------------------------------------------------- 1 | numTests = $numTests; 17 | $this->maxSize = $maxSize; 18 | $this->out = new ConsoleOutput(); 19 | } 20 | 21 | /** 22 | * @param CheckSuite[] $suites 23 | * @param int $seed 24 | */ 25 | function execute(array $suites, int $seed) { 26 | $this->writeLn("PHPQuickCheck 2.0.2. Don't write tests. Generate them."); 27 | if (function_exists('xdebug_is_enabled')) { 28 | ini_set('xdebug.max_nesting_level', '999999'); 29 | $this->writeLn('Warning: xdebug is enabled. This has a high performance impact.'); 30 | } 31 | $this->writeLn(); 32 | 33 | $this->testCount = 0; 34 | if (count($suites) === 1 && count($suites[0]->checks()) === 1) { 35 | $this->executeSingleSuiteCheck($suites[0]->checks()[0], $seed); 36 | } else { 37 | $this->executeSuiteChecks($suites, $seed); 38 | } 39 | } 40 | 41 | private function executeSingleSuiteCheck(Check $check, int $seed) { 42 | $prop = $check->property(); 43 | $numTests = $this->numTests ?? $check->numTests(); 44 | $maxSize = $this->maxSize ?? $prop->maxSize(); 45 | $failure = $this->executeTests($seed, $numTests, $prop->withMaxSize($maxSize)); 46 | if ($failure === null) { 47 | $this->writeLn("Success ($this->testCount tests)"); 48 | exit(0); 49 | } 50 | $this->executeShrinkSearch($failure); 51 | $this->writeLn("\nQED. ($this->testCount tests)"); 52 | exit(1); 53 | } 54 | 55 | private function executeSuiteChecks(array $suites, int $seed) { 56 | $propCount = 0; 57 | $totalTestCount = 0; 58 | $start = microtime(true); 59 | $failures = 0; 60 | foreach ($suites as $suite) { 61 | $singleCheck = count($suite->checks()) === 1; 62 | if (!$singleCheck) { 63 | $this->writeLn($suite->file()); 64 | } 65 | foreach ($suite->checks() as $check) { 66 | $propCount++; 67 | $name = $check->name() ?? $suite->file(); 68 | if ($singleCheck) { 69 | $this->write(sprintf('%\'.-50s', $name)); 70 | } else { 71 | $this->write(sprintf(' %\'.-48s', $name)); 72 | } 73 | $numTests = $this->numTests ?? $check->numTests(); 74 | $maxSize = $this->maxSize ?? $check->property()->maxSize(); 75 | $failure = $this->executeTests($seed, $numTests, $check->property()->withMaxSize($maxSize), true); 76 | $totalTestCount += $this->testCount; 77 | if ($failure === null) { 78 | $this->writeLn(" OK t = $this->testCount, x = $maxSize"); 79 | } else { 80 | $failures++; 81 | $this->writeLn(" QED. ($this->testCount tests) x = $maxSize\n"); 82 | $this->executeShrinkSearch($failure); 83 | $this->writeLn(); 84 | } 85 | } 86 | } 87 | $elapsed = microtime(true) - $start; 88 | $mem = memory_get_peak_usage(true)/(1024*1024); 89 | $this->writeLn(sprintf("\nTime: %d ms, Memory: %.2f MB, Seed: %d\n", 90 | $elapsed * 1000, $mem, $seed)); 91 | 92 | if ($failures === 0) { 93 | $this->writeLn("Success ($propCount properties, $totalTestCount tests)"); 94 | exit(0); 95 | } else { 96 | $this->writeLn("FAILURES! ($failures failed, $propCount properties, $totalTestCount tests)"); 97 | exit(1); 98 | } 99 | } 100 | 101 | private function executeTests(int $seed, int $numTests, Property $property, bool $silent = false) { 102 | $testProgress = new ProgressBar($this->out); 103 | if (!$silent) { 104 | $testProgress->setBarWidth(50); 105 | $testProgress->start($numTests); 106 | } 107 | 108 | $startTime = microtime(true); 109 | $rng = new Random($seed); 110 | /** @var ShrinkTreeNode[] $tests */ 111 | $tests = Lazy::take($numTests, $property->randomTests($rng)); 112 | /** @var ShrinkTreeNode|null $failure */ 113 | $failure = null; 114 | $this->testCount = 0; 115 | foreach ($tests as $test) { 116 | $this->testCount++; 117 | if (!$silent) {$testProgress->advance();} 118 | if (PropertyTest::isFailure($test->getValue())) { 119 | $failure = $test; 120 | break; 121 | } 122 | } 123 | 124 | $elapsed = microtime(true) - $startTime; 125 | if (!$silent) { 126 | $testProgress->setProgress($this->testCount); 127 | $testProgress->display(); 128 | 129 | $this->writeLn(''); 130 | $this->writeLn(''); 131 | 132 | $mem = memory_get_peak_usage(true)/(1024*1024); 133 | $this->writeLn(sprintf("Time: %d ms, Memory: %.2f MB, Seed: %d, maxSize: %d", 134 | $elapsed * 1000, $mem, $seed, $property->maxSize())); 135 | $this->writeLn(''); 136 | } 137 | 138 | return $failure; 139 | } 140 | 141 | private function formatTestArguments(PropertyTest $result, $lineLimit = 10, $wiggle = 5) { 142 | $str = var_export($result->arguments(), true); 143 | //json_encode($xs, JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT|JSON_INVALID_UTF8_IGNORE); 144 | $lines = explode("\n", $str); 145 | $lineCount = count($lines); 146 | $linesOut = array_slice($lines, 0, $lineLimit); 147 | if ($lineCount > $lineLimit + $wiggle) { 148 | $omitted = $lineCount - $lineLimit; 149 | $linesOut[] = "\n<{$omitted} more lines have been omitted>\n"; 150 | } 151 | return implode("\n", $linesOut); 152 | } 153 | 154 | private function writeLn($msg = null) { 155 | $this->out->writeLn($msg ?? ''); 156 | } 157 | 158 | private function write(string $msg) { 159 | $this->out->write($msg); 160 | } 161 | 162 | private function executeShrinkSearch(ShrinkTreeNode $failure) 163 | { 164 | /** @var PropertyTest $result */ 165 | $result = $failure->getValue(); 166 | $this->write('Failing inputs: '); 167 | $this->writeLn($this->formatTestArguments($result)); 168 | $this->writeLn(); 169 | 170 | $info = $this->out->section(); 171 | $info->writeLn('Shrinking inputs...'); 172 | 173 | $shrinkStart = microtime(true); 174 | /** @var ShrinkResult $smallest */ 175 | $smallest = new ShrinkResult(0, 0, $result); 176 | $lastShrinkProgressUpdate = 0; 177 | $progress = $this->out->section(); 178 | foreach (ShrinkResult::searchSmallest($failure) as $shrinkResult) { 179 | $now = microtime(true); 180 | $shrinkElapsed = $now - $shrinkStart; 181 | if ($now - $lastShrinkProgressUpdate > 1) { 182 | $lastShrinkProgressUpdate = $now; 183 | $throughput = $shrinkResult->visited() / $shrinkElapsed; 184 | $progress->clear(); 185 | $progress->writeLn(sprintf('visited: %d, depth: %d, memory: %.2f MB, throughput: %d visits/s', 186 | $shrinkResult->visited(), 187 | $shrinkResult->depth(), 188 | memory_get_usage() / (1024 * 1024), 189 | $throughput)); 190 | $progress->write('Current smallest failing inputs: '); 191 | $progress->writeLn($this->formatTestArguments($shrinkResult->test())); 192 | } 193 | $smallest = $shrinkResult; 194 | } 195 | $info->overwrite(sprintf('Shrinking inputs...done. (%.2f s)', microtime(true) - $shrinkStart)); 196 | $progress->clear(); 197 | $this->write('Smallest failing inputs: '); 198 | $this->writeLn($this->formatTestArguments($smallest->test())); 199 | } 200 | } -------------------------------------------------------------------------------- /test/QuickCheck/AnnotationTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($type, array('a' => 'string')); 207 | } 208 | 209 | function testCheckByName() { 210 | $result = Annotation::check('QuickCheck\_annotation_test_function'); 211 | $this->assertSuccess($result); 212 | } 213 | 214 | function testTypeByVariable() { 215 | $type = Annotation::types(self::$fun); 216 | $this->assertEquals($type, array('a' => 'string')); 217 | } 218 | 219 | function testCheckByVariable() { 220 | $result = Annotation::check(self::$fun); 221 | $this->assertSuccess($result); 222 | } 223 | 224 | function testTypeByInvoke() { 225 | $type = Annotation::types(self::$invoke); 226 | $this->assertEquals($type, array('a' => 'string')); 227 | } 228 | 229 | function testCheckByInvoke() { 230 | $result = Annotation::check(self::$invoke); 231 | $this->assertSuccess($result); 232 | } 233 | 234 | function testStatic() { 235 | $function = 'static_method'; 236 | $array = Annotation::types(self::getArrayCallable($function)); 237 | $string = Annotation::types(self::getStringCallable($function)); 238 | $object = Annotation::types(self::getObjectCallable($function)); 239 | 240 | $types = array('a' => 'string', 'b' => 'int', 'c' => 'array'); 241 | $this->assertEquals($array, $types); 242 | $this->assertEquals($string, $types); 243 | $this->assertEquals($object, $types); 244 | } 245 | 246 | function getMethods() { 247 | return array( 248 | array('str', array('a' => 'string')), 249 | array('int', array('a' => 'int')), 250 | array('arr', array('a' => 'array')), 251 | array('str_str_str', array('a' => 'string', 'b' => 'string', 'c' => 'string')), 252 | array('str_int_arr', array('a' => 'string', 'b' => 'int', 'c' => 'array')), 253 | array('reverse', array('a' => 'string', 'b' => 'int', 'c' => 'array')), 254 | array('static_method', array('a' => 'string', 'b' => 'int', 'c' => 'array')), 255 | ); 256 | } 257 | 258 | /** 259 | * @dataProvider getMethods 260 | */ 261 | function testType($function, $types) { 262 | $object = Annotation::types(self::getObjectCallable($function)); 263 | $this->assertEquals($object, $types); 264 | } 265 | 266 | function getFaultyMethods() { 267 | return array( 268 | array('nodoc'), 269 | array('faultydoc'), 270 | array('incompletedoc'), 271 | ); 272 | } 273 | 274 | /** 275 | * @dataProvider getFaultyMethods 276 | */ 277 | function testNoTypeString($function) { 278 | $this->expectException(MissingTypeAnnotationException::class); 279 | Annotation::types(self::getStringCallable($function)); 280 | } 281 | 282 | /** 283 | * @dataProvider getFaultyMethods 284 | */ 285 | function testNoTypeArray($function) { 286 | $this->expectException(MissingTypeAnnotationException::class); 287 | Annotation::types(self::getArrayCallable($function)); 288 | } 289 | 290 | /** 291 | * @dataProvider getFaultyMethods 292 | */ 293 | function testNoTypeObject($function) { 294 | $this->expectException(MissingTypeAnnotationException::class); 295 | Annotation::types(self::getObjectCallable($function)); 296 | } 297 | 298 | function getAmbiguousMethods() { 299 | return array( 300 | array('ambiguousdoc'), 301 | ); 302 | } 303 | 304 | /** 305 | * @dataProvider getAmbiguousMethods 306 | */ 307 | function testAmbiguousTypeString($function) { 308 | $this->expectException(AmbiguousTypeAnnotationException::class); 309 | Annotation::types(self::getStringCallable($function)); 310 | } 311 | 312 | /** 313 | * @dataProvider getAmbiguousMethods 314 | */ 315 | function testAmbiguousTypeArray($function) { 316 | $this->expectException(AmbiguousTypeAnnotationException::class); 317 | Annotation::types(self::getArrayCallable($function)); 318 | } 319 | 320 | /** 321 | * @dataProvider getAmbiguousMethods 322 | */ 323 | function testAmbiguousTypeObject($function) { 324 | $this->expectException(AmbiguousTypeAnnotationException::class); 325 | Annotation::types(self::getObjectCallable($function)); 326 | } 327 | 328 | function getNoGeneratorMethods() { 329 | return array( 330 | array('nogenerator'), 331 | ); 332 | } 333 | 334 | /** 335 | * @dataProvider getNoGeneratorMethods 336 | */ 337 | function testNoGeneratorTypeString($function) { 338 | $this->expectException(NoGeneratorAnnotationException::class); 339 | Annotation::check(self::getStringCallable($function)); 340 | } 341 | 342 | /** 343 | * @dataProvider getNoGeneratorMethods 344 | */ 345 | function testNoGeneratorTypeArray($function) { 346 | $this->expectException(NoGeneratorAnnotationException::class); 347 | Annotation::check(self::getArrayCallable($function)); 348 | } 349 | 350 | /** 351 | * @dataProvider getNoGeneratorMethods 352 | */ 353 | function testNoGeneratorTypeObject($function) { 354 | $this->expectException(NoGeneratorAnnotationException::class); 355 | Annotation::check(self::getObjectCallable($function)); 356 | } 357 | 358 | function getCheckMethods() { 359 | return array( 360 | array('check_str'), 361 | array('check_int'), 362 | array('check_bool'), 363 | array('check_boolean'), 364 | array('check_array'), 365 | array('check_multiple'), 366 | ); 367 | } 368 | 369 | /** 370 | * @dataProvider getCheckMethods 371 | */ 372 | function testCheck($function) { 373 | $object = Annotation::check(self::getObjectCallable($function)); 374 | $this->assertSuccess($object); 375 | } 376 | 377 | function testRegister() { 378 | $generator = Generator::any()->map(function() { return new _TestClass(); }); 379 | Annotation::register('_TestClass', $generator); 380 | 381 | $array = Annotation::check(self::getArrayCallable('custom_type'), null, 1); 382 | $string = Annotation::check(self::getStringCallable('custom_type'), null, 1); 383 | $object = Annotation::check(self::getObjectCallable('custom_type'), null, 1); 384 | 385 | $this->assertSuccess($array); 386 | $this->assertSuccess($string); 387 | $this->assertSuccess($object); 388 | } 389 | 390 | function testDuplicateRegister() { 391 | $this->expectException(DuplicateGeneratorException::class); 392 | $generator = Generator::any()->map(function() { return new _TestClass(); }); 393 | Annotation::register('_TestClass', $generator); 394 | Annotation::register('_TestClass', $generator); 395 | } 396 | 397 | function assertSuccess(CheckResult $x) { 398 | $this->assertTrue($x->isSuccess()); 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /test/QuickCheck/LazyTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 10 | [0, 2, 4], 11 | Lazy::realize(Lazy::takeNth(2, Lazy::range(0, 5))) 12 | ); 13 | $this->assertEquals( 14 | [0, 3, 6], 15 | Lazy::realize(Lazy::takeNth(3, Lazy::range(0, 8))) 16 | ); 17 | } 18 | 19 | function testPartition() { 20 | $this->assertEquals( 21 | [[0], [1], [2]], 22 | Lazy::realize(Lazy::partition(1, Lazy::range(0, 3))) 23 | ); 24 | $this->assertEquals( 25 | [[0, 1], [2, 3], [4, 5], [6]], 26 | Lazy::realize(Lazy::partition(2, Lazy::range(0, 7))) 27 | ); 28 | $this->assertEquals( 29 | [[0, 1, 2], [3, 4, 5], [6, 7]], 30 | Lazy::realize(Lazy::partition(3, Lazy::range(0, 8))) 31 | ); 32 | } 33 | } -------------------------------------------------------------------------------- /test/QuickCheck/PHPUnitIntegrationTest.php: -------------------------------------------------------------------------------- 1 | strlen($s); } 18 | ); 19 | 20 | $this->assertFalse( 21 | PropertyConstraint::check(50)->evaluate( 22 | $property, 23 | 'Expected property to fail', 24 | true 25 | ) 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/QuickCheck/QuicknDirtyTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(is_integer($int)); 16 | } 17 | function ints() { 18 | return Gen::tuples(Gen::ints())->takeSamples(); 19 | } 20 | 21 | /** 22 | * @dataProvider lists 23 | */ 24 | function testStringList($list) { 25 | $this->assertIsArray($list); 26 | $this->assertContainsOnly('int', array_keys($list)); 27 | $this->assertContainsOnly('string', $list); 28 | } 29 | function lists() { 30 | return Gen::tuples(Gen::asciiStrings()->intoArrays())->takeSamples(); 31 | } 32 | 33 | /** 34 | * @dataProvider minMaxArrays 35 | */ 36 | function testMinMaxArrays($xs) { 37 | $this->assertIsArray($xs); 38 | $this->assertGreaterThanOrEqual(23, count($xs)); 39 | $this->assertLessThanOrEqual(42, count($xs)); 40 | } 41 | function minMaxArrays() { 42 | return Gen::tuples(Gen::ints()->intoArrays(23, 42))->takeSamples(50); 43 | } 44 | 45 | /** 46 | * @dataProvider fixedLengthArrays 47 | */ 48 | function testFixedLengthArrays($xs) { 49 | $this->assertIsArray($xs); 50 | $this->assertCount(23, $xs); 51 | } 52 | function fixedLengthArrays() { 53 | return Gen::tuples(Gen::ints()->intoArrays(23))->takeSamples(50); 54 | } 55 | 56 | function testQuickCheckShrink() { 57 | $p = Property::forAll([Gen::asciiStrings()], function($s) { 58 | return !is_numeric($s); 59 | }); 60 | 61 | $result = Property::check($p, 10000); 62 | $this->assertInstanceOf(Failure::class, $result, 'property was not falsified'); 63 | 64 | $smallest = $result->shrunk()->test()->arguments()[0]; 65 | $this->assertEquals('0', $smallest, 66 | "expected smallest to be '0' but got '$smallest'"); 67 | } 68 | 69 | function testPropConstraintFailure() { 70 | $this->expectException(AssertionFailedError::class); 71 | $this->assertThat(Property::forAll([Gen::strings()], function($str) { 72 | return strlen($str) < 10; 73 | }), PropertyConstraint::check(100)); 74 | } 75 | } 76 | --------------------------------------------------------------------------------