commit aec232efcb45235ade6d1090545f94f6b21e761a Author: Jon Michael Aanes Date: Sat Oct 28 12:25:10 2017 +0200 Initial commit on assert-gooder. An improved version of `assert` that automatically creates a useful error message, when the assert fails. diff --git a/Lexer.lua b/Lexer.lua new file mode 100644 index 0000000..eabedee --- /dev/null +++ b/Lexer.lua @@ -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}) diff --git a/assert-gooder.lua b/assert-gooder.lua new file mode 100644 index 0000000..2cd7734 --- /dev/null +++ b/assert-gooder.lua @@ -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 diff --git a/lua_lang.lua b/lua_lang.lua new file mode 100644 index 0000000..0868191 --- /dev/null +++ b/lua_lang.lua @@ -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 }, +} diff --git a/test/test_assert-gooder.lua b/test/test_assert-gooder.lua new file mode 100644 index 0000000..d74d4d6 --- /dev/null +++ b/test/test_assert-gooder.lua @@ -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 + diff --git a/test/tests.lua b/test/tests.lua new file mode 100644 index 0000000..ea4acd9 --- /dev/null +++ b/test/tests.lua @@ -0,0 +1,6 @@ + +-- Util function + +local TEST_SUITE = require "TestSuite" 'assert-gooder' + TEST_SUITE:addModules 'test/test_*.lua' + TEST_SUITE:runTests()