Composite generators

These Generators implement the Composite pattern to wire together existing Generators and callables.

Frequency

Generator\frequency randomly chooses a Generator to use from the specified list, weighting the probability of each Generator with the provided value.

<?php
use Eris\Generator;

class FrequencyTest extends \PHPUnit_Framework_TestCase
{
    use Eris\TestTrait;

    public function testFalsyValues()
    {
        $this
            ->forAll(
                Generator\frequency(
                    [8, false],
                    [4, 0],
                    [4, '']
                )
            )
            ->then(function ($falsyValue) {
                $this->assertFalse((bool) $falsyValue);
            });
    }

    public function testAlwaysFails()
    {
        $this
            ->forAll(
                Generator\frequency(
                    [8, Generator\choose(1, 100)],
                    [4, Generator\choose(100, 200)],
                    [4, Generator\choose(200, 300)]
                )
            )
            ->then(function ($element) {
                $this->assertEquals(0, $element);
            });
    }
}

testFalsyValues chooses the false value half of the times, 0 one quarter of the time, and '' one quarte of the time.

testAlwaysFails chooses the Generator from 1 to 100 half of the times. However, in case of failure it will try to shrink the value only with the original Generator that created it. Therefore, each of the possible outputs will be possible:

Failed asserting that 1 matches expected 0.
Failed asserting that 100 matches expected 0.
Failed asserting that 200 matches expected 0.

One Of

Generator\oneOf is a special case of Generator\frequency which selects each of the specified Generators with the same probability.

<?php
use Eris\Generator;

class OneOfTest extends \PHPUnit_Framework_TestCase
{
    use Eris\TestTrait;

    public function testPositiveOrNegativeNumberButNotZero()
    {
        $this
            ->forAll(
                Generator\oneOf(
                    Generator\pos(),
                    Generator\neg()
                )
            )
            ->then(function ($number) {
                $this->assertNotEquals(0, $number);
            });
    }
}

See also

elements() does the same with values instead of Generators.

Map

Map allows a Generator’s output to be modified by applying the callable to the generated value.

<?php
use Eris\Generator;

class MapTest extends PHPUnit_Framework_TestCase
{
    use Eris\TestTrait;

    public function testApplyingAFunctionToGeneratedValues()
    {
        $this->forAll(
            Generator\vector(
                3,
                Generator\map(
                    function ($n) {
                        return $n * 2;
                    },
                    Generator\nat()
                )
            )
        )
            ->then(function ($tripleOfEvenNumbers) {
                foreach ($tripleOfEvenNumbers as $number) {
                    $this->assertTrue(
                        $number % 2 == 0,
                        "The element of the vector $number is not even"
                    );
                }
            });
    }

    public function testShrinkingJustMappedValues()
    {
        $this->forAll(
            Generator\map(
                function ($n) {
                    return $n * 2;
                },
                Generator\nat()
            )
        )
            ->then(function ($evenNumber) {
                $this->assertLessThanOrEqual(
                    100,
                    $evenNumber,
                    "The number is not less than 100"
                );
            });
    }

    public function testShrinkingMappedValuesInsideOtherGenerators()
    {
        $this->forAll(
            Generator\vector(
                3,
                Generator\map(
                    function ($n) {
                        return $n * 2;
                    },
                    Generator\nat()
                )
            )
        )
            ->then(function ($tripleOfEvenNumbers) {
                $this->assertLessThanOrEqual(
                    100,
                    array_sum($tripleOfEvenNumbers),
                    "The triple sum " . var_export($tripleOfEvenNumbers, true) . " is not less than 100"
                );
            });
    }

    // TODO: multiple generators means multiple values passed to map
}

testApplyingAFunctionToGeneratedValues generates a vector of even numbers. Notice that any mapping can still be composed by other Generators: in this case, the even number Generator can be composed by Generator\vector(),

testShrinkingJustMappedValues shows how shrinking respects the mapping function: running this test produces 102 as the minimal input that still makes the assertion fail. The underlying Generator\nat() shrinks number by decrementing them, but the mapping function is still applied so that only even numbers are passed to the then().

1) MapTest::testShrinkingJustMappedValues
The number is not less than 100
Failed asserting that 102 is equal to 100 or is less than 100.

/home/giorgio/code/eris/examples/MapTest.php:42
/home/giorgio/code/eris/src/Eris/Quantifier/Evaluation.php:51
/home/giorgio/code/eris/src/Eris/Shrinker/Random.php:68
/home/giorgio/code/eris/src/Eris/Quantifier/ForAll.php:128
/home/giorgio/code/eris/src/Eris/Quantifier/Evaluation.php:53
/home/giorgio/code/eris/src/Eris/Quantifier/ForAll.php:130
/home/giorgio/code/eris/src/Eris/Quantifier/ForAll.php:158
/home/giorgio/code/eris/examples/MapTest.php:43
/home/giorgio/code/eris/examples/MapTest.php:43

FAILURES!
Tests: 1, Assertions: 254, Failures: 1.

testShrinkingMappedValuesInsideOtherGenerators puts both examples together and generates a triple of even numbers, failing the test if their sum is greater than 100. The minimal failing example is a triple of number whose sum is 102.

1) MapTest::testShrinkingMappedValuesInsideOtherGenerators
The triple sum array (
  0 => 52,
  1 => 36,
  2 => 14,
) is not less than 100
Failed asserting that 102 is equal to 100 or is less than 100.

/home/giorgio/code/eris/examples/MapTest.php:62
/home/giorgio/code/eris/src/Eris/Quantifier/Evaluation.php:51
/home/giorgio/code/eris/src/Eris/Quantifier/ForAll.php:130
/home/giorgio/code/eris/src/Eris/Quantifier/ForAll.php:158
/home/giorgio/code/eris/examples/MapTest.php:63
/home/giorgio/code/eris/examples/MapTest.php:63

FAILURES!
Tests: 1, Assertions: 216, Failures: 1.

Such That

Such That allows a Generator’s output to be filtered, excluding values that to do not satisfy a condition.

<?php
use Eris\Generator;
use Eris\Listener;

class SuchThatTest extends \PHPUnit_Framework_TestCase
{
    use Eris\TestTrait;

    public function testSuchThatBuildsANewGeneratorFilteringTheInnerOne()
    {
        $this
            ->forAll(
                Generator\vector(
                    5,
                    Generator\suchThat(
                        function ($n) {
                            return $n > 42;
                        },
                        Generator\choose(0, 1000)
                    )
                )
            )
            ->then($this->allNumbersAreBiggerThan(42));
    }

    public function testFilterSyntax()
    {
        $this
            ->forAll(
                Generator\vector(
                    5,
                    Generator\filter(
                        function ($n) {
                            return $n > 42;
                        },
                        Generator\choose(0, 1000)
                    )
                )
            )
            ->then($this->allNumbersAreBiggerThan(42));
    }

    public function testSuchThatAcceptsPHPUnitConstraints()
    {
        $this
            ->forAll(
                Generator\vector(
                    5,
                    Generator\suchThat(
                        $this->isType('integer'),
                        Generator\oneOf(
                            Generator\choose(0, 1000),
                            Generator\string()
                        )
                    )
                )
            )
            ->hook(Listener\log(sys_get_temp_dir().'/eris-such-that.log'))
            ->then($this->allNumbersAreBiggerThan(42));
    }


    public function testSuchThatShrinkingRespectsTheCondition()
    {
        $this
            ->forAll(
                Generator\suchThat(
                    function ($n) {
                        return $n > 42;
                    },
                    Generator\choose(0, 1000)
                )
            )
            ->then($this->numberIsBiggerThan(100));
    }

    public function testSuchThatShrinkingRespectsTheConditionButTriesToSkipOverTheNotAllowedSet()
    {
        $this
            ->forAll(
                Generator\suchThat(
                    function ($n) {
                        return $n <> 42;
                    },
                    Generator\choose(0, 1000)
                )
            )
            ->then($this->numberIsBiggerThan(100));
    }

    public function testSuchThatAvoidingTheEmptyListDoesNotGetStuckOnASmallGeneratorSize()
    {
        $this
            ->forAll(
                Generator\suchThat(
                    function (array $ints) {
                        return count($ints) > 0;
                    },
                    Generator\seq(Generator\int())
                )
            )
            ->then(function (array $ints) use (&$i) {
                $this->assertGreaterThanOrEqual(1, count($ints));
            })
        ;
    }

    public function allNumbersAreBiggerThan($lowerLimit)
    {
        return function ($vector) use ($lowerLimit) {
            foreach ($vector as $number) {
                $this->assertTrue(
                    $number > $lowerLimit,
                    "\$number was asserted to be more than $lowerLimit, but it's $number"
                );
            }
        };
    }

    public function numberIsBiggerThan($lowerLimit)
    {
        return function ($number) use ($lowerLimit) {
            $this->assertTrue(
                $number > $lowerLimit,
                "\$number was asserted to be more than $lowerLimit, but it's $number"
            );
        };
    }
}

testSuchThatBuildsANewGeneratorFilteringTheInnerOne generates a vector of numbers greater than 42. Notice that any filterting can still be composed by other Generators: in this case, the greater-than-42 number Generator can be composed by Generator\vector(),

testFilterSyntax shows the Generator\filter() syntax, which is just an alias for Generator\suchThat(). The order of the parameters requires to pass the callable first, for consistency with Generator\map() and in opposition to array_filter.

testSuchThatAcceptsPHPUnitConstraints shows that you can pass in PHPUnit constraints in lieu of callables, in the same way as they are passed to assertThat(), or to with() when defining PHPUnit mock expectations.

testSuchThatShrinkingRespectsTheCondition shows that shrinking takes into account the callable and stops when it is not satisfied anymore. Therefore, this test will fail for all numbers lower than or equal to 100, but the minimum example found is 43 as it’s the smallest and simplest value that still satisfied the condition.

1) SuchThatTest::testSuchThatShrinkingRespectsTheCondition
$number was asserted to be more than 100, but it's 43
Failed asserting that false is true.

/home/giorgio/code/eris/examples/SuchThatTest.php:85
/home/giorgio/code/eris/src/Eris/Quantifier/Evaluation.php:51
/home/giorgio/code/eris/src/Eris/Shrinker/Random.php:68
/home/giorgio/code/eris/src/Eris/Quantifier/ForAll.php:128
/home/giorgio/code/eris/src/Eris/Quantifier/Evaluation.php:53
/home/giorgio/code/eris/src/Eris/Quantifier/ForAll.php:130
/home/giorgio/code/eris/src/Eris/Quantifier/ForAll.php:158
/home/giorgio/code/eris/examples/SuchThatTest.php:34
/home/giorgio/code/eris/examples/SuchThatTest.php:34

testSuchThatShrinkingRespectsTheConditionButTriesToSkipOverTheNotAllowedSet shows instead how shrinking does not give up easily, but shrinks the inner generator even more to see is simpler values may still satisfy the condition of being different from 42. Therefore, the test fails with the shrunk input 0, not 43 as before:

1) SuchThatTest::testSuchThatShrinkingRespectsTheConditionButTriesToSkipOverTheNotAllowedSet
$number was asserted to be more than 100, but it's 0
Failed asserting that false is true.

/home/giorgio/code/eris/examples/SuchThatTest.php:85
/home/giorgio/code/eris/src/Eris/Quantifier/Evaluation.php:51
/home/giorgio/code/eris/src/Eris/Shrinker/Random.php:68
/home/giorgio/code/eris/src/Eris/Quantifier/ForAll.php:128
/home/giorgio/code/eris/src/Eris/Quantifier/Evaluation.php:53
/home/giorgio/code/eris/src/Eris/Quantifier/ForAll.php:130
/home/giorgio/code/eris/src/Eris/Quantifier/ForAll.php:158
/home/giorgio/code/eris/examples/SuchThatTest.php:47
/home/giorgio/code/eris/examples/SuchThatTest.php:47

Bind

Bind allows a Generator’s output to be used as an input to create another Generator. This composition allows to create several random values which are correlated with each other, by using the same input for their Generators parameters.

For example, here’s how to create a vector along with a random element chosen by it.

<?php
use Eris\Generator;

class BindTest extends PHPUnit_Framework_TestCase
{
    use Eris\TestTrait;

    public function testCreatingABrandNewGeneratorFromAGeneratedValueSingle()
    {
        $this->forAll(
            Generator\bind(
                Generator\vector(4, Generator\nat()),
                function ($vector) {
                    return Generator\tuple(
                        Generator\elements($vector),
                        Generator\constant($vector)
                    );
                }
            )
        )
            ->then(function ($tuple) {
                list($element, $vector) = $tuple;
                $this->assertContains($element, $vector);
            });
    }

    // TODO: multiple generators means multiple values passed to the
    // outer Generator factory
}