3

I have recursive types like this:

type QueryInfo =
   { Title     : string
     Check     : Client -> bool
     Positive  : Decision
     Negative  : Decision }

 and Decision = 
    | Result of string
    | Query  of QueryInfo

I am going to make a generator for FsCheck. I have seen this and I am not interested in approaches like static member. My main problem is that every field has a different type.

FsCheck can already generate values of QueryInfo, but since the type is recursive, the generated values can become so deeply nested that generation of values effectively never stops (or, at least) is very slow.

Community
  • 1
  • 1
Sal-laS
  • 11,016
  • 25
  • 99
  • 169
  • Interesting, when I try to generate this using the built in reflective generators it actually stack overflows most of the time. I'll take a look at this. https://github.com/fscheck/FsCheck/issues/189 – Kurt Schelfthout Nov 25 '15 at 08:28

2 Answers2

4

Something like this ought to work:

let qi =
    let createQi t c p n = {
        Title = t
        Check = c
        Positive = p
        Negative = n }
    let makeQiGen =
        Gen.map4 createQi Arb.generate<string> Arb.generate<Client -> bool>
    let resultGen = Arb.generate<string> |> Gen.map Result
    let rec qi' size =
        if size <= 0
        then makeQiGen resultGen resultGen
        else
            let subQi = qi' (size - 1) |> Gen.map Query
            Gen.oneof [
                makeQiGen resultGen resultGen
                makeQiGen subQi subQi
                makeQiGen resultGen subQi
                makeQiGen subQi resultGen ]
    Gen.sized qi'

The essence is to prevent infinite (or very deep) recursions, which is done by Gen.sized.

When size reaches 0, the generator always returns a leaf node - that is: both Positive and Negative are Result values.

When size is greater than 0, the generators picks from one of four generators:

  • One that generates a leaf
  • One that generates a new node where both Positive and Negative are new Query values.
  • One where Positive is a Result, but Negative is a Query.
  • One where Positive is a Query, but Negative is a Result.

In each recursion, though, size is decremented, so eventually, a leaf node is returned.

This answer is based on the Generating recursive data types section of the FsCheck documentation.

Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
0

This is a generalized solution using generators and pattern matching, that can be combined with record creation (especially using the 'with' keyword):

#r @"../packages/FsCheck.2.1.0/lib/net45/FsCheck.dll"

module FactRepository = let Names = [ "A"; "B" ]

module DataGen =

    open FsCheck

    type DataTypes = ABool | ADate | Name

    let grabOne (gen:Gen<'a>) = gen.Sample(1, 10).Head
    let oneFromList list = grabOne (FsCheck.Gen.elements list)

    let generateName() = oneFromList FactRepository.Names
    let generateABool() = (Gen.oneof [ gen { return true }; gen { return false } ]).Sample(1, 1)
    let generateADate() = (Arb.generate<System.DateTime> |> Gen.sample 1 1).Head

    let makeValue (data : DataTypes) =
        match data with
        | Name -> generateName()
        | ABool -> generateABool().ToString()
        | ADate -> generateADate().ToString()

Update as per comments, to show record generation, taken from this thread:

fun (a,b,c) -> { myRecord.a = a; b = b; c = c }
        <!> (Arb.generate<float> |> Gen.suchThat ((<) 0.) |> Gen.three)
        |> Arb.fromGen
Community
  • 1
  • 1
  • First of all, i do not have **Date type**. Secondly, how does the **makevalue** function generate a record? – Sal-laS Nov 23 '15 at 21:53
  • The data types are examples showing how the technique can be applied - you've got a recursive data type so you want to look at [Generating recursive data types](https://fscheck.github.io/FsCheck/TestData.html) in the documentation. The link [you provided](http://stackoverflow.com/a/8237185/5470873) shows how to create a record with a generator, but you can use the functional approach above to create it without using 'static member' To create a record just create a record with the – Aaron Winston Comyn Nov 23 '15 at 22:12
  • This is not how you make a generator for FsCheck. At best this is an example of how to leverage FsCheck to generate a few random values, but it will not integrate well with writing tests using FsCheck. For example, the `rnd` instance is suspect (although it does not appear to be used) and you wouldn't be using `sample` at all but keep everything in the `Gen` type. – Kurt Schelfthout Nov 25 '15 at 08:15
  • Fair point, I think I read past the question title a bit quickly late at night :) I was, however, addressing the question as presented: "_... not interested in approaches like static member. My main problem is that every field has a different type... FsCheck can already generate values of QueryInfo..._". The titles question is thoroughly addressed in the (impressive!) linked documentation. This exact design problem plagued most our early FsCheck trials. We circumvented explicitly it by using alternative approaches like the example. Our use case was data gen, btw, not unit testing. – Aaron Winston Comyn Nov 25 '15 at 13:13