Create shrinker for fixed length string that does not shrink to an empty string

I have written a Fuzzer that generates fixed length alphanumeric (latin) strings. The generation works fine, but I cannot figure out how to apply a shrinker that generates a shrunk value that conforms to my length restriction and shrinks appropriately.

If I implement the Fuzzer like below it generates the appropriate string but shrinks incorrectly to empty string :

module HotFuzz exposing (..)

import Fuzz exposing (..)
import Random.Char as RandomChar
import Random.Extra as RandomExtra
import Shrink as Shrink exposing (Shrinker)

nLengthUpperCaseAlphanumeric: Int -> Fuzzer String
nLengthUpperCaseAlphanumeric length =
    let
       charGen =  RandomExtra.choices (RandomChar.char 48 57) [RandomChar.upperCaseLatin]
       gen = RandomString.string length charGen
       charShrinker = Shrink.atLeastChar '0'
       shrinker = Shrink.convert (String.fromList) (String.toList) (Shrink.list charShrinker)
    in
       custom gen shrinker

I’ve also tried some more advanced shrinking but it doesn’t do what I want.

What I want is to have is a Shrinker that demonstrates the following properties:

  • The shrunk string should shrink to a string of length "length"
  • The shrunk string should have each char in it shrunk to '0' (i.e. for size 3 ABB should shrink to 000)

I’ve also tried a more advanced (convoluted) Shrinker that I’m not sure even shrinks:

nLengthUpperCaseAlphanumeric : Int -> Fuzzer String
nLengthUpperCaseAlphanumeric length =
    let
       charGen =  RandomExtra.choices (RandomChar.char 48 57) [RandomChar.upperCaseLatin]
       gen = RandomString.string length charGen
       charShrinker = Shrink.atLeastChar '0'
       mkString ch = List.repeat length ch |> String.fromList
       shrinker str = 
            str
            |> String.toList
            |> List.map (\_ -> '0')
            |> List.head
            |> Maybe.withDefault '0'
            |> charShrinker
            |> Shrink.map mkString           
    in
       custom gen shrinker

This “shrinker” does keep my string length to the expected length, but it collapses to more choices than I’d expect.

1 Like

Shrink.list by default will shrink first to an empty list, which is not what you want, as you then get an empty string.

You can force it to shrink to a String of wanted length by adding |> Shrink.keepIf (\str -> String.length str == length) to your shrinker.

nLengthUpperCaseAlphanumeric : Int -> Fuzzer String
nLengthUpperCaseAlphanumeric length =
    let
        charGen =
            Random.Extra.choices (Random.Char.char 48 57) [ Random.Char.upperCaseLatin ]

        gen =
            Random.String.string length charGen

        charShrinker =
            Shrink.atLeastChar '0'

        charsShrinker chars =
            List.map charShrinker chars

        shrinker =
            Shrink.convert String.fromList String.toList (Shrink.list charShrinker)    
                |> Shrink.keepIf (\str -> String.length str == length)   
    in
    custom gen shrinker

1 Like

Thanks this works great. I guess I was mistaken in that I looked at keepIf as being a filter, but it actually kind of acts like a means of “pinning” a value.

Thanks gain.

I think your intuition was good, it is a filter, but for the lazy list of shrinked values to test.
So only the strings of expected length will be used as shrinked values to test.

PS: You could of course have also used |> Shrink.dropIf (\str -> String.length str /= length).

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.