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