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

Added PropertyConfig #288

Merged
merged 16 commits into from
Jan 29, 2021
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 84 additions & 62 deletions src/Hedgehog/Linq/Property.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,36 @@ namespace Hedgehog.Linq
open System
open System.Runtime.CompilerServices
open Hedgehog
open System.Runtime.InteropServices


[<Extension>]
[<AbstractClass; Sealed>]
type PropertyConfigExtensions private () =

/// Set the number of times a property is allowed to shrink before the test runner gives up and prints the counterexample.
[<Extension>]
static member WithShrinks (config : PropertyConfig, shrinkLimit: int<shrinks>) : PropertyConfig =
PropertyConfig.withShrinks shrinkLimit config

/// Restores the default shrinking behavior.
[<Extension>]
static member WithoutShrinks (config : PropertyConfig) : PropertyConfig =
PropertyConfig.withoutShrinks config

/// Set the number of times a property should be executed before it is considered successful.
[<Extension>]
static member WithTests (config : PropertyConfig, testLimit: int<tests>) : PropertyConfig =
PropertyConfig.withTests testLimit config


type PropertyConfig =

/// The default configuration for a property test.
static member Default : Hedgehog.PropertyConfig =
PropertyConfig.defaultConfig


type Property = private Property of Property<unit> with

static member Failure : Property =
Expand Down Expand Up @@ -49,6 +78,13 @@ type Property = private Property of Property<unit> with
static member ForAll (gen : Gen<'T>) : Property<'T> =
Property.forAll' gen


module internal PropertyConfig =
let coalesce = function
| Some x -> x
| None -> PropertyConfig.defaultConfig


[<Extension>]
[<AbstractClass; Sealed>]
type PropertyExtensions private () =
Expand All @@ -70,86 +106,72 @@ type PropertyExtensions private () =
//

[<Extension>]
static member Report (property : Property) : Report =
let (Property property) = property
Property.report property

[<Extension>]
static member Report (property : Property, tests : int<tests>) : Report =
let (Property property) = property
Property.report' tests property

[<Extension>]
static member Report (property : Property<bool>) : Report =
Property.reportBool property

[<Extension>]
static member Report (property : Property<bool>, tests : int<tests>) : Report =
Property.reportBool' tests property

[<Extension>]
static member Check (property : Property) : unit =
static member Report
( property : Property,
[<Optional; DefaultParameterValue null>] ?config : Hedgehog.PropertyConfig
) : Report =
let (Property property) = property
Property.check property
Property.reportWith (PropertyConfig.coalesce config) property

[<Extension>]
static member Check (property : Property, tests : int<tests>) : unit =
let (Property property) = property
Property.check' tests property
static member Report
( property : Property<bool>,
[<Optional; DefaultParameterValue null>] ?config : Hedgehog.PropertyConfig
) : Report =
Property.reportBoolWith (PropertyConfig.coalesce config) property

[<Extension>]
static member Check (property : Property<bool>) : unit =
Property.checkBool property

[<Extension>]
static member Check (property : Property<bool>, tests : int<tests>) : unit =
Property.checkBool' tests property

[<Extension>]
static member Recheck (property : Property, size : Size, seed : Seed) : unit =
static member Check
( property : Property,
[<Optional; DefaultParameterValue null>] ?config : Hedgehog.PropertyConfig
) : unit =
let (Property property) = property
Property.recheck size seed property
Property.checkWith (PropertyConfig.coalesce config) property

[<Extension>]
static member Recheck (property : Property, size : Size, seed : Seed, tests : int<tests>) : unit =
let (Property property) = property
Property.recheck' size seed tests property
static member Check
( property : Property<bool>,
[<Optional; DefaultParameterValue null>] ?config : Hedgehog.PropertyConfig
) : unit =
Property.checkBoolWith (PropertyConfig.coalesce config) property

[<Extension>]
static member Recheck (property : Property<bool>, size : Size, seed : Seed) : unit =
Property.recheckBool size seed property

[<Extension>]
static member Recheck (property : Property<bool>, size : Size, seed : Seed, tests : int<tests>) : unit =
Property.recheckBool' size seed tests property

[<Extension>]
static member ReportRecheck (property : Property, size : Size, seed : Seed) : Report =
let (Property property) = property
Property.reportRecheck size seed property

[<Extension>]
static member ReportRecheck (property : Property, size : Size, seed : Seed, tests : int<tests>) : Report =
static member Recheck
( property : Property,
size : Size,
seed : Seed,
[<Optional; DefaultParameterValue null>] ?config : Hedgehog.PropertyConfig
) : unit =
let (Property property) = property
Property.reportRecheck' size seed tests property
Property.recheckWith size seed (PropertyConfig.coalesce config) property

[<Extension>]
static member ReportRecheck (property : Property<bool>, size : Size, seed : Seed) : Report =
Property.reportRecheckBool size seed property
static member Recheck
( property : Property<bool>,
size : Size,
seed : Seed,
[<Optional; DefaultParameterValue null>] ?config : Hedgehog.PropertyConfig
) : unit =
Property.recheckBoolWith size seed (PropertyConfig.coalesce config) property

[<Extension>]
static member ReportRecheck (property : Property<bool>, size : Size, seed : Seed, tests : int<tests>) : Report =
Property.reportRecheckBool' size seed tests property

[<Extension>]
static member Print (property : Property, tests : int<tests>) : unit =
static member ReportRecheck
( property : Property,
size : Size,
seed : Seed,
[<Optional; DefaultParameterValue null>] ?config : Hedgehog.PropertyConfig
) : Report =
let (Property property) = property
Property.print' tests property
Property.reportRecheckWith size seed (PropertyConfig.coalesce config) property

[<Extension>]
static member Print (property : Property) : unit =
let (Property property) = property
Property.print property
static member ReportRecheck
( property : Property<bool>,
size : Size,
seed : Seed,
[<Optional; DefaultParameterValue null>] ?config : Hedgehog.PropertyConfig
) : Report =
Property.reportRecheckBoolWith size seed (PropertyConfig.coalesce config) property

[<Extension>]
static member Where (property : Property<'T>, filter : Func<'T, bool>) : Property<'T> =
Expand Down
116 changes: 79 additions & 37 deletions src/Hedgehog/Property.fs
Original file line number Diff line number Diff line change
@@ -1,11 +1,38 @@
namespace Hedgehog
namespace Hedgehog

open System

[<Struct>]
type Property<'a> =
| Property of Gen<Journal * Outcome<'a>>


type PropertyConfig = internal {
TestLimit : int<tests>
ShrinkLimit : int<shrinks> option
}


module PropertyConfig =

/// The default configuration for a property test.
let defaultConfig : PropertyConfig =
{ TestLimit = 100<tests>
ShrinkLimit = None }

/// Set the number of times a property is allowed to shrink before the test runner gives up and prints the counterexample.
let withShrinks (shrinkLimit : int<shrinks>) (config : PropertyConfig) : PropertyConfig =
{ config with ShrinkLimit = Some shrinkLimit }
Copy link
Member Author

@dharmaturtle dharmaturtle Jan 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that shrinkLimit is not an Option. This means that they can't set the value to None. This makes for slightly cleaner usage (PropertyConfig.withShrinkLimit 0 vs PropertyConfig.withShrinkLimit (Some 0)) but I don't feel strongly about it.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since calling this function is an explicit choice a user would make, I'm fine with the parameter being a simple int<shrinks>. Making the parameter an Option<int<shrinks>> exposes how we've chosen to implement this a bit too much. Also, if we later decided to make this required, this could be very confusing. Consider this code:

PC.defaultConfig |> PC.withShrinkLimit None

If we decided to require that parameter, it would be very surprising when a test failed due to a shrink limit being imposed when the user explicitly chose None.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could add to the API withoutShrinkLimit

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. I was torn between that and withNoShrinkLimit, but my libarts sisters talked me out of it.

Inspired by Haskell, I dropped "Limit" from withShrinkLimit and withTestLimit, but kept "limit" in the parameter name.


/// Restores the default shrinking behavior.
let withoutShrinks (config : PropertyConfig) : PropertyConfig =
{ config with ShrinkLimit = None }

/// Set the number of times a property should be executed before it is considered successful.
let withTests (testLimit : int<tests>) (config : PropertyConfig) : PropertyConfig =
{ config with TestLimit = testLimit }


module Property =

let ofGen (x : Gen<Journal * Outcome<'a>>) : Property<'a> =
Expand Down Expand Up @@ -103,26 +130,34 @@ module Property =
(size : Size)
(seed : Seed)
(Node ((journal, x), xs) : Tree<Journal * Outcome<'a>>)
(nshrinks : int<shrinks>) : Status =
(nshrinks : int<shrinks>)
(shrinkLimit : int<shrinks> Option) : Status =
let failed =
Failed {
Size = size
Seed = seed
Shrinks = nshrinks
Journal = journal
RenderRecheck = renderRecheck
}
let takeSmallest tree = takeSmallest renderRecheck size seed tree (nshrinks + 1<shrinks>) shrinkLimit
match x with
| Failure ->
match Seq.tryFind (Outcome.isFailure << snd << Tree.outcome) xs with
| None ->
Failed {
Size = size
Seed = seed
Shrinks = nshrinks
Journal = journal
RenderRecheck = renderRecheck
}
| None -> failed
| Some tree ->
takeSmallest renderRecheck size seed tree (nshrinks + 1<shrinks>)
match shrinkLimit with
| None -> takeSmallest tree
| Some shrinkLimit' ->
if nshrinks < shrinkLimit' then
takeSmallest tree
else failed
| Discard ->
GaveUp
| Success _ ->
OK

let private reportWith' (renderRecheck : bool) (size0 : Size) (seed : Seed) (n : int<tests>) (p : Property<unit>) : Report =
let private reportWith' (renderRecheck : bool) (size0 : Size) (seed : Seed) (config : PropertyConfig) (p : Property<unit>) : Report =
let random = toGen p |> Gen.toRandom

let nextSize size =
Expand All @@ -132,7 +167,7 @@ module Property =
size + 1

let rec loop seed size tests discards =
if tests = n then
if tests = config.TestLimit then
{ Tests = tests
Discards = discards
Status = OK }
Expand All @@ -148,32 +183,29 @@ module Property =
| Failure ->
{ Tests = tests + 1<tests>
Discards = discards
Status = takeSmallest renderRecheck size seed result 0<shrinks> }
Status = takeSmallest renderRecheck size seed result 0<shrinks> config.ShrinkLimit}
| Success () ->
loop seed2 (nextSize size) (tests + 1<tests>) discards
| Discard ->
loop seed2 (nextSize size) tests (discards + 1<discards>)

loop seed size0 0<tests> 0<discards>

let private reportWith (renderRecheck : bool) (size : Size) (seed : Seed) (p : Property<unit>) : Report =
p |> reportWith' renderRecheck size seed 100<tests>

let report' (n : int<tests>) (p : Property<unit>) : Report =
let reportWith (config : PropertyConfig) (p : Property<unit>) : Report =
let seed = Seed.random ()
p |> reportWith' true 1 seed n
p |> reportWith' true 1 seed config

let report (p : Property<unit>) : Report =
p |> report' 100<tests>
p |> reportWith PropertyConfig.defaultConfig

let reportBool' (n : int<tests>) (p : Property<bool>) : Report =
p |> bind ofBool |> report' n
let reportBoolWith (config : PropertyConfig) (p : Property<bool>) : Report =
p |> bind ofBool |> reportWith config

let reportBool (p : Property<bool>) : Report =
p |> bind ofBool |> report

let check' (n : int<tests>) (p : Property<unit>) : unit =
report' n p
let checkWith (config : PropertyConfig) (p : Property<unit>) : unit =
reportWith config p
|> Report.tryRaise

let check (p : Property<unit>) : unit =
Expand All @@ -183,8 +215,8 @@ module Property =
let checkBool (g : Property<bool>) : unit =
g |> bind ofBool |> check

let checkBool' (n : int<tests>) (g : Property<bool>) : unit =
g |> bind ofBool |> check' n
let checkBoolWith (config : PropertyConfig) (g : Property<bool>) : unit =
g |> bind ofBool |> checkWith config

/// Converts a possibly-throwing function to
/// a property by treating "no exception" as success.
Expand All @@ -195,34 +227,34 @@ module Property =
with
| _ -> failure

let reportRecheck' (size : Size) (seed : Seed) (n : int<tests>) (p : Property<unit>) : Report =
reportWith' false size seed n p
let reportRecheckWith (size : Size) (seed : Seed) (config : PropertyConfig) (p : Property<unit>) : Report =
reportWith' false size seed config p

let reportRecheck (size : Size) (seed : Seed) (p : Property<unit>) : Report =
reportWith false size seed p
reportWith' false size seed PropertyConfig.defaultConfig p

let reportRecheckBool' (size : Size) (seed : Seed) (n : int<tests>) (p : Property<bool>) : Report =
p |> bind ofBool |> reportRecheck' size seed n
let reportRecheckBoolWith (size : Size) (seed : Seed) (config : PropertyConfig) (p : Property<bool>) : Report =
p |> bind ofBool |> reportRecheckWith size seed config

let reportRecheckBool (size : Size) (seed : Seed) (p : Property<bool>) : Report =
p |> bind ofBool |> reportRecheck size seed

let recheck' (size : Size) (seed : Seed) (n : int<tests>) (p : Property<unit>) : unit =
reportRecheck' size seed n p
let recheckWith (size : Size) (seed : Seed) (config : PropertyConfig) (p : Property<unit>) : unit =
reportRecheckWith size seed config p
|> Report.tryRaise

let recheck (size : Size) (seed : Seed) (p : Property<unit>) : unit =
reportRecheck size seed p
|> Report.tryRaise

let recheckBool' (size : Size) (seed : Seed) (n : int<tests>) (g : Property<bool>) : unit =
g |> bind ofBool |> recheck' size seed n
let recheckBoolWith (size : Size) (seed : Seed) (config : PropertyConfig) (g : Property<bool>) : unit =
g |> bind ofBool |> recheckWith size seed config

let recheckBool (size : Size) (seed : Seed) (g : Property<bool>) : unit =
g |> bind ofBool |> recheck size seed

let print' (n : int<tests>) (p : Property<unit>) : unit =
report' n p
let printWith (config : PropertyConfig) (p : Property<unit>) : unit =
reportWith config p
|> Report.render
|> printfn "%s"

Expand All @@ -231,6 +263,16 @@ module Property =
|> Report.render
|> printfn "%s"

let printBoolWith (config : PropertyConfig) (p : Property<bool>) : unit =
reportBoolWith config p
|> Report.render
|> printfn "%s"

let printBool (p : Property<bool>) : unit =
reportBool p
|> Report.render
|> printfn "%s"

[<AutoOpen>]
module PropertyBuilder =
let rec private loop (p : unit -> bool) (m : Property<unit>) : Property<unit> =
Expand Down
Loading