Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to generate DU cases? #133

Closed
cmeeren opened this issue Oct 11, 2017 · 23 comments
Closed

How to generate DU cases? #133

cmeeren opened this issue Oct 11, 2017 · 23 comments
Labels

Comments

@cmeeren
Copy link
Contributor

cmeeren commented Oct 11, 2017

How can I generate discriminated union cases? FsCheck handles this automatically, and I can't see any methods in Hedgehog that could easily allow me to do that. The only thing I can come up with is to create generators for each case of every DU, and then use Gen.item or similar to pick one of the generators. (In the case of a very simple DU with little or no data, only case labels, Gen.item could be used directly.)

This has the drawback that if I expand the DU, I have to manually add the new cases to the generator.

@moodmosaic
Copy link
Member

You can find an example here for Gen.item but, as you correctly stated, you have to remember to update it each time a new case gets added.

Instead, what you can do is write a reflection-based generator, and that should handle this, as well as #123. Such a reflection-based generator may be fairly easy to write with TypeShape, as I wrote in #93 (comment).


Perhaps, if we come up with a fancy, battle-tested, reflection-based generator, we can consider releasing it as a separate NuGet Package so that other users can benefit from it.

@cmeeren
Copy link
Contributor Author

cmeeren commented Oct 11, 2017

I would love absolutely that. Unfortunately my F# skills aren't up to that task, so I can only hope.

@moodmosaic
Copy link
Member

(This is the example I'm talking about in #93 (comment), in case it's TL;DR.)

@moodmosaic
Copy link
Member

I would love absolutely that.

👍

I'll see if I can come up with a draft where I can paste it here so you may use it sooner.

@moodmosaic
Copy link
Member

moodmosaic commented Oct 12, 2017

Here's something that may actually work! You may add more cases below the match expression.

//
// TypeShape-driven Hedgehog generator
//
// Credits: http://www.fssnip.net/7RA/title/Generic-POCO-generators-for-FsCheck
//

open Hedgehog
open TypeShape

module Gen =
    let rec mkGenerator<'T> () : Gen<'T> =
        let wrap (t : Gen<'a>) =
            unbox<Gen<'T>> t
    
        let mkRandomMember (shape : IShapeWriteMember<'DeclaringType>) = 
            shape.Accept {
                new IWriteMemberVisitor<'DeclaringType, Gen<'DeclaringType -> 'DeclaringType>> with
                    member this.Visit(shape : ShapeWriteMember<'DeclaringType, 'Field>) = 
                        let rf = mkGenerator<'Field>()
                        gen { let! f = rf
                              return fun dt -> shape.Inject dt f } }

        match TypeShape.Create<'T>() with
        | Shape.Byte           -> wrap (Gen.byte  <| Range.linearBounded ())
        | Shape.Int16          -> wrap (Gen.int16 <| Range.linear -1000s 1000s)
        | Shape.Int32          -> wrap (Gen.int   <| Range.linear -1000  1000 )
        | Shape.Int64          -> wrap (Gen.int64 <| Range.linear -1000L 1000L)
        
        | Shape.Double         -> wrap (Gen.double <|  Range.linear -1000.0 1000.0)
        | Shape.Decimal        -> wrap (Gen.double <| (Range.linear -1000.0 1000.0) |> Gen.map decimal)
        
        | Shape.Bool           -> wrap Gen.bool
        | Shape.Guid           -> wrap Gen.guid
        | Shape.Char           -> wrap Gen.unicodeAll
        | Shape.DateTime       -> wrap Gen.dateTime

        | Shape.Unit           -> wrap (Gen.constant ())

        | Shape.String         -> wrap (Gen.string (Range.constant -1000 1000) Gen.alphaNum)
        | Shape.DateTimeOffset -> wrap (Gen.map System.DateTimeOffset Gen.dateTime)

        | Shape.FSharpOption s -> 
            s.Accept {
                new IFSharpOptionVisitor<Gen<'T>> with
                    member this.Visit<'t>() = 
                        let tGen = mkGenerator<'t> ()
                        Gen.frequency [ (10, tGen |> Gen.map Some)
                                        (1, gen { return None }) ]
                        |> wrap }

        | Shape.Array s when s.Rank = 1 -> 
            s.Accept { 
                new IArrayVisitor<Gen<'T>> with
                    member this.Visit<'t> _ = 
                        let tG = mkGenerator<'t> ()
                        gen { 
                            let! length = Gen.sized (fun n -> Gen.integral <| Range.constant -1 n)
                            match length with
                            | -1 -> return null
                            | _  -> 
                                let array = Array.zeroCreate<'t> length
                                for i = 0 to array.Length - 1 do
                                    let! t = tG
                                    array.[i] <- t
                                return array
                        }
                        |> wrap }

        | Shape.FSharpList s -> 
            s.Accept {
                new IFSharpListVisitor<Gen<'T>> with
                    member this.Visit<'t> () = 
                        let tG = mkGenerator<'t> ()
                        gen { 
                            let! length =
                                Gen.sized (fun n -> Gen.integral <| Range.constant -1 n)
                            let rec aux acc n = 
                                gen { 
                                    if n = 0 then return acc
                                    else let! t = tG
                                         return! aux (t :: acc) (n - 1)
                                }
                            return! aux [] length
                        }
                        |> wrap }

        | Shape.FSharpSet s -> 
            s.Accept {
                new IFSharpSetVisitor<Gen<'T>> with
                    member this.Visit<'t when 't : comparison> () = 
                        let tG = mkGenerator<'t list> ()
                        wrap (tG |> Gen.map Set.ofList) }

        | Shape.FSharpMap s -> 
            s.Accept {
                new IFSharpMapVisitor<Gen<'T>> with
                    member this.Visit<'k, 'v when 'k : comparison> () = 
                        let kvG = mkGenerator<('k * 'v) list> ()
                        wrap (kvG |> Gen.map Map.ofList) }

        | Shape.Tuple (:? ShapeTuple<'T> as shape) -> 
            let eGens =
                shape.Elements
                |> Array.map mkRandomMember

            gen { 
                let mutable target = shape.CreateUninitialized ()
                for eg in eGens do
                    let! u = eg
                    target <- u target
                return target
            }

        | Shape.FSharpRecord (:? ShapeFSharpRecord<'T> as shape) -> 
            let fieldGen =
                shape.Fields
                |> Array.map mkRandomMember

            gen { 
                let mutable target = shape.CreateUninitialized ()
                for eg in fieldGen do
                    let! u = eg
                    target <- u target
                return target
            }

        | Shape.FSharpUnion (:? ShapeFSharpUnion<'T> as shape) -> 
            let caseFieldGen =
                shape.UnionCases
                |> Array.map (fun uc -> uc.Fields |> Array.map mkRandomMember)

            gen { 
                let! tag = Gen.integral <| Range.constant 0 (caseFieldGen.Length - 1)
                let mutable u = shape.UnionCases.[tag].CreateUninitialized ()
                for f in caseFieldGen.[tag] do
                    let! uf = f
                    u <- uf u
                return u
            }

        | Shape.CliMutable (:? ShapeCliMutable<'T> as shape) -> 
            let propGen = shape.Properties |> Array.map mkRandomMember
            gen { 
                let mutable target = shape.CreateUninitialized ()
                for ep in propGen do
                    let! up = ep
                    target <- up target
                return target
            }

        | Shape.Poco (:? ShapePoco<'T> as shape) -> 
            let bestCtor = 
                shape.Constructors
                |> Seq.filter  (fun c -> c.IsPublic)
                |> Seq.sortBy  (fun c -> c.Arity)
                |> Seq.tryFind (fun _ -> true)

            match bestCtor with
            | None -> failwithf "Class %O lacking an appropriate ctor" typeof<'T>
            | Some ctor -> 
                ctor.Accept {
                    new IConstructorVisitor<'T, Gen<'T>> with
                        member this.Visit<'CtorParams> (ctor : ShapeConstructor<'T, 'CtorParams>) = 
                            let paramGen = mkGenerator<'CtorParams> ()
                            gen { let! args = paramGen
                                return ctor.Invoke args } }

        | _ -> raise (System.NotSupportedException ())

@moodmosaic
Copy link
Member

Here's an example (notice that you do get also shrinking for free):

type Shape = 
    | Rectangle of height : float * width : float
    | Circle    of radius : float

Gen.printSample <| Gen.mkGenerator<Shape> ();;

=== Outcome ===
Rectangle (-911.0102007,-830.9254441)
=== Shrinks ===
Rectangle (-1000.0,-830.9254441)
Rectangle (-955.5051004,-830.9254441)
Rectangle (-933.2576505,-830.9254441)
Rectangle (-922.1339256,-830.9254441)
Rectangle (-916.5720632,-830.9254441)
Rectangle (-913.791132,-830.9254441)
Rectangle (-912.4006663,-830.9254441)
Rectangle (-911.7054335,-830.9254441)
Rectangle (-911.3578171,-830.9254441)
Rectangle (-911.1840089,-830.9254441)
Rectangle (-911.0971048,-830.9254441)
Rectangle (-911.0536528,-830.9254441)
Rectangle (-911.0319267,-830.9254441)
Rectangle (-911.0210637,-830.9254441)
Rectangle (-911.0156322,-830.9254441)
Rectangle (-911.0129165,-830.9254441)
Rectangle (-911.0115586,-830.9254441)
Rectangle (-911.0108797,-830.9254441)
Rectangle (-911.0105402,-830.9254441)
Rectangle (-911.0103705,-830.9254441)
Rectangle (-911.0102856,-830.9254441)
Rectangle (-911.0102432,-830.9254441)
Rectangle (-911.0102219,-830.9254441)
Rectangle (-911.0102113,-830.9254441)
Rectangle (-911.010206,-830.9254441)
Rectangle (-911.0102034,-830.9254441)
Rectangle (-911.010202,-830.9254441)
Rectangle (-911.0102014,-830.9254441)
Rectangle (-911.0102011,-830.9254441)
Rectangle (-911.0102009,-830.9254441)
Rectangle (-911.0102008,-830.9254441)
Rectangle (-911.0102008,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-1000.0)
Rectangle (-911.0102007,-915.4627221)
Rectangle (-911.0102007,-873.1940831)
Rectangle (-911.0102007,-852.0597636)
Rectangle (-911.0102007,-841.4926039)
Rectangle (-911.0102007,-836.209024)
Rectangle (-911.0102007,-833.5672341)
Rectangle (-911.0102007,-832.2463391)
Rectangle (-911.0102007,-831.5858916)
Rectangle (-911.0102007,-831.2556679)
Rectangle (-911.0102007,-831.090556)
Rectangle (-911.0102007,-831.0080001)
Rectangle (-911.0102007,-830.9667221)
Rectangle (-911.0102007,-830.9460831)
Rectangle (-911.0102007,-830.9357636)
Rectangle (-911.0102007,-830.9306039)
Rectangle (-911.0102007,-830.928024)
Rectangle (-911.0102007,-830.9267341)
Rectangle (-911.0102007,-830.9260891)
Rectangle (-911.0102007,-830.9257666)
Rectangle (-911.0102007,-830.9256054)
Rectangle (-911.0102007,-830.9255247)
Rectangle (-911.0102007,-830.9254844)
Rectangle (-911.0102007,-830.9254643)
Rectangle (-911.0102007,-830.9254542)
Rectangle (-911.0102007,-830.9254492)
Rectangle (-911.0102007,-830.9254466)
Rectangle (-911.0102007,-830.9254454)
Rectangle (-911.0102007,-830.9254447)
Rectangle (-911.0102007,-830.9254444)
Rectangle (-911.0102007,-830.9254443)
Rectangle (-911.0102007,-830.9254442)
Rectangle (-911.0102007,-830.9254442)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
Rectangle (-911.0102007,-830.9254441)
.
=== Outcome ===
Circle -907.2173144
=== Shrinks ===
Rectangle (-907.2173144,-804.4497323)
Circle -1000.0
Circle -953.6086572
Circle -930.4129858
Circle -918.8151501
Circle -913.0162322
Circle -910.1167733
Circle -908.6670438
Circle -907.9421791
Circle -907.5797467
Circle -907.3985305
Circle -907.3079224
Circle -907.2626184
Circle -907.2399664
Circle -907.2286404
Circle -907.2229774
Circle -907.2201459
Circle -907.2187301
Circle -907.2180222
Circle -907.2176683
Circle -907.2174913
Circle -907.2174028
Circle -907.2173586
Circle -907.2173365
Circle -907.2173254
Circle -907.2173199
Circle -907.2173171
Circle -907.2173157
Circle -907.217315
Circle -907.2173147
Circle -907.2173145
Circle -907.2173144
Circle -907.2173144
Circle -907.2173144
Circle -907.2173144
Circle -907.2173144
Circle -907.2173144
Circle -907.2173144
Circle -907.2173144
Circle -907.2173144
Circle -907.2173144
Circle -907.2173144
Circle -907.2173144
Circle -907.2173144
Circle -907.2173144
Circle -907.2173144
Circle -907.2173144
Circle -907.2173144
Circle -907.2173144
Circle -907.2173144
Circle -907.2173144
Circle -907.2173144
.
=== Outcome ===
Circle -982.6691089
=== Shrinks ===
Rectangle (-982.6691089,-910.8182287)
Circle -1000.0
Circle -991.3345544
Circle -987.0018316
Circle -984.8354703
Circle -983.7522896
Circle -983.2106992
Circle -982.939904
Circle -982.8045064
Circle -982.7368077
Circle -982.7029583
Circle -982.6860336
Circle -982.6775712
Circle -982.67334
Circle -982.6712245
Circle -982.6701667
Circle -982.6696378
Circle -982.6693733
Circle -982.6692411
Circle -982.669175
Circle -982.6691419
Circle -982.6691254
Circle -982.6691171
Circle -982.669113
Circle -982.6691109
Circle -982.6691099
Circle -982.6691094
Circle -982.6691091
Circle -982.669109
Circle -982.6691089
Circle -982.6691089
Circle -982.6691089
Circle -982.6691089
Circle -982.6691089
Circle -982.6691089
Circle -982.6691089
Circle -982.6691089
Circle -982.6691089
Circle -982.6691089
Circle -982.6691089
Circle -982.6691089
Circle -982.6691089
Circle -982.6691089
Circle -982.6691089
Circle -982.6691089
Circle -982.6691089
Circle -982.6691089
Circle -982.6691089
Circle -982.6691089
Circle -982.6691089
.
=== Outcome ===
Rectangle (-847.9604826,-845.8164013)
=== Shrinks ===
Rectangle (-1000.0,-845.8164013)
Rectangle (-923.9802413,-845.8164013)
Rectangle (-885.970362,-845.8164013)
Rectangle (-866.9654223,-845.8164013)
Rectangle (-857.4629525,-845.8164013)
Rectangle (-852.7117175,-845.8164013)
Rectangle (-850.3361001,-845.8164013)
Rectangle (-849.1482914,-845.8164013)
Rectangle (-848.554387,-845.8164013)
Rectangle (-848.2574348,-845.8164013)
Rectangle (-848.1089587,-845.8164013)
Rectangle (-848.0347207,-845.8164013)
Rectangle (-847.9976017,-845.8164013)
Rectangle (-847.9790421,-845.8164013)
Rectangle (-847.9697624,-845.8164013)
Rectangle (-847.9651225,-845.8164013)
Rectangle (-847.9628026,-845.8164013)
Rectangle (-847.9616426,-845.8164013)
Rectangle (-847.9610626,-845.8164013)
Rectangle (-847.9607726,-845.8164013)
Rectangle (-847.9606276,-845.8164013)
Rectangle (-847.9605551,-845.8164013)
Rectangle (-847.9605189,-845.8164013)
Rectangle (-847.9605008,-845.8164013)
Rectangle (-847.9604917,-845.8164013)
Rectangle (-847.9604872,-845.8164013)
Rectangle (-847.9604849,-845.8164013)
Rectangle (-847.9604838,-845.8164013)
Rectangle (-847.9604832,-845.8164013)
Rectangle (-847.9604829,-845.8164013)
Rectangle (-847.9604828,-845.8164013)
Rectangle (-847.9604827,-845.8164013)
Rectangle (-847.9604827,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-1000.0)
Rectangle (-847.9604826,-922.9082006)
Rectangle (-847.9604826,-884.362301)
Rectangle (-847.9604826,-865.0893511)
Rectangle (-847.9604826,-855.4528762)
Rectangle (-847.9604826,-850.6346387)
Rectangle (-847.9604826,-848.22552)
Rectangle (-847.9604826,-847.0209607)
Rectangle (-847.9604826,-846.418681)
Rectangle (-847.9604826,-846.1175411)
Rectangle (-847.9604826,-845.9669712)
Rectangle (-847.9604826,-845.8916862)
Rectangle (-847.9604826,-845.8540438)
Rectangle (-847.9604826,-845.8352225)
Rectangle (-847.9604826,-845.8258119)
Rectangle (-847.9604826,-845.8211066)
Rectangle (-847.9604826,-845.8187539)
Rectangle (-847.9604826,-845.8175776)
Rectangle (-847.9604826,-845.8169895)
Rectangle (-847.9604826,-845.8166954)
Rectangle (-847.9604826,-845.8165483)
Rectangle (-847.9604826,-845.8164748)
Rectangle (-847.9604826,-845.816438)
Rectangle (-847.9604826,-845.8164197)
Rectangle (-847.9604826,-845.8164105)
Rectangle (-847.9604826,-845.8164059)
Rectangle (-847.9604826,-845.8164036)
Rectangle (-847.9604826,-845.8164024)
Rectangle (-847.9604826,-845.8164019)
Rectangle (-847.9604826,-845.8164016)
Rectangle (-847.9604826,-845.8164014)
Rectangle (-847.9604826,-845.8164014)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
Rectangle (-847.9604826,-845.8164013)
.
=== Outcome ===
Rectangle (-842.0160974,-850.9816042)
=== Shrinks ===
Rectangle (-1000.0,-850.9816042)
Rectangle (-921.0080487,-850.9816042)
Rectangle (-881.512073,-850.9816042)
Rectangle (-861.7640852,-850.9816042)
Rectangle (-851.8900913,-850.9816042)
Rectangle (-846.9530943,-850.9816042)
Rectangle (-844.4845959,-850.9816042)
Rectangle (-843.2503466,-850.9816042)
Rectangle (-842.633222,-850.9816042)
Rectangle (-842.3246597,-850.9816042)
Rectangle (-842.1703785,-850.9816042)
Rectangle (-842.093238,-850.9816042)
Rectangle (-842.0546677,-850.9816042)
Rectangle (-842.0353825,-850.9816042)
Rectangle (-842.02574,-850.9816042)
Rectangle (-842.0209187,-850.9816042)
Rectangle (-842.018508,-850.9816042)
Rectangle (-842.0173027,-850.9816042)
Rectangle (-842.0167001,-850.9816042)
Rectangle (-842.0163987,-850.9816042)
Rectangle (-842.0162481,-850.9816042)
Rectangle (-842.0161727,-850.9816042)
Rectangle (-842.0161351,-850.9816042)
Rectangle (-842.0161162,-850.9816042)
Rectangle (-842.0161068,-850.9816042)
Rectangle (-842.0161021,-850.9816042)
Rectangle (-842.0160997,-850.9816042)
Rectangle (-842.0160986,-850.9816042)
Rectangle (-842.016098,-850.9816042)
Rectangle (-842.0160977,-850.9816042)
Rectangle (-842.0160975,-850.9816042)
Rectangle (-842.0160975,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-1000.0)
Rectangle (-842.0160974,-925.4908021)
Rectangle (-842.0160974,-888.2362031)
Rectangle (-842.0160974,-869.6089037)
Rectangle (-842.0160974,-860.2952539)
Rectangle (-842.0160974,-855.6384291)
Rectangle (-842.0160974,-853.3100166)
Rectangle (-842.0160974,-852.1458104)
Rectangle (-842.0160974,-851.5637073)
Rectangle (-842.0160974,-851.2726557)
Rectangle (-842.0160974,-851.12713)
Rectangle (-842.0160974,-851.0543671)
Rectangle (-842.0160974,-851.0179856)
Rectangle (-842.0160974,-850.9997949)
Rectangle (-842.0160974,-850.9906995)
Rectangle (-842.0160974,-850.9861519)
Rectangle (-842.0160974,-850.983878)
Rectangle (-842.0160974,-850.9827411)
Rectangle (-842.0160974,-850.9821726)
Rectangle (-842.0160974,-850.9818884)
Rectangle (-842.0160974,-850.9817463)
Rectangle (-842.0160974,-850.9816752)
Rectangle (-842.0160974,-850.9816397)
Rectangle (-842.0160974,-850.9816219)
Rectangle (-842.0160974,-850.9816131)
Rectangle (-842.0160974,-850.9816086)
Rectangle (-842.0160974,-850.9816064)
Rectangle (-842.0160974,-850.9816053)
Rectangle (-842.0160974,-850.9816047)
Rectangle (-842.0160974,-850.9816045)
Rectangle (-842.0160974,-850.9816043)
Rectangle (-842.0160974,-850.9816043)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
Rectangle (-842.0160974,-850.9816042)
.

@moodmosaic
Copy link
Member

Classes (types that represent objects that can have properties, methods, and events) work too:

type Person (name : string, age : int) = 
    member this.Name = name
    member this.Age  = age
    override this.ToString() =
        sprintf "{ Name = \"%s\"; Age = %d }" this.Name this.Age

Gen.printSample <| Gen.mkGenerator<Person> ();;

[..]

@cmeeren
Copy link
Contributor Author

cmeeren commented Oct 12, 2017

That's absolutely lovely! A couple of questions:

  1. Is there any reason why numbers should be limited to -1000, 1000? I'd suggest Range.exponentialBounded; that way, you get a lot of small values and also some really big ones.

  2. Shrinking should be towards 0, not -1000 (or whatever the lower limit is)

  3. My gut says it's a bit weird that DU cases can shrink to another DU case. Is that intended?

  4. Sometimes, you'd want to generate, say, a complex type where its string members (recursively in case of complex-typed members) should not be unicodeAll but rather alphaNum, or where int members should only be even. It would be fantastic to let you be able to override generators for specific types in the call to mkGenerator.

@moodmosaic
Copy link
Member

moodmosaic commented Oct 12, 2017

@cmeeren, you are free to modify this based on your scenarios. Nothing stops you from changing the ranges I've used in that example.

@moodmosaic
Copy link
Member

moodmosaic commented Oct 12, 2017

@cmeeren, I'll place this on a GitHub Gist, so we can take advantage of the versioning feature it has. Then we can tweak mkGenerator based on your feedback.

@moodmosaic
Copy link
Member

@cmeeren, I'll have to look at this closer, in order to make sure it's not some weird side-effect of mkGenerator.

@cmeeren
Copy link
Contributor Author

cmeeren commented Oct 12, 2017

Sounds great. If auto-generation like this can work, and the speed issues from #135 are fixed, then I'll be a very happy Hedgehog user 😄

@cmeeren
Copy link
Contributor Author

cmeeren commented Oct 12, 2017

There seems to be a bug in the list generator definition. The length can be -1, but this is not handled anywhere.

I'm also of two minds whether the generator should by default generate nulls for e.g. arrays. Mostly, when programming in F#, nulls aren't a problem. In most cases, I'd guess the user would not want nulls to be generated, because they're testing code that never gets a null. On the other hand, when they actually do test code that needs to handle null, it would be safer to have that generated automatically (in case they forget to specify generation of nulls).

@moodmosaic
Copy link
Member

Feel free to tweak mkGenerator based on your scenario(s).

If you need to be able to 'override' the existing generators (say, the one for strings), perhaps you can modify mkGenerator so that it takes a dictionary of x -> Gen<'x> or similar.

Then, inside the match expression you would first check if there's an entry on that dictionary, and if not, you'd fallback to the existing generators in that function.

If you come up with a battle-tested version of mkGenerator we can definitely consider putting it on a NuGet Package, if you agree.


Closing this, but feel free to add as many comments as you want. Your feedback is invaluable 🚀

@cmeeren
Copy link
Contributor Author

cmeeren commented Oct 12, 2017

Thanks. I'm still going a bit back and forth between Hedgehog and FsCheck. If I fall down on Hedgehog I will likely have a closer look at this - currently the speed issue from #135 is blocking any attempts at a full adoption of Hedgehog on my part, and if FsCheck also implements integrated shrinking (fscheck/FsCheck#403), then I might still end up with FsCheck in the long run. We'll see. :)

@moodmosaic
Copy link
Member

moodmosaic commented Oct 12, 2017

currently the speed issue from #135 is blocking any attempts at a full adoption of Hedgehog

If your test suite runs in 10 seconds or less, you should be fine until we're done with #135. Otherwise perhaps you should probably take the type-based shrinking route via FsCheck or similar tool(s).

If FsCheck also implements integrated shrinking

I can imagine this being implemented à la smartcheck, and then I'll be curious to see how it performs.

@cmeeren
Copy link
Contributor Author

cmeeren commented Oct 12, 2017

If your test suite runs in 10 seconds or less

It did with FsCheck, but after converting a few of the more generation-heavy tests to Hedgehog, it decidedly does not. Some individual tests take almost a full second to run.

@cmeeren
Copy link
Contributor Author

cmeeren commented Oct 16, 2017

If you need to be able to 'override' the existing generators (say, the one for strings), perhaps you can modify mkGenerator so that it takes a dictionary of x -> Gen<'x> or similar.

I can't seem to figure out how to create a map with types as keys. For example, I tried the following:

Map.empty.Add(typeof<bool>, Gen.bool)

But I get an error saying that System.Type do not support the comparison constraint. How is it possible to create the kind of dictionary you refer to?

@moodmosaic
Copy link
Member

moodmosaic commented Oct 16, 2017

Here's a quick draft of what I had in mind in #133 (comment):

open System
open System.Collections.Generic
open Hedgehog

let localGens = Dictionary<_, _> ()
localGens.Add (typeof<bool>, box Gen.bool)
localGens.Add (typeof<char>, box Gen.unicodeAll)

let get<'a> (d : Dictionary<_, _>) : Gen<'a> =
    unbox<Gen<'a>> d.[typeof<'a>]

Examples:

let gnBool = localGens |> get<bool>
let gnChar = localGens |> get<char>

@cmeeren
Copy link
Contributor Author

cmeeren commented Oct 16, 2017

I've had a look at the whole shebang now and here's my suggestion (not extensively tested, but seems to work).

In addition to the overridable generators, I've made use of the already existing Gen.option, Gen.list, etc. to create relevant types. IMHO this makes things more predictable (and shortens the code). I've also changed the ranges to what I think makes the most sense.

I think that in general, auto-generation and generator overriding should be used for fairly simple purposes. For more complex scenarios, custom generators should be defined as per now. IMHO the biggest benefit to auto-generation is in simple DU types. From my (very limited) experience, more complex types would likely need a more controlled generator anyway (both for correctness and performance).

Usage examples:

gen {
  // Using defaults
  let! myTypeValue = Gen.auto<MyType>()

  // Overriding
  let! myTypeValue = Gen.auto'<MyType>({Gen.defaults with Char = Gen.alphaNum})
}

The whole implementation:

open Hedgehog
open TypeShape

module Gen =

  type AutoGenConfig =
    {Byte: Gen<byte>
     Int16: Gen<int16>
     Int: Gen<int>
     Int64: Gen<int64>
     Double: Gen<double>
     Decimal: Gen<decimal>
     Bool: Gen<bool>
     Guid: Gen<System.Guid>
     Char: Gen<System.Char>
     DateTime: Gen<System.DateTime>
     String: Gen<System.String>
     DateTimeOffset: Gen<System.DateTimeOffset>
     SeqRange: Range<int> // range for lists, arrays, etc.
     }

  let defaults =
    {Byte = Gen.byte <| Range.exponentialBounded()
     Int16 = Gen.int16 <| Range.exponentialBounded()
     Int = Gen.int <| Range.exponentialBounded()
     Int64 = Gen.int64 <| Range.exponentialBounded()
     Double = Gen.double <| Range.exponentialBounded()
     Decimal = Gen.double <| Range.exponentialBounded() |> Gen.map decimal
     Bool = Gen.bool
     Guid = Gen.guid
     Char = Gen.latin1
     DateTime = Gen.dateTime
     String = Gen.string (Range.linear 0 50) Gen.latin1
     DateTimeOffset = Gen.dateTime |> Gen.map System.DateTimeOffset
     SeqRange = Range.exponential 0 50}

  let rec auto'<'T> (config:AutoGenConfig) : Gen<'T> =
    let wrap (t : Gen<'a>) =
      unbox<Gen<'T>> t
  
    let mkRandomMember (shape : IShapeWriteMember<'DeclaringType>) = 
      shape.Accept {
        new IWriteMemberVisitor<'DeclaringType, Gen<'DeclaringType -> 'DeclaringType>> with
          member __.Visit(shape : ShapeWriteMember<'DeclaringType, 'Field>) = 
            let rf = auto'<'Field>(config)
            gen { let! f = rf
                  return fun dt -> shape.Inject dt f } }

    match TypeShape.Create<'T>() with
    | Shape.Byte -> wrap config.Byte
    | Shape.Int16 -> wrap config.Int16
    | Shape.Int32 -> wrap config.Int
    | Shape.Int64 -> wrap config.Int64
    
    | Shape.Double -> wrap config.Double
    | Shape.Decimal -> wrap config.Decimal
    
    | Shape.Bool -> wrap config.Bool
    | Shape.Guid -> wrap config.Guid
    | Shape.Char -> wrap config.Char
    | Shape.DateTime -> wrap config.DateTime

    | Shape.Unit -> wrap (Gen.constant ())

    | Shape.String -> wrap config.String
    | Shape.DateTimeOffset -> wrap config.DateTimeOffset

    | Shape.FSharpOption s -> 
      s.Accept {
        new IFSharpOptionVisitor<Gen<'T>> with
          member __.Visit<'t>() = 
            auto'<'t> config |> Gen.option |> wrap }

    | Shape.Array s when s.Rank = 1 -> 
      s.Accept { 
        new IArrayVisitor<Gen<'T>> with
          member __.Visit<'t> _ = 
            auto'<'t> config |> Gen.array config.SeqRange |> wrap }

    | Shape.Array _ -> 
      raise (System.NotSupportedException("Can only generate arrays of rank 1"))

    | Shape.FSharpList s -> 
      s.Accept {
        new IFSharpListVisitor<Gen<'T>> with
          member __.Visit<'t> () = 
            auto'<'t> config |> Gen.list config.SeqRange |> wrap }

    | Shape.FSharpSet s -> 
      s.Accept {
        new IFSharpSetVisitor<Gen<'T>> with
          member __.Visit<'t when 't : comparison> () = 
            auto'<'t list> config 
            |> Gen.map Set.ofList 
            |> wrap }

    | Shape.FSharpMap s -> 
      s.Accept {
        new IFSharpMapVisitor<Gen<'T>> with
          member __.Visit<'k, 'v when 'k : comparison> () = 
            auto'<('k * 'v) list> config
            |> Gen.map Map.ofList
            |> wrap }

    | Shape.Tuple (:? ShapeTuple<'T> as shape) -> 
      let eGens =
        shape.Elements
        |> Array.map mkRandomMember

      gen { 
        let mutable target = shape.CreateUninitialized ()
        for eg in eGens do
          let! u = eg
          target <- u target
        return target
      }

    | Shape.FSharpRecord (:? ShapeFSharpRecord<'T> as shape) -> 
      let fieldGen =
        shape.Fields
        |> Array.map mkRandomMember

      gen { 
        let mutable target = shape.CreateUninitialized ()
        for eg in fieldGen do
          let! u = eg
          target <- u target
        return target
      }

    | Shape.FSharpUnion (:? ShapeFSharpUnion<'T> as shape) -> 
      let caseFieldGen =
        shape.UnionCases
        |> Array.map (fun uc -> uc.Fields |> Array.map mkRandomMember)

      gen { 
        let! tag = Gen.integral <| Range.constant 0 (caseFieldGen.Length - 1)
        let mutable u = shape.UnionCases.[tag].CreateUninitialized ()
        for f in caseFieldGen.[tag] do
          let! uf = f
          u <- uf u
        return u
      }

    | Shape.CliMutable (:? ShapeCliMutable<'T> as shape) -> 
      let propGen = shape.Properties |> Array.map mkRandomMember
      gen { 
        let mutable target = shape.CreateUninitialized ()
        for ep in propGen do
          let! up = ep
          target <- up target
        return target
      }

    | Shape.Poco (:? ShapePoco<'T> as shape) -> 
      let bestCtor = 
        shape.Constructors
        |> Seq.filter  (fun c -> c.IsPublic)
        |> Seq.sortBy  (fun c -> c.Arity)
        |> Seq.tryHead

      match bestCtor with
      | None -> failwithf "Class %O lacking an appropriate ctor" typeof<'T>
      | Some ctor -> 
        ctor.Accept {
          new IConstructorVisitor<'T, Gen<'T>> with
            member __.Visit<'CtorParams> (ctor : ShapeConstructor<'T, 'CtorParams>) = 
              let paramGen = auto'<'CtorParams> (config)
              gen { let! args = paramGen
                return ctor.Invoke args } }

    | _ -> raise (System.NotSupportedException ())

  let auto<'T>() = auto'<'T>(defaults)

@moodmosaic
Copy link
Member

Usage examples:

gen {
 // Using defaults
 let! myTypeValue = Gen.auto<MyType>()

 // Overriding
 let! myTypeValue = Gen.auto'<MyType>({Gen.defaults with Char = Gen.alphaNum})
}

This looks nice 🚀

@cmeeren
Copy link
Contributor Author

cmeeren commented Oct 17, 2017

Actually, the auto and auto' functions do not need type arguments. In fact, as I have defined it, the auto function will, when used with an explicit type argument, give the following warning:

The method or function 'auto' should not be given explicit type argument(s) because it does not declare its type parameters explicitly

So the usage is simply

gen {
 // Using defaults
 let! x = Gen.auto()

 // Overriding
 let! x = Gen.auto'({Gen.defaults with Char = Gen.alphaNum})
}

and the compiler will infer what type it should generate based on the usage of x. Since I would like to give users the ability to specify the type argument explicitly (might be needed in some cases), I have updated the implementation of auto above to take an (optional) explicit type argument.

moodmosaic added a commit to hedgehogqa/fsharp-hedgehog-experimental that referenced this issue Nov 1, 2017
We're going to need it for the stuff in
hedgehogqa/fsharp-hedgehog#133
@moodmosaic
Copy link
Member

We should take this on hedgehogqa/fsharp-hedgehog-experimental#5.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants