diff --git a/src/Fuzz.elm b/src/Fuzz.elm index 34c751a..6a83b6e 100644 --- a/src/Fuzz.elm +++ b/src/Fuzz.elm @@ -265,12 +265,38 @@ asciiCharGenerator = Random.map Char.fromCode (Random.int 32 126) -whitespaceCharGenerator : Generator Char -whitespaceCharGenerator = - Random.sample [ ' ', '\t', '\n' ] |> Random.map (Maybe.withDefault ' ') +unicodeCharGeneratorFrequencyList : List ( Float, Generator Char ) +unicodeCharGeneratorFrequencyList = + let + ascii = + asciiCharGenerator + + whitespace = + Random.sample [ ' ', '\t', '\n' ] |> Random.map (Maybe.withDefault ' ') + + tilde = + '̃' + + circumflex = + '̂' + diaeresis = + '̈' + + combiningDiacriticalMarks = + Random.sample [ circumflex, tilde, diaeresis ] |> Random.map (Maybe.withDefault circumflex) + + emoji = + Random.sample [ '🌈', '❤', '🔥' ] |> Random.map (Maybe.withDefault '❤') + in + [ ( 4, ascii ) + , ( 1, whitespace ) + , ( 1, combiningDiacriticalMarks ) + , ( 1, emoji ) + ] -{-| Generates random printable ASCII strings of up to 1000 characters. + +{-| Generates random printable unicode strings of up to 1000 characters. Shorter strings are more common, especially the empty string. @@ -278,28 +304,20 @@ Shorter strings are more common, especially the empty string. string : Fuzzer String string = let - asciiGenerator : Generator String - asciiGenerator = - Random.frequency - [ ( 3, Random.int 1 10 ) - , ( 0.2, Random.constant 0 ) - , ( 1, Random.int 11 50 ) - , ( 1, Random.int 50 1000 ) - ] - |> Random.andThen (lengthString asciiCharGenerator) - - whitespaceGenerator : Generator String - whitespaceGenerator = - Random.int 1 10 - |> Random.andThen (lengthString whitespaceCharGenerator) + unicodeGenerator : Generator String + unicodeGenerator = + frequencyList + (Random.frequency + [ ( 0.2, Random.constant 0 ) + , ( 3, Random.int 1 10 ) + , ( 1, Random.int 11 50 ) + , ( 1, Random.int 50 1000 ) + ] + ) + unicodeCharGeneratorFrequencyList + |> Random.map String.fromList in - custom - (Random.frequency - [ ( 9, asciiGenerator ) - , ( 1, whitespaceGenerator ) - ] - ) - Shrink.string + custom unicodeGenerator Shrink.string {-| Given a fuzzer of a type, create a fuzzer of a maybe for that type. diff --git a/src/Util.elm b/src/Util.elm index 9d3fbbd..0be9e93 100644 --- a/src/Util.elm +++ b/src/Util.elm @@ -30,3 +30,59 @@ lengthString : Generator Char -> Int -> Generator String lengthString charGenerator stringLength = list stringLength charGenerator |> map String.fromList + + +{-| Creates a generator from either a single element from a single generator, +a single one of the generators, a pair of the generators or all of the generators, +then runs Fuzz.frequency on that subset until we have the desired length list. + +**Warning:** Do not pass an empty list or your program will crash! In practice +this is usually not a problem since you pass a list literal. + +-} +frequencyList : Generator Int -> List ( Float, Generator a ) -> Generator (List a) +frequencyList lengthGenerator pairs = + let + weightedPairs = + pairs |> List.map (\( a, b ) -> ( a, constant ( a, b ) )) + + randomGenerator : Generator ( Float, Generator a ) + randomGenerator = + weightedPairs |> frequency + + generator : Generator (Generator a) + generator = + sample + [ -- single repeated element for a single generator + pairs + |> frequency + |> map constant + + -- single generator + , randomGenerator + |> map List.singleton + |> map frequency + + -- pair of generators + , map2 + (\a b -> frequency [ a, b ]) + randomGenerator + randomGenerator + + -- all generators + , pairs + |> frequency + |> constant + ] + |> andThen + (\gen -> + case gen of + Nothing -> + Debug.crash "frequencyList is broken; list literal is empty" + + Just gen -> + gen + ) + in + map2 (,) lengthGenerator generator + |> andThen (\( len, gen ) -> list len gen) diff --git a/tests/FuzzerTests.elm b/tests/FuzzerTests.elm index 633d8a1..8862a4b 100644 --- a/tests/FuzzerTests.elm +++ b/tests/FuzzerTests.elm @@ -129,6 +129,7 @@ fuzzerTests = , shrinkingTests , manualFuzzerTests ] + , unicodeStringFuzzerTests ] @@ -260,3 +261,37 @@ manualFuzzerTests = , List.reverse >> List.head >> Expect.equal (Maybe.map Tuple.first pair) ] ] + + +unicodeStringFuzzerTests : Test +unicodeStringFuzzerTests = + describe "unicode string fuzzer" + [ expectToFail <| + fuzz string "generates ascii" <| + \str -> str |> String.contains "E" |> Expect.true "Expected to find ascii letter E" + , expectToFail <| + fuzz string "generates whitespace" <| + \str -> str |> String.contains "\t" |> Expect.true "Expected to find a tab character" + , expectToFail <| + fuzz string "generates combining diacritical marks" <| + \str -> str |> String.contains "̃" |> Expect.true "Expected to find the combining diactricial mark character tilde" + , expectToFail <| + fuzz string "generates emoji" <| + \str -> str |> String.contains "🔥" |> Expect.true "Expected to find 🔥 emoji" + , expectToFail <| + fuzz string "generates long strings with a single character" <| + \str -> + let + countSequentialUniquesAtStart s = + case s of + a :: b :: cs -> + if a == b then + 1 + countSequentialUniquesAtStart (b :: cs) + else + 0 + + _ -> + 0 + in + str |> String.toList |> countSequentialUniquesAtStart |> (\x -> x < 7) |> Expect.true "expected a string with 7-length duplicates" + ]