From fc66bf73f6bcae419a8f2ab0545340af4a8458e1 Mon Sep 17 00:00:00 2001 From: Matthieu Baechler Date: Thu, 17 Aug 2023 11:50:20 +0200 Subject: [PATCH 1/3] Add Fuzz.filterMap --- src/Fuzz.elm | 44 +++++++++++++++++++++++++++++++++------ tests/src/FuzzerTests.elm | 26 +++++++++++++++++++++++ 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/src/Fuzz.elm b/src/Fuzz.elm index 70cacdb1..50bf692d 100644 --- a/src/Fuzz.elm +++ b/src/Fuzz.elm @@ -13,6 +13,7 @@ module Fuzz exposing , map, map2, map3, map4, map5, map6, map7, map8, andMap , andThen, lazy, sequence, traverse , fromGenerator + , filterMap ) {-| This is a library of _fuzzers_ you can use to supply values to your fuzz @@ -1335,9 +1336,39 @@ a risk of infinite loop depending on the predicate), you can use this pattern: -} filter : (a -> Bool) -> Fuzzer a -> Fuzzer a -filter predicate fuzzer = +filter predicate = filterMap (\a -> if predicate a then Just a else Nothing) + +{-| A fuzzer that applies a function returning a Maybe on a given fuzzer and +output values, as List.filterMap does. + +Warning: By using `Fuzz.filterMap` you can get exceptionally unlucky and get 15 +rejections in a row, in which case the test will fluke out and fail! + +It's always preferable to get to your wanted values using [`Fuzz.map`](#map), +as you don't run the risk of rejecting too may values and slowing down your +tests, for example using `Fuzz.intRange 0 5 |> Fuzz.map (\x -> x * 2)` instead +of `Fuzz.intRange 0 9 |> Fuzz.filterMap (\x -> if modBy 2 x == 0 then Just x else Nothing)`. + +If you want to generate indefinitely until you find a satisfactory value (with +a risk of infinite loop depending on the predicate), you can use this pattern: + + goodItemFuzzer = + itemFuzzer + |> Fuzz.andThen + (\item -> + case f item of + Just b -> + Fuzz.constant b + + Nothing -> + goodItemFuzzer + ) + +-} +filterMap : (a -> Maybe b) -> Fuzzer a -> Fuzzer b +filterMap f fuzzer = let - go : Int -> Fuzzer a + go : Int -> Fuzzer b go rejectionCount = if rejectionCount > 15 then invalid "Too many values were filtered out" @@ -1346,11 +1377,12 @@ filter predicate fuzzer = fuzzer |> andThen (\value -> - if predicate value then - constant value + case f value of + Just b -> + constant b - else - go (rejectionCount + 1) + Nothing -> + go (rejectionCount + 1) ) in go 0 diff --git a/tests/src/FuzzerTests.elm b/tests/src/FuzzerTests.elm index c81b0cdf..f7c66578 100644 --- a/tests/src/FuzzerTests.elm +++ b/tests/src/FuzzerTests.elm @@ -1131,6 +1131,32 @@ fuzzerSpecificationTests = , canGenerateSatisfyingWith { runs = 5000 } "not divisible by 5" intsNotDivBy5 (not << isDivBy5) , cannotGenerateSatisfyingWith { runs = 5000 } "divisible by 5" intsNotDivBy5 isDivBy5 ] + , describe "filterMap" <| + let + {- We're using a more complicated (at least, naming and + readability wise) example than isEven to make it less + likely to randomly hit 15 even numbers in a row... + (that _has_ happened...) + -} + isDivBy5 : Int -> Bool + isDivBy5 n = + modBy 5 n == 0 + + intsNotDivBy5 : Fuzzer Int + intsNotDivBy5 = + Fuzz.int + |> Fuzz.filterMap (\i -> if isDivBy5 i then Just i else Nothing) + + in + [ rejects "impossible func (always Nothing)" + (Fuzz.int |> Fuzz.filterMap (\_ -> Nothing)) + "Too many values were filtered out" + , passes "trivial func (always Just) doesn't reject" + (Fuzz.int |> Fuzz.filterMap (\_ -> True)) + (\_ -> True) + , canGenerateSatisfyingWith { runs = 5000 } "not divisible by 5" intsNotDivBy5 (not << isDivBy5) + , cannotGenerateSatisfyingWith { runs = 5000 } "divisible by 5" intsNotDivBy5 isDivBy5 + ] ] ] From 55c4cd2c99d04a1d30cf06e8fb7d98f64caf90fe Mon Sep 17 00:00:00 2001 From: Matthieu Baechler Date: Fri, 12 Jan 2024 10:37:45 +0100 Subject: [PATCH 2/3] fixup! Add Fuzz.filterMap --- src/Fuzz.elm | 18 +++++++++++++++++- tests/src/FuzzerTests.elm | 4 ++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Fuzz.elm b/src/Fuzz.elm index 50bf692d..41c0b5e6 100644 --- a/src/Fuzz.elm +++ b/src/Fuzz.elm @@ -63,7 +63,7 @@ can usually find the simplest input that reproduces a bug. ## Working with Fuzzers -@docs constant, invalid, filter +@docs constant, invalid, filter, filterMap @docs map, map2, map3, map4, map5, map6, map7, map8, andMap @docs andThen, lazy, sequence, traverse @@ -1341,6 +1341,22 @@ filter predicate = filterMap (\a -> if predicate a then Just a else Nothing) {-| A fuzzer that applies a function returning a Maybe on a given fuzzer and output values, as List.filterMap does. +Example usage: + + type UnicodeNonLetter = UnicodeNonLetter Char + + fromChar : Char -> Maybe UnicodeNonLetter + fromChar c = + if (c |> Unicode.isLower |> not) && (c |> Unicode.isUpper |> not) then + UnicodeNonLetter |> Just + else + Nothing + + + fuzz : Fuzzer UnicodeNonLetter + fuzz = + Fuzz.char |> Fuzz.filterMap fromChar + Warning: By using `Fuzz.filterMap` you can get exceptionally unlucky and get 15 rejections in a row, in which case the test will fluke out and fail! diff --git a/tests/src/FuzzerTests.elm b/tests/src/FuzzerTests.elm index f7c66578..d8c35253 100644 --- a/tests/src/FuzzerTests.elm +++ b/tests/src/FuzzerTests.elm @@ -1145,14 +1145,14 @@ fuzzerSpecificationTests = intsNotDivBy5 : Fuzzer Int intsNotDivBy5 = Fuzz.int - |> Fuzz.filterMap (\i -> if isDivBy5 i then Just i else Nothing) + |> Fuzz.filterMap (\i -> if isDivBy5 i then Nothing else Just i) in [ rejects "impossible func (always Nothing)" (Fuzz.int |> Fuzz.filterMap (\_ -> Nothing)) "Too many values were filtered out" , passes "trivial func (always Just) doesn't reject" - (Fuzz.int |> Fuzz.filterMap (\_ -> True)) + (Fuzz.int |> Fuzz.filterMap Just) (\_ -> True) , canGenerateSatisfyingWith { runs = 5000 } "not divisible by 5" intsNotDivBy5 (not << isDivBy5) , cannotGenerateSatisfyingWith { runs = 5000 } "divisible by 5" intsNotDivBy5 isDivBy5 From 099edd40e2d35ca0c807f677082c291976297654 Mon Sep 17 00:00:00 2001 From: Matthieu Baechler Date: Sat, 20 Jan 2024 09:56:05 +0100 Subject: [PATCH 3/3] fixup! Add Fuzz.filterMap --- src/Fuzz.elm | 19 ++++++++++++++----- tests/src/FuzzerTests.elm | 8 +++++++- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/Fuzz.elm b/src/Fuzz.elm index 41c0b5e6..bd52e5f5 100644 --- a/src/Fuzz.elm +++ b/src/Fuzz.elm @@ -9,11 +9,10 @@ module Fuzz exposing , array, maybe, result , bool, unit, order, weightedBool , oneOf, oneOfValues, frequency, frequencyValues - , constant, invalid, filter + , constant, invalid, filter, filterMap , map, map2, map3, map4, map5, map6, map7, map8, andMap , andThen, lazy, sequence, traverse , fromGenerator - , filterMap ) {-| This is a library of _fuzzers_ you can use to supply values to your fuzz @@ -1336,23 +1335,33 @@ a risk of infinite loop depending on the predicate), you can use this pattern: -} filter : (a -> Bool) -> Fuzzer a -> Fuzzer a -filter predicate = filterMap (\a -> if predicate a then Just a else Nothing) +filter predicate = + filterMap + (\a -> + if predicate a then + Just a + + else + Nothing + ) + {-| A fuzzer that applies a function returning a Maybe on a given fuzzer and output values, as List.filterMap does. Example usage: - type UnicodeNonLetter = UnicodeNonLetter Char + type UnicodeNonLetter + = UnicodeNonLetter Char fromChar : Char -> Maybe UnicodeNonLetter fromChar c = if (c |> Unicode.isLower |> not) && (c |> Unicode.isUpper |> not) then UnicodeNonLetter |> Just + else Nothing - fuzz : Fuzzer UnicodeNonLetter fuzz = Fuzz.char |> Fuzz.filterMap fromChar diff --git a/tests/src/FuzzerTests.elm b/tests/src/FuzzerTests.elm index d8c35253..d2a9ace0 100644 --- a/tests/src/FuzzerTests.elm +++ b/tests/src/FuzzerTests.elm @@ -1145,8 +1145,14 @@ fuzzerSpecificationTests = intsNotDivBy5 : Fuzzer Int intsNotDivBy5 = Fuzz.int - |> Fuzz.filterMap (\i -> if isDivBy5 i then Nothing else Just i) + |> Fuzz.filterMap + (\i -> + if isDivBy5 i then + Nothing + else + Just i + ) in [ rejects "impossible func (always Nothing)" (Fuzz.int |> Fuzz.filterMap (\_ -> Nothing))