You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

201 lines
6.5 KiB
Lua

--- 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