commit 476a2de8f7a0185f7fda4319b22c0a4b21c929a1 Author: Jon Michael Aanes Date: Thu Dec 29 00:51:07 2016 +0100 This is some pretty-print library. diff --git a/pretty.lua b/pretty.lua new file mode 100644 index 0000000..82241c6 --- /dev/null +++ b/pretty.lua @@ -0,0 +1,419 @@ + +local TABLE_TYPE_SEQUENCE = 'SEQUENCE' +local TABLE_TYPE_PURE_MAP = 'PURE MAP' +local TABLE_TYPE_MIXED = 'MIXED TABLE' +local TABLE_TYPE_EMPTY = 'EMPTY TABLE' + +local SINGLE_LINE_SEQ_MAX_ELEMENTS = 10 +local SINGLE_LINE_MAP_MAX_ELEMENTS = 5 +local NR_CHARS_IN_LONG_STRING = 40 + +local TYPE_SORT_ORDER = { + ['nil'] = 0, + ['boolean'] = 1, + ['number'] = 2, + ['string'] = 3, + ['table'] = 4, + ['userdata'] = 5, + ['thread'] = 6, + ['function'] = 7, +} + +local RESERVED_LUA_WORDS = { + ['and'] = true, + ['break'] = true, + ['do'] = true, + ['else'] = true, + ['elseif'] = true, + ['end'] = true, + ['false'] = true, + ['for'] = true, + ['function'] = true, + ['if'] = true, + ['in'] = true, + ['local'] = true, + ['nil'] = true, + ['not'] = true, + ['or'] = true, + ['repeat'] = true, + ['return'] = true, + ['then'] = true, + ['true'] = true, + ['until'] = true, + ['while'] = true, +} + +local CHAR_TO_STR_REPR = {} + +do + for i = 00, 031 do CHAR_TO_STR_REPR[i] = '\\0'..(i < 10 and '0' or '')..i end + for i = 32, 255 do CHAR_TO_STR_REPR[i] = string.char(i) end + CHAR_TO_STR_REPR[7] = '\\a' + CHAR_TO_STR_REPR[8] = '\\b' + CHAR_TO_STR_REPR[9] = '\t' + CHAR_TO_STR_REPR[10] = '\n' + CHAR_TO_STR_REPR[11] = '\\v' + CHAR_TO_STR_REPR[12] = '\\f' + CHAR_TO_STR_REPR[13] = '\\r' + CHAR_TO_STR_REPR[92] = '\\\\' + CHAR_TO_STR_REPR[127] = '\\127' +end + +-------------------------------------------------------------------------------- +-- Util + +local function padnum(d) + local dec, n = string.match(d, "(%.?)0*(.+)") + return #dec > 0 and ("%.12f"):format(d) or ("%s%03d%s"):format(dec, #n, n) +end + +local function alphanum_compare_strings (a, b) + return tostring(a):gsub("%.?%d+", padnum)..("%3d"):format(#b) + < tostring(b):gsub("%.?%d+", padnum)..("%3d"):format(#a) +end + +local function count_occurances_of_substring_in_string (str, substr) + local _, count = string.gsub(str, substr, '') + return count +end + +local function smallest_secure_longform_string_level (str) + -- Determines the level a longform string needs to use, to avoid "code" + -- injection. For example, if we want to use longform on the string + -- 'Hello ]] World', we cannot use level-0 as this would result in + -- '[[Hello ]] World]]', which could be an issue in certain applications. + + local levels = { [1] = 1 } + str:gsub('%]=*%]', function (m) levels[m:len()] = true end) + return #levels - 1 +end + +local function compare_key_value_pairs (a, b) + -- Get types + local type_key_a, type_key_b = type(a[1]), type(b[1]) + local type_value_a, type_value_b = type(a[2]), type(b[2]) + + -- Tons of compare + if (type_key_a ~= 'string' or type_key_b ~= 'string') then + return TYPE_SORT_ORDER[type_key_a] < TYPE_SORT_ORDER[type_key_b] + elseif (type_value_a == type_value_b) then + return alphanum_compare_strings(a[1], b[1]) + else + return TYPE_SORT_ORDER[type_value_a] < TYPE_SORT_ORDER[type_value_b] + end +end + +local function get_key_value_pairs_in_proper_order (t) + -- Generates a sequence of key value pairs, in proper order. + -- Proper order is: + -- 1. By value type: as defined by the TYPE_SORT_ORDER in the top. + -- 2. By key type: TODO: Implement this. + -- 2.1. Numbers + -- 2.2. Strings in alphanumeric order + -- 2.3. Other wierdness. + + local key_value_pairs = {} + + for key, value in pairs(t) do + key_value_pairs[#key_value_pairs+1] = { key, value } + end + + table.sort(key_value_pairs, compare_key_value_pairs) + + return key_value_pairs +end + +local function nr_elements_in_map (t) + local k, count = nil, -1 + repeat + k, count = next(t, k), count + 1 + until not k + return count +end + +local function is_identifier(str) + -- An identier is defined in the lua reference guide + + return str:match('^[_%a][_%w]*$') and not RESERVED_LUA_WORDS[str] +end + +local function contains_only_nice_string_keys (t) + -- A "nice" string is here defined is one following the rules of lua + -- identifiers. + + for k, _ in pairs(t) do + if type(k) ~= 'string' or not is_identifier(k) then + return false + end + end + return true +end + +local function escape_string (str) + local l = {} + for i = 1, #str do + l[#l+1] = CHAR_TO_STR_REPR[str:byte(i)] + end + return table.concat(l, '') +end + +-------------------------------------------------------------------------------- +-- Identifyer stuff + +local SIMPLE_VALUE_TYPES = { + ['nil'] = true, + ['boolean'] = true, + ['number'] = true, + ['string'] = true, +} + +local function is_empty_table (value) + assert( type(value) == 'table', '[is_empty_table]: Only tables allowed!' ) + return next(value) == nil +end + +local function is_short_table (value) + -- In this context, a short table is either an empty table, or one with a + -- single element. + + assert( type(value) == 'table', '[is_short_table]: Only tables allowed!' ) + + local first_key = next(value) + return (not first_key or SIMPLE_VALUE_TYPES[type(value[first_key])]) + and (next(value, first_key) == nil) +end + +local function is_simple_value (value) + -- In this context, a simple value is a either nil, a boolean, a number, + -- a string or a short table. + -- TODO: Add clause about long strings. (Maybe >7 chars?) + + --if type(value) == 'table' then print(value, is_short_table(value)) end + return SIMPLE_VALUE_TYPES[ type(value) ] + or type(value) == 'table' and is_short_table(value) +end + +local function contains_non_simple_key_or_value (t) + for k, v in pairs(t) do + if not is_simple_value(k) or not is_simple_value(v) then + return true + end + end + return false +end + +local function get_table_type (value) + -- Determines table type: + -- * Sequence: All keys are integer in the range 1..#value + -- * Pure Map: #value == 0 + -- * Mixed: Any other + + if is_empty_table(value) then return TABLE_TYPE_EMPTY end + + local not_sequence = false + local not_pure_map = (#value ~= 0) + local max_index = #value + + -- Determine if there exist some non-index + for k, v in pairs(value) do + if type(k) ~= 'number' or k < 1 or max_index < k or k ~= math.floor(k) then + not_sequence = true + break + end + end + + -- Return type + if not not_sequence then + return TABLE_TYPE_SEQUENCE + elseif not_pure_map then + return TABLE_TYPE_MIXED + else + return TABLE_TYPE_PURE_MAP + end +end + +local function is_single_line_table (value) + -- In this context, a single-line table, is: + -- A) Either a sequence or a pure map. + -- B) Has no non-simple keys or values + -- C 1) If sequence, has at most SINGLE_LINE_SEQ_MAX_ELEMENTS elements. + -- C 2) If map, has at most SINGLE_LINE_MAP_MAX_ELEMENTS elements. + + local table_type = get_table_type(value) + + return not contains_non_simple_key_or_value(value) + and(table_type == TABLE_TYPE_SEQUENCE and #value <= SINGLE_LINE_SEQ_MAX_ELEMENTS + or table_type == TABLE_TYPE_PURE_MAP and nr_elements_in_map(value) <= SINGLE_LINE_MAP_MAX_ELEMENTS) +end + +-------------------------------------------------------------------------------- +-- Formatting stuff + +local format_table, format_value + +-- Ways to format keys + +local function format_key_strings (key) + return key +end + +local function format_key_any_type (key, options) + return '[' .. format_value(key, options, 'max') .. ']' +end + +-- Formatting tables + +local function format_single_line_sequence (t, options) + -- NOTE: Assumes that the input table was pre-checked with `is_single_line_table()` + + local l = {} + for i = 1, #t do l[i] = format_value(t[i], options) end + return '{ ' .. table.concat(l, ', ') .. ' }' +end + +local function format_single_line_map (t, options) + -- NOTE: Assumes that the input table was pre-checked with `is_single_line_table()` + + local key_format_func = contains_only_nice_string_keys(t) and format_key_strings or format_key_any_type + + local key_value_pairs = get_key_value_pairs_in_proper_order(t) + local l = {'{ '} + for _, pair in ipairs(key_value_pairs) do + l[#l+1] = key_format_func(pair[1], options) + l[#l+1] = ' = ' + l[#l+1] = format_value(pair[2], options) + l[#l+1] = ', ' + end + if l[#l] == ', ' then l[#l] = nil end + l[#l+1] = ' }' + return table.concat(l, '') +end + +local function format_sequence (t, options, depth) + if depth ~= 'max' and depth >= options.max_depth then return '{...}' + elseif is_single_line_table(t) then return format_single_line_sequence(t, options) + elseif depth == 'max' then return '{...}' + end + + local l = {'{\n'} + for index, value in ipairs(t) do + l[#l+1] = options.indent:rep(depth + 1) + l[#l+1] = format_value(value, options, depth + 1) + l[#l+1] = ',\n' + end + l[#l] = '\n' + l[#l+1] = options.indent:rep(depth) + l[#l+1] = '}' + return table.concat(l, '') +end + +local function format_map (t, options, depth) + if depth ~= 'max' and depth >= options.max_depth then return '{...}' + elseif is_single_line_table(t) then return format_single_line_map(t, options) + elseif depth == 'max' then return '{...}' + end + + local key_value_pairs = get_key_value_pairs_in_proper_order(t) + local key_format_func = contains_only_nice_string_keys(t) and format_key_strings or format_key_any_type + local max_key_len = 0 + + -- Figure out the max key length + for _, pair in pairs(key_value_pairs) do + pair[1] = key_format_func(pair[1], options) + pair[2] = format_value(pair[2], options, depth + 1) + max_key_len = math.max(max_key_len, pair[1]:len()) + end + + -- Finally figure out formatting + local l = {'{\n'} + for _, pair in ipairs(key_value_pairs) do + l[#l+1] = options.indent:rep(depth + 1) + l[#l+1] = pair[1] + l[#l+1] = string.rep(' ', max_key_len + 1 - pair[1]:len()) + l[#l+1] = '= ' + l[#l+1] = pair[2] + l[#l+1] = ',\n' + end + l[#l] = '\n' + l[#l+1] = options.indent:rep(depth) + l[#l+1] = '}' + return table.concat(l, '') +end + +function format_table (t, options, depth) + local table_type = get_table_type(t) + + if table_type == TABLE_TYPE_EMPTY then return '{}' + elseif table_type == TABLE_TYPE_SEQUENCE then return format_sequence(t, options, depth) + else return format_map(t, options, depth) + end +end + +local function format_string (str, options) + -- TODO: Add option for escaping unicode characters. + + local is_long_string = (str:len() >= NR_CHARS_IN_LONG_STRING) + local newline_or_tab_index = str:find('[\n\t]') + local single_quote_index = str:find('\'') + local double_quote_index = str:find('\"') + + + local chance_of_longform = is_long_string and (newline_or_tab_index <= NR_CHARS_IN_LONG_STRING) or double_quote_index and single_quote_index + + + local cut_string_index = options.cut_strings and (is_long_string or chance_of_longform) + and math.min(NR_CHARS_IN_LONG_STRING - 3, newline_or_tab_index or 1/0, double_quote_index or 1/0, single_quote_index or 1/0) + + local longform = chance_of_longform and ((not cut_string_index) or cut_string_index < math.min(newline_or_tab_index or 1/0, double_quote_index or 1/0, single_quote_index or 1/0)) + + local escape_newline_and_tab = not longform and newline_or_tab_index + + -- Determine string delimiters + local left, right + if longform then + local level = smallest_secure_longform_string_level(str) + left, right = '['..string.rep('=', level)..'[', ']'..string.rep('=', level)..']' + if newline_or_tab_index then str = '\n' .. str end + elseif not single_quote_index then + left, right = '\'', '\'' + else + left, right = '\"', '\"' + end + + -- Cut string + if cut_string_index then str = str:sub(1, cut_string_index) end + str = escape_string(str) + -- Escape newline and tab + if escape_newline_and_tab then str = str:gsub('\n', '\\n'):gsub('\t', '\\t') end + + return left .. str .. right +end + +local function format_number (value, shorthand) + if value ~= value then return shorthand and 'nan' or '0/0' + elseif value == 1/0 then return shorthand and 'inf' or '1/0' + elseif value == -1/0 then return shorthand and '-inf' or '-1/0' + else return tostring(value) + end +end + +function format_value (value, options, depth) + local type = type(value) + + if type == 'table' then return format_table(value, options, depth or 'max') + elseif type == 'string' then return format_string(value, options) + elseif type == 'number' then return format_number(value, options.math_shorthand) + else return tostring(value) + end +end + +-------------------------------------------------------------------------------- + +local function pretty_format (value, options) + local options = options or {} + options.max_depth = options.max_depth or math.huge + options.indent = options.indent or '\t' + return format_value(value, options, 0) +end + +return pretty_format diff --git a/test/TestSuite.lua b/test/TestSuite.lua new file mode 100644 index 0000000..9ca7c8d --- /dev/null +++ b/test/TestSuite.lua @@ -0,0 +1,275 @@ + +local TERM_COLOR_CODE_WHITE = '\27[37;1m' +local TERM_COLOR_CODE_GREEN = '\27[32;1m' +local TERM_COLOR_CODE_RED = '\27[31;1m' +local TERM_COLOR_CODE_GREY = '\27[30;0m' + +local ASSERT_ERROR_TYPE = [[ +Types not compatible: + Expected: %s (%s) + Gotten: %s (%s) +]] + +local ASSERT_ERROR_VALUE = [[ +Values of type '%s' not equal: + Expected: %s + Gotten: %s +]] + +local ASSERT_ERROR_TABLE_VALUE = [[ +Values in tables not equal: + For key: %s + + Expected: %s (%s) + Gotten: %s (%s) +]] + +local function assert_equal (expected, gotten) + if type(gotten) ~= type(expected) then + error(ASSERT_ERROR_TYPE:format(tostring(gotten), type(gotten), tostring(expected), type(expected))) + elseif type(expected) == 'table' and gotten ~= expected then + for key, expected_value in pairs(expected) do + if expected_value ~= gotten[key] then + error(ASSERT_ERROR_TABLE_VALUE:format(key, expected_value, type(expected_value), gotten[key], type(gotten[key]))) + end + end + elseif gotten ~= expected then + error(ASSERT_ERROR_VALUE:format(type(gotten), gotten, expected)) + end +end + +local libraries_tests_and_wrappers = { + assert_equal = assert_equal +} + +for k, v in pairs(_G) do + libraries_tests_and_wrappers[k] = v +end + +local function indent_string (str, indent) + return indent .. str:gsub('\n', '\n'..indent) +end + +local TEST_SUITE_TRACEBACK = "\n\t\t[C]: in function 'xpcall'\n\t\t./TestSuite.lua" + +local function find_cutoff_index_for_traceback_string (str) + local index = str:find(TEST_SUITE_TRACEBACK, 1, true) + return index +end + +-------------------------------------------------------------------------------- + +local TestSuite = {} + TestSuite.__index = TestSuite + +function TestSuite.new (module_name) + local new_test_suite = setmetatable({}, TestSuite) + new_test_suite.name = module_name + new_test_suite.submodules = {} + new_test_suite.tests = {} + new_test_suite.custom_env = {} + return new_test_suite +end + +-------------------------------------------------------------------------------- + +function TestSuite:setEnviroment (new_enviroment) + self.custom_env = new_enviroment +end + +function TestSuite:createUniqueEnviroment () + local new_enviroment = {} + for k, v in pairs(libraries_tests_and_wrappers) do + new_enviroment[k] = v + end + for k, v in pairs(self.custom_env) do + new_enviroment[k] = v + end + return new_enviroment +end + +-------------------------------------------------------------------------------- + +function TestSuite:addModules (modules) + for _, module in ipairs(modules) do + table.insert(self.submodules, module) + end +end + +function TestSuite:addTest (test_name, extra_info, test_func) + if type(extra_info) == 'function' then + extra_info, test_func = test_func, extra_info + end + + table.insert(self.tests, { + name = test_name, + test = test_func, + extra_info = extra_info or {} + }) +end + +-------------------------------------------------------------------------------- + +function TestSuite:runSubmodules (prefix, indent) + local total_errors, total_tests = 0, 0 + for _, module in ipairs(self.submodules) do + local errors, tests = module:runTests(prefix, indent) + total_errors, total_tests = total_errors + errors, total_tests + tests + end + return total_errors, total_tests +end + +local function setup_debug_hooks (extra_info) + assert(not (extra_info.max_time and extra_info.max_lines), "TEST CONFIG ERROR: Only one line hook allowed pr. test!") + local trace + if extra_info.max_time then + local stop_time = os.clock() + extra_info.max_time + trace = function (l) + if os.clock() >= stop_time then + debug.sethook(nil, 'l') + error("Test timed out! This is usually symptom of an infinite loop!") + end + end + elseif extra_info.max_lines then + local line_countdown = 10 + extra_info.max_lines + trace = function (l) + line_countdown = line_countdown - 1 + if line_countdown <= 0 then + debug.sethook(nil, 'l') + error("Test exteeded allowed number of lines! This is usually symptom of an infinite loop! (Or too low an estimate!)") + end + end + end + debug.sethook(trace, 'l') +end + +function TestSuite:runTests (parent_prefix, parent_indent) + local prefix, indent = self.name, (parent_indent or 0) + 2 + if parent_prefix then + prefix = parent_prefix .. '.' .. prefix + else + io.write('\n## Running tests ##\n\n') + end + + local nr_errors, nr_tests = self:runSubmodules(prefix, indent) + + local function error_handler (err) + return err..debug.traceback('', 4) + end + + local function print_status (left, right, color) + local term_width = 80 + local color = color or '' + io.write(TERM_COLOR_CODE_WHITE..left..': '..string.rep(' ', term_width + 1-#left-#right)..color..right..'\n'..TERM_COLOR_CODE_GREY) + end + + for index, test in ipairs(self.tests) do + nr_tests = nr_tests + 1 + -- Setup Test Env, and name + local ext_name = prefix .. '.' .. test.name + local env = self:createUniqueEnviroment() + setfenv(test.test, env) + + setup_debug_hooks(test.extra_info) + -- Call tests + local success, traceback = xpcall(test.test, error_handler) + -- Unset line hook, if set earlier + debug.sethook(nil, 'l') + -- Write work (or not.) + if success then + print_status(ext_name, 'SUCCESS!', TERM_COLOR_CODE_GREEN) + else + print_status(ext_name, 'ERROR!', TERM_COLOR_CODE_RED) + traceback = indent_string(traceback, '\t') + local stop_index = find_cutoff_index_for_traceback_string(traceback) + io.write('\n'..TERM_COLOR_CODE_GREY) + io.write(traceback:sub(1, stop_index)) + io.write('\n\n') + nr_errors = nr_errors + 1 + end + end + + if nr_errors == 0 then + print_status(prefix, 'NO ERRORS! WELL DONE!', TERM_COLOR_CODE_GREEN) + elseif nr_errors == 1 then + print_status(prefix, 'A SINGLE ERROR! ALMOST THERE!', TERM_COLOR_CODE_RED) + else + print_status(prefix, nr_errors..' ERRORS! GET TO WORK!', TERM_COLOR_CODE_RED) + end + + if not parent_prefix then + io.write(TERM_COLOR_CODE_WHITE) + io.write('\n## All tests run! ##\n\n') + + local width_of_bar = 70 + local size_of_red_bar = math.ceil( width_of_bar * nr_errors/nr_tests) + local size_of_green_bar = width_of_bar-size_of_red_bar + + io.write('Status: ') + io.write(nr_errors == 0 and TERM_COLOR_CODE_GREEN or TERM_COLOR_CODE_RED) + io.write(string.rep('#', width_of_bar)) + io.write(TERM_COLOR_CODE_WHITE) + + -- Get + io.write('\n\nSummary: '..TERM_COLOR_CODE_GREEN..string.rep('#', size_of_green_bar)..TERM_COLOR_CODE_RED..string.rep('#', size_of_red_bar)..'\n\n') + io.write(TERM_COLOR_CODE_WHITE) + end + + return nr_errors, nr_tests +end + +-------------------------------------------------------------------------------- + +local function get_end_of_function (text, start_i) + local indent, i, end_i = 0, start_i, text:len() + while i <= end_i do + if text:sub(i,i) == '"' then -- "style" strings + i = text:find('"', i + 1) + 1 + elseif text:sub(i,i) == "'" then -- 'style' strings + i = text:find("'", i + 1) + 1 + elseif text:sub(i,i+1) == '[[' then -- [[style]] strings + i = text:find(']]', i) + 2 + elseif text:sub(i,i+3) == '--[[' then -- multi line comments + i = text:find(']]', i) + 2 + elseif text:sub(i,i+1) == '--' then -- single line comments + i = text:find('\n', i) + 1 + elseif text:sub(i-1, i+8):find('%sfunction%s') or i == start_i and text:sub(i, i+7) == 'function' then + indent, i = indent + 1, i + 8 + elseif text:sub(i-1, i+5):find('%swhile%s') or i == start_i and text:sub(i, i+4) == 'while' then + indent, i = indent + 1, i + 4 + elseif text:sub(i-1, i+3):find('%sfor%s') or i == start_i and text:sub(i, i+2) == 'for' then + indent, i = indent + 1, i + 2 + elseif text:sub(i-1, i+2):find('%sif%s') or i == start_i and text:sub(i, i+1) == 'if' then + indent, i = indent + 1, i + 2 + elseif text:sub(i-1, i+3):find('%send%s') or (i == end_i - 2) and text:sub(i, i+2) == 'end' then + indent, i = indent - 1, i + 2 + if indent == 0 then + return i + end + else + i = i + 1 + end + end +end + +function TestSuite.getLocalFunction (module_name, func_name) + local file = io.open(module_name .. '.lua') + local file_text = file:read('*a') + file:close() + + local function_start_index = file_text:find('function '..func_name) + local function_end_index = get_end_of_function(file_text, function_start_index) + + local new_text = file_text:sub(1, function_end_index):gsub('local function '..func_name, 'return function') + assert(new_text ~= file_text, ('No function in module "%s" with name "%s" '):format(module_name, func_name)) + local chunk, error_msg = load(new_text) + if not chunk then + error(('While loading function in module "%s" with name "%s":\n\n\t%s\n'):format(module_name, func_name, error_msg)) + end + assert(chunk, error) + return chunk() +end + +-------------------------------------------------------------------------------- + +return TestSuite diff --git a/test/test_pretty.lua b/test/test_pretty.lua new file mode 100644 index 0000000..957cb78 --- /dev/null +++ b/test/test_pretty.lua @@ -0,0 +1,277 @@ + +local SUITE = require('TestSuite').new('pretty') +SUITE:setEnviroment{ + format = require('pretty') +} + +-------------------------------------------------------------------------------- + +local function format_test (t) + SUITE:addTest(t.expect, function () + local input_value = t.input + local input_options = t.options + local expected_result = t.expect + local actual_result = format(input_value, input_options) + assert_equal(actual_result, expected_result) + end) +end + +-------------------------------------------------------------------------------- +-- Strings + +format_test { + input = 'Hello World', + expect = '\'Hello World\'', +} + +format_test { + input = 'Hello \'World\'', + expect = '\"Hello \'World\'\"', +} + +format_test { + input = 'Hello \"World\"', + expect = '\'Hello \"World\"\'', +} + +format_test { + input = 'Hello [[World]]', + expect = '\'Hello [[World]]\'', +} + +format_test { + input = '\'Hello\' [[World]]', + expect = '\"\'Hello\' [[World]]\"', +} + +format_test { + input = '\'Hello\' \"there\" [[World]]', + expect = '[=[\'Hello\' \"there\" [[World]]]=]', +} + +format_test { + input = '\'Hello\' \"there\" [=[World]=]', + expect = '[[\'Hello\' \"there\" [=[World]=]]]', +} + +format_test { + input = '\nHello World', + expect = '\'\\nHello World\'', +} + +format_test { + input = '\'\"\n', + expect = '[[\n\'\"\n]]', +} + +format_test { + input = '\n', + expect = '\'\\n\'', +} + +format_test { + input = '\\', + expect = '\'\\\\\'', +} + +format_test { + input = '\000', + expect = '\'\\000\'', +} + +format_test { + input = '\a\b\v\r\f', + expect = '\'\\a\\b\\v\\r\\f\'', +} + +format_test { + input = 'ø', + expect = '\'ø\'', +} + +-------------------------------------------------------------------------------- +-- Numbers + +format_test { + input = 0, + expect = '0', +} + +format_test { + input = 2000, + expect = '2000', +} + +format_test { + input = -2000, + expect = '-2000', +} + +format_test { + input = 1/0, -- Same as math.huge + expect = '1/0', +} + +format_test { + input = -1/0, -- Same as -math.huge + expect = '-1/0', +} + +format_test { + input = 0/0, -- Same as nan + expect = '0/0', +} + +format_test { + input = 1/0, -- Same as math.huge + options = { math_shorthand = true }, + expect = 'inf', +} + +format_test { + input = -1/0, -- Same as -math.huge + options = { math_shorthand = true }, + expect = '-inf', +} + +format_test { + input = 0/0, -- Same as nan + options = { math_shorthand = true }, + expect = 'nan', +} + +-------------------------------------------------------------------------------- +-- Single-line tables + +format_test { + input = {}, + expect = '{}', +} + +format_test { + input = {1, 2, 3}, + expect = '{ 1, 2, 3 }', +} + +format_test { + input = { 'Hello', 'World' }, + expect = '{ \'Hello\', \'World\' }', +} + +format_test { + input = { a = 1, b = 2 }, + expect = '{ a = 1, b = 2 }', +} + +format_test { + input = { __hello = true }, + expect = '{ __hello = true }', +} + +format_test { + input = { [']]'] = true }, + expect = '{ [\']]\'] = true }', +} + +format_test { + input = { ['and'] = true }, + expect = '{ [\'and\'] = true }', +} + +format_test { + input = { [false] = false, [true] = true }, + expect = '{ [false] = false, [true] = true }', +} + +format_test { + input = { [100] = 'Hi', [300] = 'Hello' }, + expect = '{ [100] = \'Hi\', [300] = \'Hello\' }', +} + +format_test { -- Order does not matter + input = { b = 1, a = 2 }, + expect = '{ a = 2, b = 1 }', +} + +format_test { -- Can include empty tables + input = { {}, {}, {} }, + expect = '{ {}, {}, {} }', +} + +format_test { -- Can include very small tables + input = { {1}, {2}, {3} }, + expect = '{ { 1 }, { 2 }, { 3 } }', +} + + +-------------------------------------------------------------------------------- +-- Multi-line tables + +format_test { + input = { {1, 2, 3}, {4, 5, 6} }, + expect = '{\n\t{ 1, 2, 3 },\n\t{ 4, 5, 6 }\n}', +} + +format_test { + input = { a = {1, 2, 3}, b = {4, 5, 6} }, + expect = '{\n\ta = { 1, 2, 3 },\n\tb = { 4, 5, 6 }\n}', +} + +format_test { + input = { 'Hi', [300] = 'Hello' }, + expect = '{\n\t[1] = \'Hi\',\n\t[300] = \'Hello\'\n}', +} + +format_test { + input = { { {} } }, + expect = '{\n\t{ {} }\n}', +} + +format_test { + input = { [{ 1, 2 }] = { 2, 1 } }, + expect = '{\n\t[{ 1, 2 }] = { 2, 1 }\n}', +} + +format_test { + input = { { {1, 2}, {3, 4} }, {5, 6} }, + expect = '{\n\t{\n\t\t{ 1, 2 },\n\t\t{ 3, 4 }\n\t},\n\t{ 5, 6 }\n}', +} + +format_test { + input = { { {1, 2}, {3, 4} }, {5, 6} }, + options = { max_depth = 0 }, + expect = '{...}', +} + +format_test { + input = { { {1, 2}, {3, 4} }, {5, 6} }, + options = { max_depth = 1 }, + expect = '{\n\t{...},\n\t{...}\n}', +} + +format_test { + input = { { {1, 2}, {3, 4} }, {5, 6} }, + options = { max_depth = 2 }, + expect = '{\n\t{\n\t\t{...},\n\t\t{...}\n\t},\n\t{ 5, 6 }\n}', +} + +format_test { + input = { { {1, 2}, {3, 4} }, {5, 6} }, + options = { max_depth = 3 }, + expect = '{\n\t{\n\t\t{ 1, 2 },\n\t\t{ 3, 4 }\n\t},\n\t{ 5, 6 }\n}', +} + +format_test { + input = { [{ {1,2}, {3,4} }] = 'Hello World' }, + expect = '{\n\t[{...}] = \'Hello World\'\n}', +} + + +format_test { + input = false, + expect = 'true', +} + +-------------------------------------------------------------------------------- + +return SUITE diff --git a/test/tests.lua b/test/tests.lua new file mode 100644 index 0000000..f3ce4f5 --- /dev/null +++ b/test/tests.lua @@ -0,0 +1,49 @@ + +-- Util function + +local function get_test_modules (test_folder_and_prefix) + + local function string_split (str, sep) + local t, i = {}, 1 + for str in string.gmatch(str, "([^".. (sep or '%s') .."]+)") do + t[#t+1] = str + end + return t + end + + local function os_execute (command) + local handle = io.popen(command) + local result = handle:read("*a") + handle:close() + return string_split(result) + end + + local function get_test_filenames (dir_and_prefix) + return os_execute('ls '..dir_and_prefix) + end + + local function load_test_modules (test_filenames) + local modules = {} + for _, filename in ipairs(test_filenames) do + local module = require(filename:sub(1,-5)) + assert(type(module) == 'table', ('ERROR: Module "%s" did not return a Table value.'):format(filename)) + modules[#modules+1] = module + end + return modules + end + + local test_filenames = get_test_filenames(test_folder_and_prefix) + local test_modules = load_test_modules(test_filenames) + return test_modules +end + +-- Load modules and run them + +package.path = package.path .. ';./test/?.lua' + +local TEST_SUITE = require("TestSuite").new('Pretty') + +local modules = get_test_modules('test/test_*') +TEST_SUITE:addModules(modules) + +TEST_SUITE:runTests()