1
0
Fork 0

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:
Jon Michael Aanes 2017-10-28 12:25:10 +02:00
commit aec232efcb
5 changed files with 300 additions and 0 deletions

54
Lexer.lua Normal file
View 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
View 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
View 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 },
}

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

@ -0,0 +1,6 @@
-- Util function
local TEST_SUITE = require "TestSuite" 'assert-gooder'
TEST_SUITE:addModules 'test/test_*.lua'
TEST_SUITE:runTests()