Skip to content

Commit

Permalink
Minimize enumerated children when rechecking hedgehogqa#432
Browse files Browse the repository at this point in the history
Also changes the serialized form of the shrink path.
Previously, it was a string of 0's and 1's.  Now, it is a sequence
of dash-separated integers.  In terms of the new serialization,
the integers in the new sequence count the number of 1's between each consecutive pair of 0's.
  • Loading branch information
TysonMN committed Jul 14, 2023
1 parent c2b5e7e commit 7ffba4b
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 31 deletions.
4 changes: 4 additions & 0 deletions src/Hedgehog/Linq/Property.fs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ type PropertyExtensions private () =
static member ReportRecheck (property : Property<bool>, recheckData: string) : Report =
property |> Property.falseToFailure |> Property.reportRecheck recheckData

[<Extension>]
static member ReportRecheck (property : Property<bool>, recheckData: RecheckData) : Report =
property |> Property.falseToFailure |> Property.reportRecheck (RecheckData.serialize recheckData)

[<Extension>]
static member ReportRecheck (property : Property<bool>, recheckData: string, config : Hedgehog.PropertyConfig) : Report =
property |> Property.falseToFailure |> Property.reportRecheckWith recheckData config
Expand Down
39 changes: 21 additions & 18 deletions src/Hedgehog/Property.fs
Original file line number Diff line number Diff line change
Expand Up @@ -152,28 +152,31 @@ module Property =
| Some shrinkLimit', _ when nshrinks >= shrinkLimit' -> getFailed ()
| _, None -> getFailed ()
| _, Some (idx, tree) ->
let nextShrinkPathRev = ShrinkOutcome.Fail :: (List.replicate idx ShrinkOutcome.Pass @ shrinkPathRev)
let nextShrinkPathRev = ShrinkOutcome.Pass idx :: shrinkPathRev
loop (nshrinks + 1<shrinks>) nextShrinkPathRev tree
loop 0<shrinks> []

let rec private followShrinkPath
(Node (root, children) : Tree<Lazy<Journal * Outcome<'a>>>) =
let rec skipPassedChild children shrinkPath =
match children, shrinkPath with
| _, [] ->
let journal, outcome = root.Value
match outcome with
| Failure ->
{ Shrinks = 0<shrinks>
Journal = journal
RecheckInfo = None }
|> Failed
| Success _ -> OK
| Discard -> failwith "Unexpected 'Discard' result when rechecking. This should never happen."
| [], _ -> failwith "The shrink path lead to a dead end. This should never happen."
| _ :: childrenTail, ShrinkOutcome.Pass :: shrinkPathTail -> skipPassedChild childrenTail shrinkPathTail
| childrenHead :: _, ShrinkOutcome.Fail :: shrinkPathTail -> followShrinkPath childrenHead shrinkPathTail
skipPassedChild (Seq.toList children)
(Node (root, children) : Tree<Lazy<Journal * Outcome<'a>>>)
shrinkPath =
match shrinkPath with
| [] ->
let journal, outcome = root.Value
match outcome with
| Failure ->
{ Shrinks = 0<shrinks>
Journal = journal
RecheckInfo = None }
|> Failed
| Success _ -> OK
| Discard -> failwith "Unexpected 'Discard' result when rechecking. This should never happen."
| ShrinkOutcome.Pass i :: shinkPathTail ->
let nextRoot =
children
|> Seq.skip i
|> Seq.tryHead
|> Option.defaultWith (fun () -> failwith "The shrink path lead to a dead end. This should never happen.")
followShrinkPath nextRoot shinkPathTail

let private splitAndRun p data =
let seed1, seed2 = Seed.split data.Seed
Expand Down
18 changes: 9 additions & 9 deletions src/Hedgehog/Report.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ namespace Hedgehog

[<RequireQualifiedAccess>]
type ShrinkOutcome =
| Pass
| Fail
| Pass of int

[<Struct>]
type RecheckData = internal {
Expand Down Expand Up @@ -49,14 +48,15 @@ module internal RecheckData =
open System

let private separator = "_"
let private pathSeparator = "-"

let serialize data =
[ string data.Size
string data.Seed.Value
string data.Seed.Gamma
data.ShrinkPath
|> List.map (function ShrinkOutcome.Fail -> "0" | ShrinkOutcome.Pass -> "1" )
|> String.concat "" ]
|> List.map (function ShrinkOutcome.Pass i -> i.ToString() )
|> String.concat pathSeparator ]
|> String.concat separator

let deserialize (s: string) =
Expand All @@ -67,11 +67,11 @@ module internal RecheckData =
{ Value = parts.[1] |> UInt64.Parse
Gamma = parts.[2] |> UInt64.Parse }
let path =
parts.[3]
|> Seq.map (function '0' -> ShrinkOutcome.Fail
| '1' -> ShrinkOutcome.Pass
| c -> failwithf "Unexpected character %c in shrink path" c)
|> Seq.toList
if parts.[3] = ""
then []
else parts.[3].Split([|pathSeparator|], StringSplitOptions.None)
|> Seq.map (Int32.Parse >> ShrinkOutcome.Pass)
|> Seq.toList
{ Size = size
Seed = seed
ShrinkPath = path }
Expand Down
42 changes: 39 additions & 3 deletions tests/Hedgehog.Linq.Tests/LinqTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;

// Import ForAll:
Expand Down Expand Up @@ -55,14 +57,48 @@ from i in ForAll(gen)
if (report2.Status is Status.Failed)
{
Assert.Equal(1, count);
} else {
} else
{
throw new Exception("Recheck report should be Failed but is not");
}
} else {
} else
{
throw new Exception("Initial report should be Failed but is not");
}
}

[Fact]
// https://github.com/hedgehogqa/fsharp-hedgehog/issues/432
public void RecheckIsFasterThanCheck()
{
var low = Gen.Int32(Range.Constant(0, 5));
var mid = Gen.Int32(Range.Constant(10, 50));
var big = Gen.Int32(Range.Constant(100, 200));
var large = Gen.Int32(Range.Constant(500, 1000));
var choice = Gen.Choice(new List<Gen<int>> { low, mid, big, large }).List(Range.Constant(100, 200));
var prop = ForAll(choice).Select(x => x.Any((x) => x == 990));
var watch = new System.Diagnostics.Stopwatch();

watch.Start();
var report1 = prop.Report();
watch.Stop();

var checkTime = watch.ElapsedMilliseconds;
watch.Reset();

if (!(report1.Status is Status.Failed failure))
{
throw new Exception("Initial report should be Failed but is not");
}

watch.Start();
prop.ReportRecheck(failure.Item.RecheckInfo.Value.Data);
watch.Stop();

var recheckTime = watch.ElapsedMilliseconds;
Assert.InRange(recheckTime, 0, checkTime * 1.25); // Added 25% buffer for robustness
}

/*
* The main object the following tests is just to make sure that the examples compile.
* There's nothing fancy in the properties being tested.
Expand Down
4 changes: 3 additions & 1 deletion tests/Hedgehog.Tests/ReportTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ let reportTests = testList "Report tests" [
property {
let! size = Range.linear 0 1000 |> Gen.int32
let! path =
Gen.item [ ShrinkOutcome.Fail; ShrinkOutcome.Pass ]
Range.linear 0 3
|> Gen.int32
|> Gen.map ShrinkOutcome.Pass
|> Gen.list (Range.linear 0 10)
let expected = {
Size = size
Expand Down

0 comments on commit 7ffba4b

Please sign in to comment.