giorgiosironi/eris

Composite generators in bind cause type error when shrinking

Closed this issue · 3 comments

Vinai commented

This is the simplest example I could come up with that reproduces the error:

$this->forAll(
    Generator\bind(
        Generator\choose(1, 10),
        function ($x) {
            return Generator\tuple($x);
        } 
    )
)->then(function ($tuple) {
    $this->assertTrue(false, var_export($tuple, true));
});

The error is:

Error: Argument 1 passed to Eris\Generator\TupleGenerator::shrink()
must be an instance of Eris\Generator\GeneratedValueSingle,
instance of Eris\Generator\GeneratedValueOptions given,
called in /home/me/project/vendor/giorgiosironi/eris/src/Generator/BindGenerator.php on line 42

It seems to happen with composite generators being returned from inside of a bind generator function.
I've tried with tuple and associative.

In practice a GeneratedValueOptions should work in shrink, too, since the GeneratedValueOptions instance also has a input method, but I'm not sure how the type signature should be fixed.
The \Eris\Generator\TupleGenerator::shrink(GeneratedValueSingle $element) signature can't be changed because it has to match \Eris\Generator::shrink(GeneratedValueSingle $element).

Maybe if the argument type where changed to \Eris\Generator\GeneratedValue instead, and that interface then implemented the input method?

That would be a backward compatibility breaking change, but at the moment I'm not sure how to build generators that are related to other generated values without using composite generators with bind.

Vinai commented

As a workaround (a.k.a. hacky patch), I've added the following to the \Eris\Generator\BindGenerator::shrink method:

        while ($outerGeneratorValue instanceof GeneratedValueOptions) {
            $outerGeneratorValue = $outerGeneratorValue->last();
        }

Now the bind generator work as expected.
This is the complete shrink method:

    public function shrink(GeneratedValueSingle $element)
    {
        list($outerGeneratorValue, $innerGeneratorValue) = $element->input();
        // TODO: shrink also the second generator
        $outerGenerator = call_user_func($this->outerGeneratorFactory, $innerGeneratorValue->unbox());
        while ($outerGeneratorValue instanceof GeneratedValueOptions) {
            $outerGeneratorValue = $outerGeneratorValue->last();
        }
        $shrinkedOuterGeneratorValue = $outerGenerator->shrink($outerGeneratorValue);
        return $this->packageGeneratedValueSingle(
            $shrinkedOuterGeneratorValue,
            $innerGeneratorValue
        );
    }

I have no idea if I broke something else by adding this line... It doesn't seem like the right fix. Just thought I'd add the info here because it might be helpful.

Vinai commented

I've narrowed it down a bit. The error happens on the second shrink. It can be reproduced with the following test (in \Eris\Generator\BindGeneratorTest):

    public function testShrinkBindGeneratorWithCompositeValue()
    {
        $bindGenerator     = new BindGenerator(
            new ChooseGenerator(0, 5),
            function ($n) {
                return new TupleGenerator([$n]);
            }
        );
        $generatedValue    = $bindGenerator->__invoke(10, new RandomRange(new RandSource()));
        $firstShrunkValue  = $bindGenerator->shrink($generatedValue);
        $secondShrunkValue = $bindGenerator->shrink($firstShrunkValue);
        $this->assertInstanceOf('\Eris\Generator\GeneratedValue', $secondShrunkValue);
    }

From what I can see the problem is that according to the return type hint \Eris\Generator::shrink() returns either a GeneratedValueSingle<T> or a GeneratedValueOptions<T>, but the method argument allows onlyGeneratedValueSingle instances.
This breaks shrinking for generators that indeed return GeneratedValueOptions.

I assume that #127 fixes this. Feel free to reopen otherwise.