Expecto FsCheck getting stack overflow exception when generating string
Asked Answered
C

2

6

I'm trying to learn how to use FsCheck properly, and integrating it with Expecto at the moment. I can get property tests to run if I'm using the default FsCheck config, but when I try to use my own Generator, it causes a stack overflow exception.

Here is my generator

type NameGen() =
    static member Name() =
        Arb.generate<string * string>
        |> Gen.where (fun (firstName, lastName) ->
            firstName.Length > 0 && lastName.Length > 0
        )
        |> Gen.map (fun (first, last) -> sprintf "%s %s" first last)
        |> Arb.fromGen
        |> Arb.convert string id

And I'm trying to use it like this:

let config = { FsCheckConfig.defaultConfig with arbitrary = [typeof<NameGen>] }

let propertyTests input =
    let output = toInitials input
    output.EndsWith(".")

testPropertyWithConfig config "Must end with period" propertyTests

The exception is thrown before it even gets into the Gen.where function

What am I doing wrong? Thanks

Circassia answered 28/6, 2017 at 10:5 Comment(0)
H
6

You are trying to use FsCheck's generator of strings to redefine how its generator of strings work, but when you do that, it'll recursively call itself until it runs out of stack space. It's a known issue: https://github.com/fscheck/FsCheck/issues/109

Does this alternative work?

type NameGen =
    static member Name () =
        Arb.Default.NonEmptyString().Generator
        |> Gen.map (fun (NonEmptyString s) -> s)
        |> Gen.two
        |> Gen.map (fun (first, last) -> sprintf "%s %s" first last)
        |> Arb.fromGen
Hypermeter answered 28/6, 2017 at 10:56 Comment(0)
B
3

You are defining a new generator for the type string, but within that you're using the generator for string * string, which uses the generator for string. FsCheck unfortunately seems to store generators in global mutable state (perhaps with good reason?) and I think this means that the generator keeps calling itself until stack overflow.

You could solve this by defining the generator for a custom wrapper type instead of a plain string (shown below).

The next problem you'll hit will be null reference exceptions. The initial generated string could be null and you try to access the .Length property. This can be resolved using the String.length function instead, which returns 0 for null.

With these changes your generator looks like this:

type Name = Name of string

type NameGen() =
    static member Name() =
        Arb.generate<string * string>
        |> Gen.where (fun (firstName, lastName) ->
            String.length firstName > 0 && String.length lastName > 0
        )
        |> Gen.map (fun (first, last) -> sprintf "%s %s" first last)
        |> Arb.fromGen
        |> Arb.convert Name (fun (Name n) -> n)

And your property needs a slight modification:

let propertyTests (Name input) =
    let output = toInitials input
    output.EndsWith(".")
Burgrave answered 28/6, 2017 at 10:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.