Initial commit on assert-gooder. An improved version of assert
that automatically creates a useful error message, when the assert fails.
This commit is contained in:
commit
aec232efcb
54
Lexer.lua
Normal file
54
Lexer.lua
Normal file
|
@ -0,0 +1,54 @@
|
|||
|
||||
local LEXER_RULE_CONTINUE = {}
|
||||
|
||||
local function lex_string (lexer, str, offset)
|
||||
offset = offset or 0
|
||||
if type(lexer) ~= 'table' then error(('[Lexer]: Bad argument #1, expected table, but got %s (%s).' ):format(lexer, type(lexer))) end
|
||||
if type(str) ~= 'string' then error(('[Lexer]: Bad argument #2, expected string, but got %s (%s).' ):format(str, type(str))) end
|
||||
if type(offset) ~= 'number' then error(('[Lexer]: Bad argument #3, expected number, but got %s (%s).' ):format(offset, type(offset))) end
|
||||
local index, tokens = 1, {}
|
||||
while index <= #str do
|
||||
local longest_match, longest_match_right = nil, index - 1
|
||||
for i = 1, #lexer.rules do
|
||||
local _, match_right = string.find(str, lexer.rules[i].pattern, index)
|
||||
if match_right and longest_match_right < match_right then
|
||||
longest_match, longest_match_right = lexer.rules[i], match_right
|
||||
end
|
||||
end
|
||||
--
|
||||
if longest_match then
|
||||
if longest_match.token ~= LEXER_RULE_CONTINUE then
|
||||
tokens[#tokens+1] = { text = str:sub(index, longest_match_right)
|
||||
, left = offset + index
|
||||
, right = offset + longest_match_right
|
||||
, token = longest_match.token }
|
||||
end
|
||||
index = longest_match_right
|
||||
end
|
||||
index = index + 1
|
||||
end
|
||||
|
||||
return tokens
|
||||
end
|
||||
|
||||
local LEXER_MT = { __index = { lex = lex_string } }
|
||||
local STRICT_MT = { __index = error, __newindex = error }
|
||||
|
||||
local function new_lexer (t)
|
||||
|
||||
assert(type(t) == 'table')
|
||||
--
|
||||
local rules = {}
|
||||
local tokens = {}
|
||||
--
|
||||
for _, rule in ipairs(t) do
|
||||
assert(type(rule[1]) == 'string', 'Pattern must be string!')
|
||||
assert(type(rule[2]) == 'string' or rule[2] == LEXER_RULE_CONTINUE)
|
||||
assert(not string.match('', rule[1]), 'Pattern must not match empty string!')
|
||||
rules[#rules+1] = { pattern = '^'..rule[1], token = rule[2] }
|
||||
end
|
||||
|
||||
return setmetatable({ rules = rules, tokens = setmetatable(tokens, STRICT_MT) }, LEXER_MT)
|
||||
end
|
||||
|
||||
return setmetatable({ CONTINUE = LEXER_RULE_CONTINUE }, {__call = function(_, ...) return new_lexer(...) end})
|
90
assert-gooder.lua
Normal file
90
assert-gooder.lua
Normal file
|
@ -0,0 +1,90 @@
|
|||
|
||||
local lexer = require 'lua_lang'
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local function get_assert_body_text (call_info)
|
||||
if call_info.what == 'Lua' then
|
||||
-- Find filetext
|
||||
local filetext = nil
|
||||
if call_info.source:find '^@' then
|
||||
local f = io.open(call_info.short_src, 'r')
|
||||
filetext = f:read '*all'
|
||||
f:close()
|
||||
elseif call_info.short_src:find '^%[string' then
|
||||
filetext = call_info.source
|
||||
else
|
||||
error 'Not implemented yet!'
|
||||
end
|
||||
-- Get lines
|
||||
local filetext = filetext .. '\n'
|
||||
local lines_after, line_i = {}, 0
|
||||
for line in filetext:gmatch '([^\r\n]*)[\r\n]' do
|
||||
line_i = line_i + 1
|
||||
if call_info.currentline == line_i then
|
||||
lines_after[#lines_after+1] = line
|
||||
end
|
||||
end
|
||||
-- Find body exclusively.
|
||||
return table.concat(lines_after, '\n'):match('assert%s*(%b())'):sub(2, -2)
|
||||
end
|
||||
|
||||
error 'Not implemented yet!'
|
||||
end
|
||||
|
||||
local function get_assert_body (call_info)
|
||||
local text = get_assert_body_text(call_info)
|
||||
return lexer:lex(text), text
|
||||
end
|
||||
|
||||
local function get_variable (var_name, level)
|
||||
-- Local
|
||||
local index = 0
|
||||
repeat
|
||||
index = index + 1
|
||||
local name, val = debug.getlocal(level + 1, index)
|
||||
if name == var_name then
|
||||
local info = debug.getinfo(level + 1)
|
||||
local is_par = index <= info.nparams
|
||||
return val, is_par and ('argument #'..index) or 'local', info.name or is_par and ''
|
||||
end
|
||||
until not name
|
||||
|
||||
-- Up-value
|
||||
local index, func = 0, debug.getinfo(level + 1).func
|
||||
repeat
|
||||
index = index + 1
|
||||
local name, val = debug.getupvalue(func, index)
|
||||
if name == var_name then return val, 'upvalue' end
|
||||
until not name
|
||||
|
||||
-- Global
|
||||
return getfenv(level + 1)[var_name], 'global'
|
||||
end
|
||||
|
||||
local function get_value_of_string (string_str)
|
||||
if string_str:sub(1, 1) == '"' or string_str:sub(1, 1) == '\'' then
|
||||
return string_str:sub(2, -2)
|
||||
end
|
||||
assert(false)
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
return function (condition)
|
||||
if condition then return condition end
|
||||
local call_info = debug.getinfo(2)
|
||||
local tokens, body_text = get_assert_body(call_info)
|
||||
|
||||
if tokens[1].text == 'type' and tokens[2].token == 'LPAR' and tokens[3].token == 'IDENTIFIER' and tokens[4].token == 'RPAR'and tokens[5].token == 'EQ'and tokens[6].token == 'STRING' then
|
||||
local var_name = tokens[3].text
|
||||
local var_val, var_scope, is_func = get_variable(var_name, 2)
|
||||
local func_name = (is_func == '' and ' to anonymous function') or is_func and (' to \''..is_func..'\'') or ''
|
||||
error(('assertion failed! bad %s \'%s\'%s (%s expected, but got %s: %s)'):format(var_scope, var_name, func_name, get_value_of_string(tokens[6].text), type(var_val), var_val), 2)
|
||||
else
|
||||
error(('assertion failed! expression `%s` evaluated to %s'):format(body_text, condition), 2)
|
||||
end
|
||||
end
|
64
lua_lang.lua
Normal file
64
lua_lang.lua
Normal file
|
@ -0,0 +1,64 @@
|
|||
|
||||
local Lexer = require 'Lexer'
|
||||
|
||||
return Lexer {
|
||||
{ 'and', 'AND' },
|
||||
{ 'break', 'BREAK' },
|
||||
{ 'do', 'DO' },
|
||||
{ 'else', 'ELSE' },
|
||||
{ 'elseif', 'ELSEIF' },
|
||||
{ 'end', 'END' },
|
||||
{ 'for', 'FOR' },
|
||||
{ 'function', 'FUNCTION' },
|
||||
{ 'if', 'IF' },
|
||||
{ 'in', 'IN' },
|
||||
{ 'local', 'LOCAL' },
|
||||
{ 'not', 'NOT' },
|
||||
{ 'or', 'OR' },
|
||||
{ 'repeat', 'REPEAT' },
|
||||
{ 'return', 'RETURN' },
|
||||
{ 'then', 'THEN' },
|
||||
{ 'until', 'UNTIL' },
|
||||
{ 'while', 'WHILE' },
|
||||
{ '%+' , 'PLUS' },
|
||||
{ '%-' , 'MINUS' },
|
||||
{ '%*' , 'TIMES' },
|
||||
{ '%/' , 'DIVIDE' },
|
||||
{ '%%' , 'MODULO' },
|
||||
{ '%^' , 'CARET' },
|
||||
{ '%#' , 'HASHTAG' },
|
||||
{ '%==' , 'EQ' },
|
||||
{ '%~=' , 'NEQ' },
|
||||
{ '%<=' , 'LEQ' },
|
||||
{ '%>=' , 'GEQ' },
|
||||
{ '%<' , 'LE' },
|
||||
{ '%>' , 'GT' },
|
||||
{ '%=', 'ASSIGN' },
|
||||
{ '%(' , 'LPAR' },
|
||||
{ '%)' , 'RPAR' },
|
||||
{ '%{' , 'LBRACE' },
|
||||
{ '%}' , 'RBRACE' },
|
||||
{ '%;' , 'SEMICOLON' },
|
||||
{ '%,' , 'COMMA' },
|
||||
{ '%.%.' , 'CONCAT' },
|
||||
{ '%.%.%.' , 'VARARG' },
|
||||
--
|
||||
{ 'false', 'FALSE' },
|
||||
{ 'true', 'TRUE' },
|
||||
{ 'nil', 'NIL' },
|
||||
{ '%[', 'LBRACK' },
|
||||
{ '%]', 'RBRACK' },
|
||||
{ '%:', 'COLON' },
|
||||
{ '%.', 'DOT' },
|
||||
--
|
||||
{ '%d+%.?%d*', 'NUMBER' },
|
||||
{ '%d*%.?%d+', 'NUMBER' },
|
||||
{ '[%a_][%d%a_]*', 'IDENTIFIER' },
|
||||
{ '\".-\"', 'STRING' },
|
||||
{ '\'.-\'', 'STRING' },
|
||||
{ '%[%[.-%]%]', 'STRING' },
|
||||
|
||||
{ '%-%-.-\n', Lexer.CONTINUE },
|
||||
{ '%-%-%[%[.-%]%]', Lexer.CONTINUE },
|
||||
{ '%s+', Lexer.CONTINUE },
|
||||
}
|
86
test/test_assert-gooder.lua
Normal file
86
test/test_assert-gooder.lua
Normal file
|
@ -0,0 +1,86 @@
|
|||
|
||||
local SUITE = require 'TestSuite' 'assert-gooder'
|
||||
SUITE:setEnvironment {
|
||||
assert = require 'assert-gooder'
|
||||
}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
SUITE:addTest('local variable', function ()
|
||||
local _, msg = pcall(function ()
|
||||
local a = 2
|
||||
assert(type(a) == 'string')
|
||||
end)
|
||||
assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad local \'a\' (string expected, but got number: 2)', msg)
|
||||
end)
|
||||
|
||||
SUITE:addTest('upvalue variable', function ()
|
||||
local a = 2
|
||||
local _, msg = pcall(function ()
|
||||
assert(type(a) == 'string')
|
||||
end)
|
||||
assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad upvalue \'a\' (string expected, but got number: 2)', msg)
|
||||
end)
|
||||
|
||||
SUITE:addTest('global variable', function ()
|
||||
a = 2
|
||||
local _, msg = pcall(function ()
|
||||
assert(type(a) == 'string')
|
||||
end)
|
||||
assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad global \'a\' (string expected, but got number: 2)', msg)
|
||||
end)
|
||||
|
||||
SUITE:addTest('argument to anon function', function ()
|
||||
local _, msg = pcall(function (a)
|
||||
assert(type(a) == 'string')
|
||||
end, 2)
|
||||
assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad argument #1 \'a\' to anonymous function (string expected, but got number: 2)', msg)
|
||||
end)
|
||||
|
||||
SUITE:addTest('argument to named function', function ()
|
||||
local f = function (a)
|
||||
assert(type(a) == 'string')
|
||||
end
|
||||
local _, msg = pcall(function () f(2) end)
|
||||
assert_equal('./test/test_assert-gooder.lua:'..curline(-3)..': '..'assertion failed! bad argument #1 \'a\' to \'f\' (string expected, but got number: 2)', msg)
|
||||
end)
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Other assert types
|
||||
|
||||
SUITE:addTest('compare values', function ()
|
||||
local _, msg = pcall(function ()
|
||||
local a = 2
|
||||
assert(a == 4)
|
||||
end)
|
||||
assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad local \'a\' (value of 4 expected, but got: 2)', msg)
|
||||
end)
|
||||
|
||||
SUITE:addTest('compare values across types', function ()
|
||||
local _, msg = pcall(function ()
|
||||
local a = "hi"
|
||||
assert(a == 4)
|
||||
end)
|
||||
assert_equal('./test/test_assert-gooder.lua:'..curline(-2)..': '..'assertion failed! bad local \'a\' (value of 4 expected, but got string: "hi")', msg)
|
||||
end)
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
SUITE:addTest('can improve asserts in loaded strings too', function ()
|
||||
local func = loadstring "return function(a) assert(type(a) == 'string') end" ()
|
||||
local _, msg = pcall(setfenv(func, getfenv()), 42)
|
||||
assert_equal('[string "return function(a) assert(type(a) == \'string\'..."]:1: '..'assertion failed! bad argument #1 \'a\' to anonymous function (string expected, but got number: 42)', msg)
|
||||
end)
|
||||
|
||||
SUITE:addTest('really complicated expression', function ()
|
||||
local f = function ()
|
||||
assert(a == 3 and math.floor(2.522) == 2 or 5 == n)
|
||||
end
|
||||
local _, msg = pcall(f, 2)
|
||||
assert_equal('./test/test_assert-gooder.lua:'..curline(-3)..': '..'assertion failed! expression `a == 3 and math.floor(2.522) == 2 or 5 == n` evaluated to false', msg)
|
||||
end)
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
return SUITE
|
||||
|
6
test/tests.lua
Normal file
6
test/tests.lua
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
-- Util function
|
||||
|
||||
local TEST_SUITE = require "TestSuite" 'assert-gooder'
|
||||
TEST_SUITE:addModules 'test/test_*.lua'
|
||||
TEST_SUITE:runTests()
|
Loading…
Reference in New Issue
Block a user