From d88f9c978327a0543240f8ae58d7725170a3f68b Mon Sep 17 00:00:00 2001 From: HiPhish Date: Sat, 9 Mar 2024 15:08:41 +0100 Subject: [PATCH] feat(core): Implement loader for Fennel Tests can be written in Fennel if `fennel` is among the loaders. Currently the traceback is wrong for failures and pending tests, but it is wrong for tests written in Moonscript as well, so this seems to be a general problem with busted. --- busted-scm-1.rockspec | 1 + busted/modules/files/fennel.lua | 102 ++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 busted/modules/files/fennel.lua diff --git a/busted-scm-1.rockspec b/busted-scm-1.rockspec index 751f682e..43febd9f 100644 --- a/busted-scm-1.rockspec +++ b/busted-scm-1.rockspec @@ -82,6 +82,7 @@ build = { ['busted.modules.files.lua'] = 'busted/modules/files/lua.lua', ['busted.modules.files.moonscript'] = 'busted/modules/files/moonscript.lua', ['busted.modules.files.terra'] = 'busted/modules/files/terra.lua', + ['busted.modules.files.fennel'] = 'busted/modules/files/fennel.lua', ['busted.outputHandlers.base'] = 'busted/outputHandlers/base.lua', ['busted.outputHandlers.utfTerminal'] = 'busted/outputHandlers/utfTerminal.lua', diff --git a/busted/modules/files/fennel.lua b/busted/modules/files/fennel.lua new file mode 100644 index 00000000..f72fd245 --- /dev/null +++ b/busted/modules/files/fennel.lua @@ -0,0 +1,102 @@ +-- NOTE: the traceback of `pending` and `error` is broken, and there does not +-- seem to be anything we can do about it. The Moonscript loader has the same +-- problem. + + +local path = require 'pl.path' +local ok, fennel = pcall(require, 'fennel') + +local MESSAGE_PATTERN = '^([^\n]-):(%d+): (.*)' +local MESSAGE_TEMPLATE = '%s:%d: %s' +local source_maps = {} + +local ret = {} + +-- A Fennel variant of the standard Lua loadstring function; registers a source +-- map for the given chunk as a side effect. +local function loadstring_(str, chunk_name) + local options = {filename = chunk_name:sub(2)} + local success, lua, src_map = pcall(fennel.compileString, str, options) + if not success then + return nil, lua, nil + end + -- NOTE: `loadstring` was deprecated in favour of a more general `load` in + -- Lua 5.2 + local thunk, err = (loadstring or load)(lua, chunk_name) + return thunk, err, src_map +end + +-- A Fennel variant of the standard Lua loadfile function +local function loadfile_(fname) + local file, err = io.open(fname) + if not (file) then + return nil, err + end + local text = assert(file:read("*a")) + file:close() + return loadstring_(text, '@' .. tostring(fname)) +end + +-- Sometimes Fennel gives files like [string "./filename.fnl"], so we'll chop +-- it up to only get the filename. +local rewrite_filename = function(filename) + return filename:match('string "(.+)"') or filename +end + +-- Maps a Lua line number to the corresponding Fennel line number using a +-- previously stored source map. +local rewrite_linenumber = function(fname, lineno) + local src_map = source_maps['@' .. fname] + if not fname or not src_map then return lineno end + + local entry = src_map[lineno] + if not entry then return lineno end + + -- Assume that all lines of a Lua file are stored in the same Fennel file; + -- the format of all entries is {file_name, line_number} + return entry[2] +end + +local getTrace = function(filename, info) + -- NOTE: `info` is the result of `debug.getinfo(level, 'Sl')`, enriched with + -- `traceback` and `message`. The `traceback` is result of + -- `debug.traceback('', level)` + + local index = info.traceback:find('\n%s*%[C]') + info.traceback = info.traceback:sub(1, index) + + info.short_src = rewrite_filename(info.short_src) + info.linedefined = rewrite_linenumber(filename, info.linedefined) + info.currentline = rewrite_linenumber(filename, info.currentline) + + return info +end + +local rewriteMessage = function(filename, message) + local fname, lineno, msg = message:match(MESSAGE_PATTERN) + if not fname then + return message + end + + fname = rewrite_filename(fname) + lineno = rewrite_linenumber(fname, tonumber(lineno)) + + return MESSAGE_TEMPLATE:format(fname, tostring(lineno), msg) +end + +ret.match = function(busted, filename) + local result = ok and path.extension(filename) == '.fnl' + return result +end + +ret.load = function(busted, filename) + local file, err, src_map = loadfile_(filename) + if not file then + busted.publish({'error', 'file'}, {descriptor = 'file', name = filename}, nil, err, {}) + else + source_maps[assert(src_map).key] = src_map + end + return file, getTrace, rewriteMessage +end + +return ret