diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml new file mode 100644 index 000000000..d358979c9 --- /dev/null +++ b/.forgejo/workflows/test.yml @@ -0,0 +1,9 @@ +on: [push] +jobs: + test: + runs-on: docker + container: + image: 127.0.0.1:5000/voxelibre-test:latest + steps: + - run: cd /VoxeLibre && git pull && git checkout ${{ github.sha }} + - run: cd /VoxeLibre && sh test.sh diff --git a/test.sh b/test.sh new file mode 100755 index 000000000..2fd41548c --- /dev/null +++ b/test.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +set -ex +which luarocks && eval $(luarocks path) +which luarocks-5.3 && eval $(luarocks-5.3 path) + +sh tests/luacheck/test.sh || true + +# Run unit tests +find ./ -name 'unit-test.lua' | while read TEST; do + ( + cd $(dirname $TEST) + busted unit-test.lua + ) +done diff --git a/tests/docker/Dockerfile b/tests/docker/Dockerfile new file mode 100644 index 000000000..45d45ccba --- /dev/null +++ b/tests/docker/Dockerfile @@ -0,0 +1,19 @@ +FROM alpine + +RUN apk add --no-cache --virtual=build-dependencies build-base lua5.3-dev luajit-dev && \ + apk add --no-cache luajit luarocks5.3 git && \ + luarocks-5.3 install busted && \ + luarocks-5.3 install luacov && \ + luarocks-5.3 install luacheck && \ + luarocks-5.3 install luaposix && \ + git clone https://github.com/AlberTajuelo/bitop-lua.git && \ + cd bitop-lua && \ + sed -i 's/"lua >= 5.1, < 5.3"/"lua >= 5.1"/' bitop-lua-1.0-0.rockspec && \ + luarocks-5.3 make && \ + apk del --purge build-dependencies +ENTRYPOINT ["/bin/sh"] + +RUN git config --global http.postBuffer 52428800 && \ + git clone REPO && \ + apk add minetest x11vnc xdotool + diff --git a/tests/docker/build.sh b/tests/docker/build.sh new file mode 100755 index 000000000..195ecb51b --- /dev/null +++ b/tests/docker/build.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +echo "Enter repo: " +read REPO + +sed -i "s#REPO#$REPO#" Dockerfile +docker build . -t 127.0.0.1:5000/voxelibre-test:latest +git restore Dockerfile + diff --git a/tests/lib/README.md b/tests/lib/README.md new file mode 100644 index 000000000..f6961e0f9 --- /dev/null +++ b/tests/lib/README.md @@ -0,0 +1 @@ +`misc_helpers.lua` was copied from [Luanti 5.10.0](https://github.com/minetest/minetest/blob/568f7a8e8fb457c7b7bcfd3211c7f3f0481ed2e7/builtin/common/misc_helpers.lua), available under the GNU Lesser Public License (see https://github.com/minetest/minetest/blob/568f7a8e8fb457c7b7bcfd3211c7f3f0481ed2e7/LICENSE.txt) diff --git a/tests/lib/misc_helpers.lua b/tests/lib/misc_helpers.lua new file mode 100644 index 000000000..ce4179f54 --- /dev/null +++ b/tests/lib/misc_helpers.lua @@ -0,0 +1,772 @@ +-------------------------------------------------------------------------------- +-- Localize functions to avoid table lookups (better performance). +local string_sub, string_find = string.sub, string.find +local math = math + +-------------------------------------------------------------------------------- +local function basic_dump(o) + local tp = type(o) + if tp == "number" then + return tostring(o) + elseif tp == "string" then + return string.format("%q", o) + elseif tp == "boolean" then + return tostring(o) + elseif tp == "nil" then + return "nil" + -- Uncomment for full function dumping support. + -- Not currently enabled because bytecode isn't very human-readable and + -- dump's output is intended for humans. + --elseif tp == "function" then + -- return string.format("loadstring(%q)", string.dump(o)) + elseif tp == "userdata" then + return tostring(o) + else + return string.format("<%s>", tp) + end +end + +local keywords = { + ["and"] = true, + ["break"] = true, + ["do"] = true, + ["else"] = true, + ["elseif"] = true, + ["end"] = true, + ["false"] = true, + ["for"] = true, + ["function"] = true, + ["goto"] = true, -- Lua 5.2 + ["if"] = true, + ["in"] = true, + ["local"] = true, + ["nil"] = true, + ["not"] = true, + ["or"] = true, + ["repeat"] = true, + ["return"] = true, + ["then"] = true, + ["true"] = true, + ["until"] = true, + ["while"] = true, +} +local function is_valid_identifier(str) + if not str:find("^[a-zA-Z_][a-zA-Z0-9_]*$") or keywords[str] then + return false + end + return true +end + +-------------------------------------------------------------------------------- +-- Dumps values in a line-per-value format. +-- For example, {test = {"Testing..."}} becomes: +-- _["test"] = {} +-- _["test"][1] = "Testing..." +-- This handles tables as keys and circular references properly. +-- It also handles multiple references well, writing the table only once. +-- The dumped argument is internal-only. + +function dump2(o, name, dumped) + name = name or "_" + -- "dumped" is used to keep track of serialized tables to handle + -- multiple references and circular tables properly. + -- It only contains tables as keys. The value is the name that + -- the table has in the dump, eg: + -- {x = {"y"}} -> dumped[{"y"}] = '_["x"]' + dumped = dumped or {} + if type(o) ~= "table" then + return string.format("%s = %s\n", name, basic_dump(o)) + end + if dumped[o] then + return string.format("%s = %s\n", name, dumped[o]) + end + dumped[o] = name + -- This contains a list of strings to be concatenated later (because + -- Lua is slow at individual concatenation). + local t = {} + for k, v in pairs(o) do + local keyStr + if type(k) == "table" then + if dumped[k] then + keyStr = dumped[k] + else + -- Key tables don't have a name, so use one of + -- the form _G["table: 0xFFFFFFF"] + keyStr = string.format("_G[%q]", tostring(k)) + -- Dump key table + t[#t + 1] = dump2(k, keyStr, dumped) + end + else + keyStr = basic_dump(k) + end + local vname = string.format("%s[%s]", name, keyStr) + t[#t + 1] = dump2(v, vname, dumped) + end + return string.format("%s = {}\n%s", name, table.concat(t)) +end + +-------------------------------------------------------------------------------- +-- This dumps values in a one-statement format. +-- For example, {test = {"Testing..."}} becomes: +-- [[{ +-- test = { +-- "Testing..." +-- } +-- }]] +-- This supports tables as keys, but not circular references. +-- It performs poorly with multiple references as it writes out the full +-- table each time. +-- The indent field specifies a indentation string, it defaults to a tab. +-- Use the empty string to disable indentation. +-- The dumped and level arguments are internal-only. + +function dump(o, indent, nested, level) + local t = type(o) + if not level and t == "userdata" then + -- when userdata (e.g. player) is passed directly, print its metatable: + return "userdata metatable: " .. dump(getmetatable(o)) + end + if t ~= "table" then + return basic_dump(o) + end + + -- Contains table -> true/nil of currently nested tables + nested = nested or {} + if nested[o] then + return "" + end + nested[o] = true + indent = indent or "\t" + level = level or 1 + + local ret = {} + local dumped_indexes = {} + for i, v in ipairs(o) do + ret[#ret + 1] = dump(v, indent, nested, level + 1) + dumped_indexes[i] = true + end + for k, v in pairs(o) do + if not dumped_indexes[k] then + if type(k) ~= "string" or not is_valid_identifier(k) then + k = "["..dump(k, indent, nested, level + 1).."]" + end + v = dump(v, indent, nested, level + 1) + ret[#ret + 1] = k.." = "..v + end + end + nested[o] = nil + if indent ~= "" then + local indent_str = "\n"..string.rep(indent, level) + local end_indent_str = "\n"..string.rep(indent, level - 1) + return string.format("{%s%s%s}", + indent_str, + table.concat(ret, ","..indent_str), + end_indent_str) + end + return "{"..table.concat(ret, ", ").."}" +end + +-------------------------------------------------------------------------------- +function string.split(str, delim, include_empty, max_splits, sep_is_pattern) + delim = delim or "," + if delim == "" then + error("string.split separator is empty", 2) + end + max_splits = max_splits or -2 + local items = {} + local pos, len = 1, #str + local plain = not sep_is_pattern + max_splits = max_splits + 1 + repeat + local np, npe = string_find(str, delim, pos, plain) + np, npe = (np or (len+1)), (npe or (len+1)) + if (not np) or (max_splits == 1) then + np = len + 1 + npe = np + end + local s = string_sub(str, pos, np - 1) + if include_empty or (s ~= "") then + max_splits = max_splits - 1 + items[#items + 1] = s + end + pos = npe + 1 + until (max_splits == 0) or (pos > (len + 1)) + return items +end + +-------------------------------------------------------------------------------- +function table.indexof(list, val) + for i, v in ipairs(list) do + if v == val then + return i + end + end + return -1 +end + +-------------------------------------------------------------------------------- +function table.keyof(tb, val) + for k, v in pairs(tb) do + if v == val then + return k + end + end + return nil +end + +-------------------------------------------------------------------------------- +function string:trim() + return self:match("^%s*(.-)%s*$") +end + +local formspec_escapes = { + ["\\"] = "\\\\", + ["["] = "\\[", + ["]"] = "\\]", + [";"] = "\\;", + [","] = "\\,", + ["$"] = "\\$", +} +function core.formspec_escape(text) + -- Use explicit character set instead of dot here because it doubles the performance + return text and string.gsub(text, "[\\%[%];,$]", formspec_escapes) +end + + +local hypertext_escapes = { + ["\\"] = "\\\\", + ["<"] = "\\<", + [">"] = "\\>", +} +function core.hypertext_escape(text) + return text and text:gsub("[\\<>]", hypertext_escapes) +end + + +function core.wrap_text(text, max_length, as_table) + local result = {} + local line = {} + if #text <= max_length then + return as_table and {text} or text + end + + local line_length = 0 + for word in text:gmatch("%S+") do + if line_length > 0 and line_length + #word + 1 >= max_length then + -- word wouldn't fit on current line, move to next line + table.insert(result, table.concat(line, " ")) + line = {word} + line_length = #word + else + table.insert(line, word) + line_length = line_length + 1 + #word + end + end + + table.insert(result, table.concat(line, " ")) + return as_table and result or table.concat(result, "\n") +end + +-------------------------------------------------------------------------------- + +if INIT == "game" then + local dirs1 = {9, 18, 7, 12} + local dirs2 = {20, 23, 22, 21} + + function core.rotate_and_place(itemstack, placer, pointed_thing, + infinitestacks, orient_flags, prevent_after_place) + orient_flags = orient_flags or {} + + local unode = core.get_node_or_nil(pointed_thing.under) + if not unode then + return + end + local undef = core.registered_nodes[unode.name] + local sneaking = placer and placer:get_player_control().sneak + if undef and undef.on_rightclick and not sneaking then + return undef.on_rightclick(pointed_thing.under, unode, placer, + itemstack, pointed_thing) + end + local fdir = placer and core.dir_to_facedir(placer:get_look_dir()) or 0 + + local above = pointed_thing.above + local under = pointed_thing.under + local iswall = (above.y == under.y) + local isceiling = not iswall and (above.y < under.y) + + if undef and undef.buildable_to then + iswall = false + end + + if orient_flags.force_floor then + iswall = false + isceiling = false + elseif orient_flags.force_ceiling then + iswall = false + isceiling = true + elseif orient_flags.force_wall then + iswall = true + isceiling = false + elseif orient_flags.invert_wall then + iswall = not iswall + end + + local param2 = fdir + if iswall then + param2 = dirs1[fdir + 1] + elseif isceiling then + if orient_flags.force_facedir then + param2 = 20 + else + param2 = dirs2[fdir + 1] + end + else -- place right side up + if orient_flags.force_facedir then + param2 = 0 + end + end + + local old_itemstack = ItemStack(itemstack) + local new_itemstack = core.item_place_node(itemstack, placer, + pointed_thing, param2, prevent_after_place) + return infinitestacks and old_itemstack or new_itemstack + end + + +-------------------------------------------------------------------------------- +--Wrapper for rotate_and_place() to check for sneak and assume Creative mode +--implies infinite stacks when performing a 6d rotation. +-------------------------------------------------------------------------------- + core.rotate_node = function(itemstack, placer, pointed_thing) + local name = placer and placer:get_player_name() or "" + local invert_wall = placer and placer:get_player_control().sneak or false + return core.rotate_and_place(itemstack, placer, pointed_thing, + core.is_creative_enabled(name), + {invert_wall = invert_wall}, true) + end +end + +-------------------------------------------------------------------------------- +function core.explode_table_event(evt) + if evt ~= nil then + local parts = evt:split(":") + if #parts == 3 then + local t = parts[1]:trim() + local r = tonumber(parts[2]:trim()) + local c = tonumber(parts[3]:trim()) + if type(r) == "number" and type(c) == "number" + and t ~= "INV" then + return {type=t, row=r, column=c} + end + end + end + return {type="INV", row=0, column=0} +end + +-------------------------------------------------------------------------------- +function core.explode_textlist_event(evt) + if evt ~= nil then + local parts = evt:split(":") + if #parts == 2 then + local t = parts[1]:trim() + local r = tonumber(parts[2]:trim()) + if type(r) == "number" and t ~= "INV" then + return {type=t, index=r} + end + end + end + return {type="INV", index=0} +end + +-------------------------------------------------------------------------------- +function core.explode_scrollbar_event(evt) + local retval = core.explode_textlist_event(evt) + + retval.value = retval.index + retval.index = nil + + return retval +end + +-------------------------------------------------------------------------------- +function core.rgba(r, g, b, a) + return a and string.format("#%02X%02X%02X%02X", r, g, b, a) or + string.format("#%02X%02X%02X", r, g, b) +end + +-------------------------------------------------------------------------------- +function core.pos_to_string(pos, decimal_places) + local x = pos.x + local y = pos.y + local z = pos.z + if decimal_places ~= nil then + x = string.format("%." .. decimal_places .. "f", x) + y = string.format("%." .. decimal_places .. "f", y) + z = string.format("%." .. decimal_places .. "f", z) + end + return "(" .. x .. "," .. y .. "," .. z .. ")" +end + +-------------------------------------------------------------------------------- +function core.string_to_pos(value) + if value == nil then + return nil + end + + value = value:match("^%((.-)%)$") or value -- strip parentheses + + local x, y, z = value:trim():match("^([%d.-]+)[,%s]%s*([%d.-]+)[,%s]%s*([%d.-]+)$") + if x and y and z then + x = tonumber(x) + y = tonumber(y) + z = tonumber(z) + return vector.new(x, y, z) + end + + return nil +end + + +-------------------------------------------------------------------------------- + +do + local rel_num_cap = "(~?-?%d*%.?%d*)" -- may be overly permissive as this will be tonumber'ed anyways + local num_delim = "[,%s]%s*" + local pattern = "^" .. table.concat({rel_num_cap, rel_num_cap, rel_num_cap}, num_delim) .. "$" + + local function parse_area_string(pos, relative_to) + local pp = {} + pp.x, pp.y, pp.z = pos:trim():match(pattern) + return core.parse_coordinates(pp.x, pp.y, pp.z, relative_to) + end + + function core.string_to_area(value, relative_to) + local p1, p2 = value:match("^%((.-)%)%s*%((.-)%)$") + if not p1 then + return + end + + p1 = parse_area_string(p1, relative_to) + p2 = parse_area_string(p2, relative_to) + + if p1 == nil or p2 == nil then + return + end + + return p1, p2 + end +end + +-------------------------------------------------------------------------------- +function table.copy(t, seen) + local n = {} + seen = seen or {} + seen[t] = n + for k, v in pairs(t) do + n[(type(k) == "table" and (seen[k] or table.copy(k, seen))) or k] = + (type(v) == "table" and (seen[v] or table.copy(v, seen))) or v + end + return n +end + + +function table.insert_all(t, other) + if table.move then -- LuaJIT + return table.move(other, 1, #other, #t + 1, t) + end + for i=1, #other do + t[#t + 1] = other[i] + end + return t +end + + +function table.key_value_swap(t) + local ti = {} + for k,v in pairs(t) do + ti[v] = k + end + return ti +end + + +function table.shuffle(t, from, to, random) + from = from or 1 + to = to or #t + random = random or math.random + local n = to - from + 1 + while n > 1 do + local r = from + n-1 + local l = from + random(0, n-1) + t[l], t[r] = t[r], t[l] + n = n-1 + end +end + + +-------------------------------------------------------------------------------- +-- mainmenu only functions +-------------------------------------------------------------------------------- +if core.gettext then -- for client and mainmenu + function fgettext_ne(text, ...) + text = core.gettext(text) + local arg = {n=select('#', ...), ...} + if arg.n >= 1 then + -- Insert positional parameters ($1, $2, ...) + local result = '' + local pos = 1 + while pos <= text:len() do + local newpos = text:find('[$]', pos) + if newpos == nil then + result = result .. text:sub(pos) + pos = text:len() + 1 + else + local paramindex = + tonumber(text:sub(newpos+1, newpos+1)) + result = result .. text:sub(pos, newpos-1) + .. tostring(arg[paramindex]) + pos = newpos + 2 + end + end + text = result + end + return text + end + + function fgettext(text, ...) + return core.formspec_escape(fgettext_ne(text, ...)) + end +end + +local ESCAPE_CHAR = string.char(0x1b) + +function core.get_color_escape_sequence(color) + return ESCAPE_CHAR .. "(c@" .. color .. ")" +end + +function core.get_background_escape_sequence(color) + return ESCAPE_CHAR .. "(b@" .. color .. ")" +end + +function core.colorize(color, message) + local lines = tostring(message):split("\n", true) + local color_code = core.get_color_escape_sequence(color) + + for i, line in ipairs(lines) do + lines[i] = color_code .. line + end + + return table.concat(lines, "\n") .. core.get_color_escape_sequence("#ffffff") +end + + +function core.strip_foreground_colors(str) + return (str:gsub(ESCAPE_CHAR .. "%(c@[^)]+%)", "")) +end + +function core.strip_background_colors(str) + return (str:gsub(ESCAPE_CHAR .. "%(b@[^)]+%)", "")) +end + +function core.strip_colors(str) + return (str:gsub(ESCAPE_CHAR .. "%([bc]@[^)]+%)", "")) +end + +local function translate(textdomain, str, num, ...) + local start_seq + if textdomain == "" and num == "" then + start_seq = ESCAPE_CHAR .. "T" + elseif num == "" then + start_seq = ESCAPE_CHAR .. "(T@" .. textdomain .. ")" + else + start_seq = ESCAPE_CHAR .. "(T@" .. textdomain .. "@" .. num .. ")" + end + local arg = {n=select('#', ...), ...} + local end_seq = ESCAPE_CHAR .. "E" + local arg_index = 1 + local translated = str:gsub("@(.)", function(matched) + local c = string.byte(matched) + if string.byte("1") <= c and c <= string.byte("9") then + local a = c - string.byte("0") + if a ~= arg_index then + error("Escape sequences in string given to core.translate " .. + "are not in the correct order: got @" .. matched .. + "but expected @" .. tostring(arg_index)) + end + if a > arg.n then + error("Not enough arguments provided to core.translate") + end + arg_index = arg_index + 1 + return ESCAPE_CHAR .. "F" .. arg[a] .. ESCAPE_CHAR .. "E" + elseif matched == "n" then + return "\n" + else + return matched + end + end) + if arg_index < arg.n + 1 then + error("Too many arguments provided to core.translate") + end + return start_seq .. translated .. end_seq +end + +function core.translate(textdomain, str, ...) + return translate(textdomain, str, "", ...) +end + +function core.translate_n(textdomain, str, str_plural, n, ...) + assert (type(n) == "number") + assert (n >= 0) + assert (math.floor(n) == n) + + -- Truncate n if too large + local max = 1000000 + if n >= 2 * max then + n = n % max + max + end + if n == 1 then + return translate(textdomain, str, "1", ...) + else + return translate(textdomain, str_plural, tostring(n), ...) + end +end + +function core.get_translator(textdomain) + return + (function(str, ...) return core.translate(textdomain or "", str, ...) end), + (function(str, str_plural, n, ...) return core.translate_n(textdomain or "", str, str_plural, n, ...) end) +end + +-------------------------------------------------------------------------------- +-- Returns the exact coordinate of a pointed surface +-------------------------------------------------------------------------------- +function core.pointed_thing_to_face_pos(placer, pointed_thing) + -- Avoid crash in some situations when player is inside a node, causing + -- 'above' to equal 'under'. + if vector.equals(pointed_thing.above, pointed_thing.under) then + return pointed_thing.under + end + + local eye_height = placer:get_properties().eye_height + local eye_offset_first = placer:get_eye_offset() + local node_pos = pointed_thing.under + local camera_pos = placer:get_pos() + local pos_off = vector.multiply( + vector.subtract(pointed_thing.above, node_pos), 0.5) + local look_dir = placer:get_look_dir() + local offset, nc + local oc = {} + + for c, v in pairs(pos_off) do + if nc or v == 0 then + oc[#oc + 1] = c + else + offset = v + nc = c + end + end + + local fine_pos = {[nc] = node_pos[nc] + offset} + camera_pos.y = camera_pos.y + eye_height + eye_offset_first.y / 10 + local f = (node_pos[nc] + offset - camera_pos[nc]) / look_dir[nc] + + for i = 1, #oc do + fine_pos[oc[i]] = camera_pos[oc[i]] + look_dir[oc[i]] * f + end + return fine_pos +end + +function core.string_to_privs(str, delim) + assert(type(str) == "string") + delim = delim or ',' + local privs = {} + for _, priv in pairs(string.split(str, delim)) do + privs[priv:trim()] = true + end + return privs +end + +function core.privs_to_string(privs, delim) + assert(type(privs) == "table") + delim = delim or ',' + local list = {} + for priv, bool in pairs(privs) do + if bool then + list[#list + 1] = priv + end + end + table.sort(list) + return table.concat(list, delim) +end + +function core.is_nan(number) + return number ~= number +end + +--[[ Helper function for parsing an optionally relative number +of a chat command parameter, using the chat command tilde notation. + +Parameters: +* arg: String snippet containing the number; possible values: + * "": return as number + * "~": return relative_to + + * "~": return relative_to + * Anything else will return `nil` +* relative_to: Number to which the `arg` number might be relative to + +Returns: +A number or `nil`, depending on `arg. + +Examples: +* `core.parse_relative_number("5", 10)` returns 5 +* `core.parse_relative_number("~5", 10)` returns 15 +* `core.parse_relative_number("~", 10)` returns 10 +]] +function core.parse_relative_number(arg, relative_to) + if not arg then + return nil + elseif arg == "~" then + return relative_to + elseif string.sub(arg, 1, 1) == "~" then + local number = tonumber(string.sub(arg, 2)) + if not number then + return nil + end + if core.is_nan(number) or number == math.huge or number == -math.huge then + return nil + end + return relative_to + number + else + local number = tonumber(arg) + if core.is_nan(number) or number == math.huge or number == -math.huge then + return nil + end + return number + end +end + +--[[ Helper function to parse coordinates that might be relative +to another position; supports chat command tilde notation. +Intended to be used in chat command parameter parsing. + +Parameters: +* x, y, z: Parsed x, y, and z coordinates as strings +* relative_to: Position to which to compare the position + +Syntax of x, y and z: +* "": return as number +* "~": return + player position on this axis +* "~": return player position on this axis + +Returns: a vector or nil for invalid input or if player does not exist +]] +function core.parse_coordinates(x, y, z, relative_to) + if not relative_to then + x, y, z = tonumber(x), tonumber(y), tonumber(z) + return x and y and z and { x = x, y = y, z = z } + end + local rx = core.parse_relative_number(x, relative_to.x) + local ry = core.parse_relative_number(y, relative_to.y) + local rz = core.parse_relative_number(z, relative_to.z) + return rx and ry and rz and { x = rx, y = ry, z = rz } +end diff --git a/tests/luacheck/test.lua b/tests/luacheck/test.lua new file mode 100755 index 000000000..188c44d7a --- /dev/null +++ b/tests/luacheck/test.lua @@ -0,0 +1,147 @@ +#!/usr/bin/lua + +local luacheck = "luacheck" + +core = {} +dofile("tests/lib/misc_helpers.lua") + +local WHITESPACE = { + [" "] = true, + ["\t"] = true, + ["\r"] = true, + ["\n"] = true, +} +function string.strip(str) + local start = 0 + local stop + for i = 1,#str do + if not WHITESPACE[str:sub(i,i)] then + start = i + break + end + end + for i = 1,#str do + local p = #str - i + 1 + if not WHITESPACE[str:sub(p,p)] then + stop = #str - i + 1 + break + end + end + + return str:sub(start, stop) +end + +function read_mod_configuration(file) + local parts = file:split("/") + parts[#parts] = nil + + local conf = { + file = file, + dir = table.concat(parts,"/").."/", + } + + local f = io.open(file) + for line in f:lines() do + local parts = string.split(line,"=") + if parts and #parts >= 2 then + local key = parts[1]:strip() + local value = parts[2]:strip() + conf[key] = value + end + end + f:close() + return conf +end + +function read_mod_data() + local mods = {} + local mod_names = {} + + while true do + local line = io.read() + if not line or line == "EOF" then break end + + local mod_conf = read_mod_configuration(line) + local name = mod_conf.name + if name then + table.insert(mod_names, name) + mods[name] = mod_conf + else + print("echo 'Warning! "..line.." doesn't declare module name'") + end + end + + return mods,mod_names +end + +function get_deps_for_mod(mod, mods) + local function add_deps_for_mod(mod, mods, deps, seen) + if not mods[mod] then return end + + local dep_list = mods[mod].depends + if dep_list then + local depends = dep_list:split(",") + for i = 1,#depends do + local depend = depends[i]:strip() + if not seen[depend] then + seen[depend] = true + table.insert(deps, depend) + add_deps_for_mod(depend, mods, deps, seen) + end + end + end + + local dep_list = mods[mod].optional_depends + if dep_list then + local depends = dep_list:split(",") + for i = 1,#depends do + local depend = depends[i]:strip() + if not seen[depend] then + seen[depend] = true + table.insert(deps, depend) + add_deps_for_mod(depend, mods, deps, seen) + end + end + end + + local dep_list = mods[mod].luacheck_globals + if dep_list then + local globals = dep_list:split(",") + for i = 1,#globals do + local global = globals[i]:strip() + if not seen["global: "..global] then + seen["global: "..global] = true + table.insert(deps, global) + end + end + end + end + local deps = {mod} + local seen = {[mod] = true} + add_deps_for_mod(mod, mods, deps, seen) + return deps +end + +local mods,mod_names = read_mod_data() +for i = 1,#mod_names do + local mod = mod_names[i] + local config = mods[mod] + + local deps = get_deps_for_mod(mod, mods) + + local cmd_options = " --no-color" + for i = 1,#deps do + cmd_options = cmd_options .. " --globals "..deps[i] + end + + local filename = "" + print("echo Checking "..config.name.." located at "..config.dir) + print("(") + print("\tcd "..config.dir) + print("\tfor FILE in *.lua; do") + print("\t\techo "..luacheck.." $FILE"..cmd_options) + print("\t\t"..luacheck.." $FILE"..cmd_options) + print("\tdone") + print(")") +end + diff --git a/tests/luacheck/test.sh b/tests/luacheck/test.sh new file mode 100755 index 000000000..9cd6dd3cf --- /dev/null +++ b/tests/luacheck/test.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +LUACHECK=$HOME/.luarocks/bin/luacheck + +( + find ./ -name mod.conf + echo EOF +) | lua5.3 tests/luacheck/test.lua | sh +