diff --git a/errors.lua b/errors.lua index 0b1baab..63cd7d0 100644 --- a/errors.lua +++ b/errors.lua @@ -1,7 +1,9 @@ +-- Check for possible loading errors + local string_dist do - local thispath = ... and select('1', ...):match('.+%.') or '' + local thispath = ... and (...):match('.+%.') or '' local function import (name, ignore_failure) local was_loaded, lib_or_error = pcall(require, thispath..name) if not was_loaded then @@ -14,16 +16,26 @@ do string_dist = import 'string_distance' end +if not debug then + error('[errors/loading]: Expected the debug library to be available in _G.') +elseif not debug.getinfo then + error('[errors/loading]: Expected debug.getinfo to be available in the debug library.') +end -assert(debug and debug.getinfo) +-------------------------------------------------------------------------------- +-- Constants + +local DEFAULT_ERROR_MSG_NORMAL = 'Unknown error occured' +local DEFAULT_ERROR_MSG_INTERNAL = 'Unknown internal error occured' +local DEFAULT_ERROR_MSG_CORRECT = 'Unexpected input "%s", maybe you meant %s?' -------------------------------------------------------------------------------- -- Util local function format_probable_strings (probable_strings, amount) assert(type(probable_strings) == 'table') - assert(type(amount) == 'number') assert(#probable_strings > 0) + assert(type(amount) == 'number') local l = {} for i = 1, math.min(amount, #probable_strings) do @@ -36,70 +48,96 @@ local function format_probable_strings (probable_strings, amount) end -------------------------------------------------------------------------------- --- Error handler -local ErrorHandler = setmetatable({}, {__call = function (c, ...) return c.new(...) end}) - ErrorHandler.__index = ErrorHandler +local function internal_error (self, module_suffix, format_msg, ...) + -- internal_error(error_handler, module_suffix [, format_msg, ...]) -function ErrorHandler.new (module_name) - return setmetatable({module_name = module_name, registered = {}}, ErrorHandler) + -- Error check + assert(type(self) == 'table' and self.is_error_handler) + assert(type(module_suffix) == 'string') + format_msg = format_msg or DEFAULT_ERROR_MSG_NORMAL + assert(type(format_msg) == 'string') + -- Format + return error(('[%s%s]: '..format_msg):format(self.module_name, module_suffix, ...), 3) end -function ErrorHandler:register (f) - assert(type(self) == 'table') - assert(type(f) == 'function') - self.registered[f] = true +local function external_error (self, module_suffix, format_msg, ...) + -- external_error(error_handler, module_suffix [, format_msg, ...]) + + -- Error check + assert(type(self) == 'table' and self.is_error_handler) + assert(type(module_suffix) == 'string') + format_msg = format_msg or DEFAULT_ERROR_MSG_INTERNAL + assert(type(format_msg) == 'string') + -- Find error level + local level = 3 + while self.registered[debug.getinfo(level, 'f').func] do + level = level + 1 + end + -- Format + return error(('[%s%s]: '..format_msg):format(self.module_name, module_suffix, ...), level -1) end -local ERROR_MODES = { ['internal'] = true, ['external'] = true } +local function correct_error (self, module_suffix, format_msg, gotten_string, possible_strings) + -- correct_error(error_handler, module_suffix [, format_msg], gotten_string, possible_strings) -function ErrorHandler:getErrorFunc (mode, name) + -- Error check + assert(type(self) == 'table' and self.is_error_handler) + assert(type(module_suffix) == 'string') + format_msg = format_msg or DEFAULT_ERROR_MSG_CORRECT + assert(type(format_msg) == 'string') + assert(type(gotten_string) == 'string') + assert(type(possible_strings) == 'table') - -- Parameter fixing - local mode = mode or 'internal' - local name = name or (mode == 'external' and '') or mode - if name ~= '' then name = '/'..name end + -- Do stuff + local possible_strings = string_dist.strings_with_highest_similarity(gotten_string, possible_strings) + local list_string = format_probable_strings(possible_strings, 3) + -- Format + return error(('[%s%s]: '..format_msg):format(self.module_name, module_suffix, gotten_string, list_string), 3) +end - -- Error checking - assert(ERROR_MODES[mode]) - assert(type(name) == 'string') - -- Create error funcs. - if mode == 'internal' then - return function (format_msg, ...) - assert(type(format_msg) == 'string') - return error(('[%s%s]: '..format_msg):format(self.module_name, name, ...), 2) - end - elseif mode == 'external' then - return function (format_msg, ...) - assert(type(format_msg) == 'string') - local level = 2 - while self.registered[debug.getinfo(level, 'f').func] do - level = level + 1 - end - return error(('[%s%s]: '..format_msg):format(self.module_name, name, ...), level) +local ErrorHandler_mt = {__call = function (self, ...) internal_error(self, '', ...) end} + + +return function (module_name) + assert(type(module_name) == 'string') + + local err_hdl = setmetatable({}, ErrorHandler_mt) + err_hdl.is_error_handler = true + err_hdl.module_name = module_name + err_hdl.registered = {} + err_hdl.any_registered = false + + function err_hdl.internal (...) + local args = {...} + if args[1] == err_hdl then table.remove(args, 1) end + internal_error(err_hdl, '/internal', unpack(args)) + end + + function err_hdl.external (...) + local args = {...} + if args[1] == err_hdl then table.remove(args, 1) end + external_error(err_hdl, '', unpack(args)) + end + + function err_hdl.correct (...) + local args = {...} + if args[1] == err_hdl then table.remove(args, 1) end + if #args == 2 then table.insert(args, 1, DEFAULT_ERROR_MSG_CORRECT) end + correct_error(err_hdl, '', unpack(args)) + end + + function err_hdl.register (...) + local args = {...} + if args[1] == err_hdl then table.remove(args, 1) end + assert(#args > 0) + for i = 1, #args do + assert(type(args[i]) == 'function') + err_hdl.registered[args[i]] = true + err_hdl.any_registered = true end end - assert(false) + + return err_hdl end - -function ErrorHandler:attemptCorrection (format_msg, gotten_string, probable_strings) - assert(type(self) == 'table', 'Wow, mister, remember to call with OO notation.') - assert(type(format_msg) == 'string') - assert(type(gotten_string) == 'string') - assert(type(probable_strings) == 'table') - - local probable_strings = string_dist.strings_with_highest_similarity(gotten_string, probable_strings) - local list_string = format_probable_strings(probable_strings, 3) - return self:getErrorFunc('internal', '')(format_msg, gotten_string, list_string) -end - -function ErrorHandler:__call (format_msg, ...) - return self:getErrorFunc('internal')(format_msg, ...) -end - --------------------------------------------------------------------------------- - -return { - ErrorHandler = ErrorHandler -} diff --git a/init.lua b/init.lua index 57d1a64..83d91e4 100644 --- a/init.lua +++ b/init.lua @@ -1,2 +1,2 @@ -return require 'errors.errors' +return require (((...) ~= 'init' and (...) .. '.' or '') .. 'errors') diff --git a/test/test_errors.lua b/test/test_errors.lua index c42a5c0..637f50f 100644 --- a/test/test_errors.lua +++ b/test/test_errors.lua @@ -1,69 +1,95 @@ -local error_handler = require('errors').ErrorHandler 'errors_test' - -local SUITE = require('TestSuite').new('errors') +local SUITE = require 'TestSuite' 'errors' SUITE:setEnviroment { - error_handler = error_handler, - external_error = error_handler:getErrorFunc 'external', - internal_error = error_handler:getErrorFunc 'internal', + error = require 'errors' 'test_errors' } -------------------------------------------------------------------------------- --- Basic errors functionallity +-- Basic errors functions -SUITE:addTest('errors basic functionallity', function() - local func_a = function () external_error 'Hello World' end - local func_b, func_b_line = function () func_a() end, curline() - error_handler:register(func_a) - - local status, error_msg = pcall(func_b) - assert_equal(false, status) - local expected_error = './test/test_errors.lua:'..func_b_line..': [errors_test]: Hello World' - assert_equal(expected_error, error_msg) +SUITE:addTest('Basic error', function() + local expected_error = './test/test_errors.lua:'..curline(1)..': [test_errors]: Hello World' + assert_equal(expected_error, select(2, pcall(error, 'Hello World'))) end) -SUITE:addTest('errors deep nesting works', function() - local func_a = function () external_error 'Hello World' end - local func_b, func_b_line = function () func_a() end, curline() - local func_c, func_c_line = function () func_b() end, curline() - error_handler:register(func_a) - error_handler:register(func_b) - - local status, error_msg = pcall(func_c) - assert_equal(false, status) - local expected_error = './test/test_errors.lua:'..func_c_line..': [errors_test]: Hello World' - assert_equal(expected_error, error_msg) +SUITE:addTest('Basic formatting', function() + local expected_error = './test/test_errors.lua:'..curline(1)..': [test_errors]: Welcome to the Errors' + assert_equal(expected_error, select(2, pcall(error, '%s to the %s', 'Welcome', 'Errors'))) end) -SUITE:addTest('internal errors also works', function() - local func_a, func_a_line = function () internal_error 'Hello World' end, curline() - local func_b, func_b_line = function () func_a() end, curline() - local func_c, func_c_line = function () func_b() end, curline() - error_handler:register(func_a) - error_handler:register(func_b) - - local status, error_msg = pcall(func_c) - assert_equal(false, status) - local expected_error = './test/test_errors.lua:'..func_a_line..': [errors_test/internal]: Hello World' - assert_equal(expected_error, error_msg) +SUITE:addTest('Default generic error', function() + local expected_error = './test/test_errors.lua:'..curline(1)..': [test_errors]: Unknown error occured' + assert_equal(expected_error, select(2, pcall(error))) end) -SUITE:addTest('errors modes can be overwritten', function() - local extra_error = error_handler:getErrorFunc('external', 'extra') - local func, func_line = function () extra_error 'Hi' end, curline() - - local status, error_msg = pcall(func) - local expected_error = './test/test_errors.lua:'..func_line..': [errors_test/extra]: Hi' - assert_equal(expected_error, error_msg) +SUITE:addTest('Internal is almost the same as error in itself', function() + local expected_error = './test/test_errors.lua:'..curline(1)..': [test_errors/internal]: World Hello' + assert_equal(expected_error, select(2, pcall(error.internal, 'World Hello'))) end) -SUITE:addTest('errors modes can be overwritten, including internal', function() - local extra_error = error_handler:getErrorFunc('internal', 'extra') - local func, func_line = function () extra_error 'Hi' end, curline() +SUITE:addTest('Internal can also be called using object index', function() + local expected_error = './test/test_errors.lua:'..curline(1)..': [test_errors/internal]: André the Giant' + assert_equal(expected_error, select(2, pcall(error.internal, error, 'André the Giant'))) +end) - local status, error_msg = pcall(func) - local expected_error = './test/test_errors.lua:'..func_line..': [errors_test/extra]: Hi' - assert_equal(expected_error, error_msg) +SUITE:addTest('Internal includes the stack', function() + local func_a = function() error.internal 'Thomas the Steam Train' end + local expected_error = './test/test_errors.lua:'..curline(-1)..': [test_errors/internal]: Thomas the Steam Train' + assert_equal(expected_error, select(2, pcall(func_a))) +end) + +SUITE:addTest('Internal includes the stack 2', function() + local func_a = function() error.internal 'Thomas the Steam Train' end + local func_b = function() return pcall(func_a) end + local expected_error = './test/test_errors.lua:'..curline(-2)..': [test_errors/internal]: Thomas the Steam Train' + assert_equal(expected_error, select(2, pcall(func_a))) +end) + +-------------------------------------------------------------------------------- +-- External + +SUITE:addTest('External errors hides the stack', function() + local func_a = function () error.external 'Hello World' end + local func_b = function () func_a() end + error.register(func_a) + + local expected_error = './test/test_errors.lua:'..curline(-3)..': [test_errors]: Hello World' + assert_equal(expected_error, select(2, pcall(func_b))) +end) + +SUITE:addTest('External errors can be triggered deep', function() + local func_a = function () error.external 'Hello World' end + local func_b = function () func_a() end + local func_c = function () func_b() end + error.register(func_a, func_b) + + local expected_error = './test/test_errors.lua:'..curline(-3)..': [test_errors]: Hello World' + assert_equal(expected_error, select(2, pcall(func_c))) +end) + +SUITE:addTest('Internal errors don\'t hide the stack', function() + local func_a = function () error.internal 'Hello World' end + local func_b = function () func_a() end + local func_c = function () func_b() end + error.register(func_a, func_b) + + local expected_error = './test/test_errors.lua:'..curline(-5)..': [test_errors/internal]: Hello World' + assert_equal(expected_error, select(2, pcall(func_c))) +end) + +-------------------------------------------------------------------------------- +-- Correcting errors + +SUITE:addTest('Correct errors helps with autocorrection', function() + local _, err_msg = pcall(error.correct, 'Not %s, maybe: %s', 'help', {'Help', 'whelp', 'halp'}) + local expected_error = './test/test_errors.lua:'..curline(-1)..': [test_errors]: Not help, maybe: Help, whelp or halp' + assert_equal(expected_error, err_msg) +end) + +SUITE:addTest('Correct errors has nice default string', function() + local _, err_msg = pcall(error.correct, 'Vlard', {'Vlad', 'Question Mark', 'Frankenstein'}) + local expected_error = './test/test_errors.lua:'..curline(-1)..': [test_errors]: Unexpected input "Vlard", maybe you meant Vlad, Question Mark or Frankenstein?' + assert_equal(expected_error, err_msg) end) -------------------------------------------------------------------------------- diff --git a/test/test_string_distance.lua b/test/test_string_distance.lua index 0a1dfe9..64645d4 100644 --- a/test/test_string_distance.lua +++ b/test/test_string_distance.lua @@ -1,5 +1,5 @@ -local SUITE = require('TestSuite').new('string_distance') +local SUITE = require 'TestSuite' 'string_distance' SUITE:setEnviroment { levenshtein = require('string_distance').levenshtein, longest_common_subsequence = require('string_distance').longest_common_subsequence, diff --git a/test/tests.lua b/test/tests.lua index d309267..5890d0f 100644 --- a/test/tests.lua +++ b/test/tests.lua @@ -1,7 +1,7 @@ package.path = package.path .. ';./test/?.lua;./src/?.lua' -local TEST_SUITE = require("TestSuite").new('errors') - TEST_SUITE:addModules('test/test_*') +local TEST_SUITE = require 'TestSuite' 'errors' + TEST_SUITE:addModules 'test/test_*' TEST_SUITE:setOptions(...) TEST_SUITE:runTests()