Skip to content
This repository has been archived by the owner on Oct 9, 2019. It is now read-only.

Make Fuzz.string generate unicode strings #204

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
68 changes: 43 additions & 25 deletions src/Fuzz.elm
Original file line number Diff line number Diff line change
Expand Up @@ -265,41 +265,59 @@ 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.

-}
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.
Expand Down
56 changes: 56 additions & 0 deletions src/Util.elm
Original file line number Diff line number Diff line change
Expand Up @@ -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)
35 changes: 35 additions & 0 deletions tests/FuzzerTests.elm
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ fuzzerTests =
, shrinkingTests
, manualFuzzerTests
]
, unicodeStringFuzzerTests
]


Expand Down Expand Up @@ -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"
]