local SUITE = require('TestSuite').new('function') SUITE:setEnviroment{ format = require('pretty') } -------------------------------------------------------------------------------- -- Compat if not loadstring then loadstring = load end -- Lua 5.3 compat local HAS_ADV_GETLOCAL = not not debug.getinfo(1, 'u').nparams -- Lua 5.1 compat local HAS_UNICODE_IDEN = not not loadstring 'local ϕ = 1; return ϕ' -- Lua 5.1 compat local HAS_JIT_LIBRARY = type(rawget(_G, 'jit')) == 'table' -- Non-LuaJIT compat -- local function format_test (t) if t.adv_getlocal and not HAS_ADV_GETLOCAL then return end if t.single then return SUITE:addTest(t.name or t.expect, function () local format = format({t.input}, t.options):gsub('^{%s+(.-)%s+}$', function(a) return '{ '..a..' }' end) assert_equal('{ '..t.expect..' }', format) end, { line = debug.getinfo(2).currentline }) end SUITE:addTest(t.name or t.expect, function () assert_equal(t.expect, format(t.input, t.options)) end, { line = debug.getinfo(2).currentline }) end local function curline (delta) return debug.getinfo(2).currentline + (delta or 0) end local TEST_UPVALUE = 42 -------------------------------------------------------------------------------- -- Basic inline functions format_test { name = 'Function basic formatting', adv_getlocal = true, single = true, input = function () end, expect = 'function () ... end', } format_test { name = 'Function with a single argument', adv_getlocal = true, single = true, input = function (a) end, expect = 'function (a) ... end', } format_test { name = 'Function with multiple arguments', adv_getlocal = true, single = true, input = function (a, b) end, expect = 'function (a, b) ... end', } format_test { name = 'Function with vararg', adv_getlocal = true, single = true, input = function (...) end, expect = 'function (...) ... end', } format_test { name = 'Function with vararg and multiple arguments', adv_getlocal = true, single = true, input = function (a, b, ...) end, expect = 'function (a, b, ...) ... end', } format_test { name = 'Function with upvalues don\'t affected by them in single mode 1', adv_getlocal = true, single = true, input = function () l = TEST_UPVALUE end, expect = 'function () ... end', } format_test { name = 'Function with upvalues don\'t affected by them in single mode 2', adv_getlocal = true, single = true, input = function () TEST_UPVALUE = true end, expect = 'function () ... end', } -------------------------------------------------------------------------------- -- Basic elaborate functions local func_line = curline(3) format_test { name = 'Singleline elaborate function', input = function () end, expect = 'function ()\n -- Source file: \'./test/test_function.lua\' [Line: '..func_line..']\n\n ...\nend', } local func_line = curline(3) format_test { name = 'Multiline elaborate function', input = function () end, expect = 'function ()\n -- Source file: \'./test/test_function.lua\' [Lines: '..func_line..' - '..(func_line+1)..']\n\n ...\nend', } local func_line = curline(3) -- Must be exactly 3 lines above function format_test { name = 'Elaborate function with upvalue included 1', input = function () l = TEST_UPVALUE end, adv_getlocal = true, expect = 'function ()\n -- Source file: \'./test/test_function.lua\' [Line: '..func_line..']\n -- Up values: { TEST_UPVALUE = 42 }\n\n ...\nend' } local func_line = curline(3) -- Must be exactly 3 lines above function format_test { name = 'Elaborate function with upvalue included 2', input = function () TEST_UPVALUE = true end, adv_getlocal = true, expect = 'function ()\n -- Source file: \'./test/test_function.lua\' [Line: '..func_line..']\n -- Up values: { TEST_UPVALUE = 42 }\n\n ...\nend' } -------------------------------------------------------------------------------- -- Elaborate functions with documentation format_test { name = 'Basic Func with docs', input = function () -- This is docs return true end, expect = 'function ()\n -- This is docs\n\n ...\nend', } format_test { name = 'Comments after other code won\'t be included as docs', input = function () -- This is also docs if true then return false end -- Some other comment, that won't appear as docs. return true end, expect = 'function ()\n -- This is also docs\n\n ...\nend', } format_test { name = 'We can leave a space between doc lines', input = function () -- This is docs -- This is also docs return true end, expect = 'function ()\n -- This is docs\n -- This is also docs\n\n ...\nend', } format_test { name = 'We can also leave a line with an empty comment', input = function () -- This is docs -- -- This is also docs return true end, expect = 'function ()\n -- This is docs\n -- \n -- This is also docs\n\n ...\nend', } format_test { name = 'No opvalues when docs are there', input = function () -- This is docs return TEST_UPVALUE end, expect = 'function ()\n -- This is docs\n\n ...\nend', } format_test { name = 'Can find docs when body contains the word "function"', input = function () -- Hi _function() end, expect = 'function ()\n -- Hi\n\n ...\nend', } format_test { name = 'Can find docs from string-loaded function', input = loadstring 'return function ()\n--Hello\nend' (), expect = 'function ()\n -- Hello\n\n ...\nend', } format_test { name = 'String-loaded functions without docs, won\'t display Source file comment', input = loadstring 'return function () end' (), expect = 'function ()\n -- Loaded from string\n\n ...\nend', } -------------------------------------------------------------------------------- -- Builtins format_test { single = true, input = math.abs, expect = 'builtin function (x) ... end', } format_test { input = math.abs, expect = 'builtin function (x)\n -- math.abs\n -- Returns the absolute value of x.\n\n ...\nend', } format_test { single = true, input = math.random, expect = 'builtin function ([m [, n]) ... end', } format_test { input = math.random, expect = 'builtin function ([m [, n])\n -- math.random\n -- When called without arguments, returns a uniform pseudo-random real\n -- number in the range [0,1). When called with an integer number m,\n -- math.random returns a uniform pseudo-random integer in the range [1, m].\n -- When called with two integer numbers m and n, math.random returns a\n -- uniform pseudo-random integer in the range [m, n].\n\n ...\nend', } format_test { input = string.byte, expect = 'builtin function (s [, i [, j]])\n -- string.byte\n -- Returns the internal numerical codes of the characters s[i], s[i+1],\n -- ..., s[j]. The default value for i is 1; the default value for j is\n -- i.\n -- Note that numerical codes are not necessarily portable across\n -- platforms.\n\n ...\nend', } -- short_builtins option: If an builtin is expected to be available by some name -- in a standard enviroment, return that name, instead of other complex info. format_test { -- NOTE: These tests may be counter to intention, soon. input = math.random, options = { short_builtins = true }, expect = 'math.random', } format_test { -- NOTE: These tests may be counter to intention, soon. input = { math.cos, math.sin, math.abs }, options = { short_builtins = true }, expect = '{ math.cos, math.sin, math.abs }', } format_test { name = 'Replace function with short version, if key', input = { [math.random] = 365 }, expect = '{ [math.random] = 365 }', } -------------------------------------------------------------------------------- -- Embedding functions loaded with loadstring format_test { name = 'Embed functions loaded with loadstring', single = true, input = loadstring('return function (a, b) return a + b end')(), expect = 'function (a, b) return a + b end', } format_test { name = 'Embed functions with extra whitespace stripped', single = true, input = loadstring('return function (a, b)\n return a + b\nend')(), expect = 'function (a, b) return a + b end', } format_test { name = 'Embedding nested function, when on same line is too hard 1', single = true, adv_getlocal = true, input = loadstring('return function () return function () end end')(), expect = 'function () ... end', } format_test { name = 'Embedding nested function, when on same line is too hard 2', single = true, adv_getlocal = true, input = loadstring('return function () return function () end end')()(), expect = 'function () ... end', } format_test { name = 'Embedding nested function, when they end on different lines is too hard', single = true, adv_getlocal = true, input = loadstring('return function () return function () end\nend')()(), expect = 'function () ... end', } format_test { name = 'Embed functions which contains the word "function"', single = true, adv_getlocal = true, input = loadstring('return function () function_body() end')(), expect = 'function () function_body() end', } format_test { name = 'Embed functions defined with OOP style', single = true, adv_getlocal = true, input = loadstring('local obj = {} \n function obj:a () return self end \n return obj.a')(), expect = 'function (self) return self end', } --[[ NOTE: This turned out to be hard. Requires traversal of AST. format_test { name = 'Embed correct function, when two are on the same line, but with different arguments 1', single = true, adv_getlocal = true, input = loadstring 'return { a = function (a) return a end, b = function (b) return b end }' ()['a'], expect = 'function (a) return a end', } format_test { name = 'Embed correct function, when two are on the same line, but with different arguments 2', single = true, adv_getlocal = true, input = loadstring 'return { a = function (a) return a end, b = function (b) return b end }' ()['b'], expect = 'function (b) return b end', } --]] format_test { name = 'If embedding a function would be too long, ignore', single = true, adv_getlocal = true, input = loadstring('return function () return 1234578901235789012357890 end')(), expect = 'function () ... end', } format_test { name = 'If embedding a function would include newline, ignore', single = true, adv_getlocal = true, input = loadstring('return function () -- Hi\n return 1234 end')(), expect = 'function () ... end', } -------------------------------------------------------------------------------- -- Indent functions nicely format_test { name = 'Align the tail part, letting people focus on the important aspects.', input = { random = math.random, abs = math.abs }, expect = '{\n abs = builtin function (x) ... end,\n random = builtin function ([m [, n]) ... end\n}', } format_test { name = 'Align the function part, even if some are builtin and some are not.', adv_getlocal = true, input = { random = math.random, abs = function (x) return x < 0 and -x or x end }, expect = '{\n abs = function (x) ... end,\n random = builtin function ([m [, n]) ... end\n}', } format_test { name = 'Align the function part, even if one is loaded from a string.', adv_getlocal = true, input = { random = math.random, abs = loadstring('return function () return 1 end')() }, expect = '{\n abs = function () return 1 end,\n random = builtin function ([m [, n]) ... end\n}', } format_test { name = 'Align the end part when both are loaded from strings.', adv_getlocal = true, input = { a = loadstring'return function(a) return a end'(), b = loadstring'return function (...) return ... end'() }, expect = '{\n a = function (a) return a end,\n b = function (...) return ... end\n}', } format_test { name = 'Align without special indent if there is function modifier.', adv_getlocal = true, input = { max = function(a, b) return a > b and a or b end, abs = function (x) return x < 0 and -x or x end }, expect = '{\n abs = function (x) ... end,\n max = function (a, b) ... end\n}', } if HAS_UNICODE_IDEN then format_test { name = 'Align functions with unicode-named parameters nicely', adv_getlocal = true, input = loadstring 'return {\nfunction (ψ) return ψ end,\nfunction (b) return b end\n}' (), expect = '{\n function (ψ) return ψ end\n function (b) return b end\n}', } end -------------------------------------------------------------------------------- -- Closure creation -- _include_closure option: If a function uses upvalues, return code that creates -- a closure for that function, including the function code. -- NOTE: Without AST traversal, it's impossible to garentee that values refered -- by multiple recursive closures are the same, because they can actually refer -- to different variables with the same name, as the following example shows: -- -- local val_a = 1 -- local func_a = function () return val_a end -- local val_a = 2 -- local func_c = function () return func_a() + val_a end -- NOTE: The following tests are all EXPERIMENTAL! All of the tests use features -- which may change at any time. format_test { name = 'Closures do not affect non-upvalue function', adv_getlocal = true, options = { _include_closure = true }, input = function (a, b) return (a > b) and a or b end, expect = 'function (a, b) return (a > b) and a or b end', } format_test { name = 'Closures do not affect builtins', input = math.abs, options = { _include_closure = true }, expect = 'builtin function (x)\n -- math.abs\n -- Returns the absolute value of x.\n\n ...\nend', } do local input_func = (function () local i = 0 return function (a) i = i + a; return i end end)() for i = 1, 10 do input_func(1) end format_test { adv_getlocal = true, options = { _include_closure = true }, input = input_func, expect = '(function () local i = 10; return function (a) i = i + a; return i end end)()', } end do local input_func = loadstring([[ return function () local i = 0 return function (a) i = i + a; return i end end]])()() for i = 1, 10 do input_func(1) end format_test { name = 'Can include variables in closure', adv_getlocal = true, options = { _include_closure = true }, input = input_func, expect = '(function () local i = 10; return function (a) i = i + a; return i end end)()', } end format_test { name = 'Can include functions in closure', adv_getlocal = true, options = { _include_closure = true }, input = (function () local custom_max = function (a, b) return (a > b) and a or b end return function (a, b) return custom_max(a, b) + 2 end end)(), expect = '(function () local custom_max = function (a, b) return (a > b) and a or b end; return function (a, b) return custom_max(a, b) + 2 end end)()', } format_test { name = 'Can include functions defined with `function () ...` style', adv_getlocal = true, options = { _include_closure = true }, input = (function () local function custom_max (a, b) return (a > b) and a or b end return function (a, b) return custom_max(a, b) + 2 end end)(), expect = '(function () local custom_max = function (a, b) return (a > b) and a or b end; return function (a, b) return custom_max(a, b) + 2 end end)()', } do local custom_max = function (a, b) return (a > b) and a or b end format_test { name = 'Can include functions from outside scope into closure', adv_getlocal = true, options = { _include_closure = true }, input = (function () return function (a, b) return custom_max(a, b) + 2 end end)(), expect = '(function () local custom_max = function (a, b) return (a > b) and a or b end; return function (a, b) return custom_max(a, b) + 2 end end)()', } end do local a_func = function (x) return x + 2 end local b_func = function (x) return a_func(x) * a_func(x) end local c_func = function (x) return b_func(a_func(x)) end format_test { name = 'Can support recursive closures', adv_getlocal = true, options = { _include_closure = true }, input = c_func, expect = '(function () local b_func = (function () local a_func = function (x) return x + 2 end; return function (x) return a_func(x) * a_func(x) end end)(); local a_func = function (x) return x + 2 end; return function (x) return b_func(a_func(x)) end end)()', } end -------------------------------------------------------------------------------- --[[ format_test { adv_getlocal = true, options = { _all_function_info = true, max_depth = 2 }, input = function() end, expect = '', } --]] return SUITE