--- Module for generating random strings -- @module lqc.generators.string -- @alias new local random = require 'lqc.random' local lqc = require 'lqc.quickcheck' local Gen = require 'lqc.generator' local char = require 'lqc.generators.char' local char_gen = char() -- NOTE: The shrink algorithms are *heavily* based on triq -- https://github.com/krestenkrab/triq --- Determines how many items to shrink. -- @param length size of the string -- @return integer indicating how many items to shrink local function shrink_how_many(length) -- 20% chance more than 1 member is shrunk if random.between(1, 5) == 1 then return random.between(1, length) end return 1 end --- Replaces a character in the string 'str' at index 'idx' with 'new_char'. -- @param str string to be modified -- @param new_char value that will replace the old character in the string -- @param idx position in the string where the character should be replaced -- @return updated string local function string_replace_char(str, new_char, idx) local result = {} result[1] = string.sub(str, 1, idx - 1) result[2] = new_char result[3] = string.sub(str, idx + 1) return table.concat(result) end --- Replaces 1 character at a random location in the string, tries up to 100 -- times if shrink gave back same result. -- @param prev previously generated string value -- @param length size of the string -- @param iterations_count remaining tries to shrink down this string -- @return shrunk down string value local function do_shrink_generic(prev, length, iterations_count) local idx = random.between(1, length) local old_char = string.sub(prev, idx, idx) local new_char = char_gen:shrink(old_char) if new_char == old_char and iterations_count ~= 0 then -- Shrink introduced no simpler result, retry at other index. return do_shrink_generic(prev, length, iterations_count - 1) end return string_replace_char(prev, new_char, idx) end --- Shrinks an amount of characters in the string. -- @param str string to be shrunk down -- @param length size of the string -- @param how_many amount of characters to shrink -- @return shrunk down string value local function shrink_generic(str, length, how_many) if how_many ~= 0 then local new_str = do_shrink_generic(str, length, lqc.numshrinks) return shrink_generic(new_str, length, how_many - 1) end return str end --- Determines if the string should be shrunk down to a shorter string -- @param str_len size of the string -- @return true: string should be made shorter; false: string should remain -- size during shrinking local function should_shrink_smaller(str_len) if str_len == 0 then return false end return random.between(1, 5) == 1 end --- Shrinks the string by removing 1 character -- @param str string to be shrunk down -- @param str_len size of the string -- @return new string with 1 random character removed local function shrink_smaller(str, str_len) local idx = random.between(1, str_len) -- Handle edge cases (first or last char) if idx == 1 then return string.sub(str, 2) end if idx == str_len then return string.sub(str, 1, idx - 1) end local new_str = {string.sub(str, 1, idx - 1), string.sub(str, idx + 1)} return table.concat(new_str) end --- Generates a string with a specific size. -- @param size size of the string to generate -- @return string of a specific size local function do_generic_pick(size) local result = {} for _ = 1, size do result[#result + 1] = char_gen:pick() end return table.concat(result) end --- Generates a string with arbitrary length (0 <= size <= numtests). -- @param numtests Number of times the property uses this generator; used to -- guide the optimization process. -- @return string of an arbitrary size local function arbitrary_length_pick(numtests) local size = random.between(0, numtests) return do_generic_pick(size) end --- Shrinks a string to a simpler form (smaller / different chars). -- 1. Returns empty strings instantly -- 2. Determine if string should be made shorter -- 2.1 if true: remove a char -- 2.2 otherwise: -- * simplify a random amount of characters -- * remove a char if simplify did not help -- * otherwise return the simplified string -- @param prev previously generated string value -- @return shrunk down string value local function arbitrary_length_shrink(prev) local length = #prev if length == 0 then return prev end -- handle empty strings if should_shrink_smaller(length) then return shrink_smaller(prev, length) end local new_str = shrink_generic(prev, length, shrink_how_many(length)) if new_str == prev then -- shrinking didn't help, remove an element return shrink_smaller(new_str, length) end -- string shrunk succesfully! return new_str end --- Helper function for generating a string with a specific size -- @param size size of the string to generate -- @return function that can generate strings of a specific size local function specific_length_pick(size) local function do_specific_pick() return do_generic_pick(size) end return do_specific_pick end --- Shrinks a string to a simpler form (only different chars since length is fixed). -- * "" -> "" -- * non-empty string, shrinks upto max 5 chars of the string -- @param prev previously generated string value -- @return shrunk down string value local function specific_length_shrink(prev) local length = #prev if length == 0 then return prev end -- handle empty strings return shrink_generic(prev, length, shrink_how_many(length)) end --- Generator for a string with an arbitrary size -- @return generator for a string of arbitrary size local function arbitrary_length_string() return Gen.new(arbitrary_length_pick, arbitrary_length_shrink) end --- Creates a generator for a string of a specific size -- @param size size of the string to generate -- @return generator for a string of a specific size local function specific_length_string(size) return Gen.new(specific_length_pick(size), specific_length_shrink) end --- Creates a new ASCII string generator -- @param size size of the string -- @return generator that can generate the following: -- 1. size provided: a string of a specific size -- 2. no size provided: string of an arbitrary size local function new(size) if size then return specific_length_string(size) end return arbitrary_length_string() end return new