Janiczek / hs-microthesis

Microthesis in Haskell

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

hs-microthesis

Better shrinking for PBT libraries

Port of elm-microthesis to Haskell.

Both are just (purely functional) ports of Minithesis, which is itself a distilled down version the core idea in Hypothesis.

That idea is:

  • when generating, remember integers we picked from the PRNG
  • give generators the ability to run on hardcoded lists of those integers (instead of running on the PRNG)
  • there is a single internal shrinker, which instead of working on the generated valus, works on the recording of the PRNG history
  • after every shrink attempt the generators run again (with <100% success rate), and the generated value is tested for the user property.
  • since the internal shrinker knows nothing about the generators, shrinker and generators agree to follow the rule "Shortlex-smaller recording leads to simpler generated values"

See also my talk describing this approach: How to do Property based Testing Shrinkers Right @ HaskellX 2022


Tests in app/Main.elm:

M.test "Shrunk values hold generation invariants" 
  (fmap (* 100) $ Gen.uniformInt 20)
  (\n -> n < 321)

M.test "Monadic bind can shrink both 'before' and 'after'" 
  (naiveListGen (Gen.uniformInt 20))
  (\list -> list == List.sort list)

result in test results like the following:

Seed: 718886798816897144

Shrunk values hold generation invariants
Fails with counterexample: 400
  Recording: Recording [4]
  Shrink history:
  - Step {value = 1400, recording = Recording [14], shrinkerUsed = "Initial"}
  - Step {value = 400,  recording = Recording [4],  shrinkerUsed = "Minimize with binary search (i=0)"}

Monadic bind can shrink both 'before' and 'after'
Fails with counterexample: [1,0]
  Recording: Recording [2,1,0]
  Shrink history:
  - Step {value = [14,4,16,12,12], recording = Recording [5,14,4,16,12,12], shrinkerUsed = "Initial"}
  - Step {value = [4,16,12,12],    recording = Recording [4,4,16,12,12],    shrinkerUsed = "Delete chunk and maybe decrement previous (i=1..1)"}
  - Step {value = [4,16,0,0],      recording = Recording [4,4,16,0,0],      shrinkerUsed = "Replace chunk with zero (i=3..4)"}
  - Step {value = [4,0,0,0],       recording = Recording [4,4,0,0,0],       shrinkerUsed = "Replace chunk with zero (i=2..3)"}
  - Step {value = [0,4,0,0],       recording = Recording [4,0,4,0,0],       shrinkerUsed = "Sort chunk (i=1..2)"}
  - Step {value = [0,1,0,0],       recording = Recording [4,0,1,0,0],       shrinkerUsed = "Minimize with binary search (i=2)"}
  - Step {value = [0,1,0],         recording = Recording [3,0,1,0,0],       shrinkerUsed = "Minimize with binary search (i=0)"}
  - Step {value = [0,1,0],         recording = Recording [3,0,1,0],         shrinkerUsed = "Delete chunk and maybe decrement previous (i=4..4)"}
  - Step {value = [1,0],           recording = Recording [2,1,0],           shrinkerUsed = "Delete chunk and maybe decrement previous (i=1..1)"}

About

Microthesis in Haskell

License:BSD 3-Clause "New" or "Revised" License


Languages

Language:Haskell 100.0%