diff options
author | Adam Harrison <adamdharrison@gmail.com> | 2022-11-22 15:56:17 -0500 |
---|---|---|
committer | Adam Harrison <adamdharrison@gmail.com> | 2022-11-22 15:56:17 -0500 |
commit | efe7590e7d786e4a82d6d291a1313cabe76690f1 (patch) | |
tree | 7c1c88ce37e53c577b67e36301219f9c1a0e748c | |
parent | 086cbe2a095b52bf44f2ac5e361152cb8e77dc1b (diff) | |
download | lite-xl-plugin-manager-efe7590e7d786e4a82d6d291a1313cabe76690f1.tar.gz lite-xl-plugin-manager-efe7590e7d786e4a82d6d291a1313cabe76690f1.zip |
Running tests.
-rw-r--r-- | .github/workflows/build.yml | 3 | ||||
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | src/lpm.lua | 70 | ||||
-rw-r--r-- | t/run.lua | 458 |
4 files changed, 516 insertions, 16 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 468bbb4..a3bab9c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,6 +27,9 @@ jobs: run: | sudo apt-get install mingw-w64 && ./build.sh clean && CC=x86_64-w64-mingw32-gcc AR=x86_64-w64-mingw32-gcc-ar WINDRES=x86_64-w64-mingw32-windres LZMA_CONFIGURE="--host=x86_64-w64-mingw32" ARCHIVE_CONFIGURE="-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_SYSTEM_NAME=Windows" CURL_CONFIGURE="-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_SYSTEM_NAME=Windows" GIT2_CONFIGURE="-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY -DBUILD_CLAR=OFF -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_SYSTEM_NAME=Windows -DDLLTOOL=x86_64-w64-mingw32-dlltool" SSL_CONFIGURE="no-tests mingw" ./build.sh -DLPM_VERSION='"'$VERSION-x86_64-windows-`git rev-parse --short HEAD`'"' && zip -r lpm-$VERSION-x86_64-windows.zip lpm.exe cp lpm.exe lpm.x86_64-windows.exe + - name: Run Tests + run: | + gcc -O3 -Ilib/lua lib/lua/onelua.c -DMAKE_LUA -DLUA_USE_LINUX -lm -o lua && ./lua t/run.lua - name: Create Release(s) env: { GITHUB_TOKEN: "${{ github.token }}" } run: | @@ -1,4 +1,5 @@ lpm +lua lpm-* *.o lpm.lua.c diff --git a/src/lpm.lua b/src/lpm.lua index 8dfc8d8..b915762 100644 --- a/src/lpm.lua +++ b/src/lpm.lua @@ -369,6 +369,7 @@ function common.slice(t, i, l) local n = {} for j = i, l ~= nil and (i - l) or # function common.join(j, l) local s = "" for i, v in ipairs(l) do if i > 1 then s = s .. j .. v else s = v end end return s end function common.sort(t, f) table.sort(t, f) return t end function common.write(path, contents) local f, err = io.open(path, "wb") if not f then error("can't write to " .. path .. ": " .. err) end f:write(contents) f:flush() end +function common.read(path) local f, err = io.open(path, "rb") if not f then error("can't read from " .. path .. ": " .. err) end return f:read("*all") end function common.split(splitter, str) local o = 1 local res = {} @@ -494,7 +495,14 @@ local function match_version(version, pattern) end - +-- There can exist many different versions of a plugin. All statuses are relative to a particular lite bottle. +-- available: Plugin is available in a repository, and can be installed. There is no comparable version on the system. +-- upgradable: Plugin is installed, but does not match the highest version in any repository. +-- orphan: Plugin is installed, but there is no corresponding plugin in any repository. +-- installed: Plugin is installed, and matches the highest version in any repository, or highest version is incompatible. +-- core: Plugin is a part of the lite data directory, and doesn't have corresponding plugins in any repository. +-- bundled: Plugin is part of the lite data directory, but has corresponding plugins in any repository. +-- incompatible: Plugin is not installed and conflicts with existing installed plugins. function Plugin.__index(self, idx) return rawget(self, idx) or Plugin[idx] end function Plugin.new(repository, metadata) local type = metadata.type or "plugin" @@ -517,6 +525,21 @@ function Plugin.new(repository, metadata) return self end +-- Determines whether two plugins located at different paths are actually different based on their contents. +function Plugin.is_path_different(path1, path2) + if path1:find("%.lua$") then + if not path2:find("%.lua$") then return true end + local stat1, stat2 = system.stat(path1), system.stat(path2) + if not stat1 or not stat2 or stat1.size ~= stat2.size then return true end + if system.hash(path1, "file") ~= system.hash(path2, "file") then return true end + else + if path2:find("%.lua$") then return true end + return false + end + return false +end + + function Plugin:get_install_path(bottle) local folder = self.type == "library" and "libraries" or "plugins" local path = (((self:is_core(bottle) or self:is_bundled()) and bottle.lite_xl.datadir_path) or (bottle.local_path and (bottle.local_path .. PATHSEP .. "user") or USERDIR)) .. PATHSEP .. folder .. PATHSEP .. (self.path and common.basename(self.path):gsub("%.lua$", "") or self.name) @@ -526,7 +549,13 @@ end function Plugin:is_core(bottle) return self.type == "core" end function Plugin:is_bundled(bottle) return self.type == "bundled" end -function Plugin:is_installed(bottle) return self:is_core(bottle) or (bottle.lite_xl:is_compatible(self) and system.stat(self:get_install_path(bottle))) end +function Plugin:is_installed(bottle) + if self:is_core(bottle) or self:is_bundled(bottle) or not self.repository then return true end + local install_path = self:get_install_path(bottle) + if not system.stat(install_path) then return false end + if #common.grep({ bottle:get_plugin(self.name, nil, { }) }, function(plugin) return not plugin.repository end) > 0 then return false end + return not Plugin.is_path_different(install_path, self.local_path) +end function Plugin:is_incompatible(plugin) return (self.dependencies[plugin.name] and not match_version(plugin.version, self.dependencies[plugin.name] and self.dependencies[plugin.name].version)) or (self.conflicts[plugin.name] and match_version(plugin.version, self.conflicts[plugin.name] and self.conflicts[plugin.name].version)) @@ -703,7 +732,7 @@ function Repository:parse_manifest(already_pulling) if system.stat(self.local_path) and system.stat(self.local_path .. PATHSEP .. (self.commit or self.branch)) then self.manifest_path = self.local_path .. PATHSEP .. (self.commit or self.branch) .. PATHSEP .. "manifest.json" if not system.stat(self.manifest_path) then self:generate_manifest() end - self.manifest = json.decode(io.open(self.manifest_path, "rb"):read("*all")) + self.manifest = json.decode(common.read(self.manifest_path)) self.plugins = {} self.remotes = {} for i, metadata in ipairs(self.manifest["plugins"] or {}) do @@ -969,7 +998,12 @@ local function get_repository_plugins() local t, hash = { }, { } for i,p in ipairs(common.flat_map(repositories, function(r) return r.plugins end)) do local id = p.name .. ":" .. p.version - if not hash[id] then table.insert(t, p) hash[id] = p hash[p.name] = p end + if not hash[id] then + table.insert(t, p) + hash[id] = p + if not hash[p.name] then hash[p.name] = {} end + table.insert(hash[p.name], p ) + end end return t, hash end @@ -981,17 +1015,21 @@ function Bottle:all_plugins() self.lite_xl.datadir_path .. PATHSEP .. "plugins" } for i, plugin_path in ipairs(common.grep(plugin_paths, function(e) return system.stat(e) end)) do - for k, v in ipairs(common.grep(system.ls(plugin_path), function(e) return i == 2 or not hash[e:gsub("%.lua$", "")] end)) do + for j, v in ipairs(system.ls(plugin_path)) do local name = v:gsub("%.lua$", "") - table.insert(t, Plugin.new(nil, { - name = name, - type = i == 2 and (hash[name] and "bundled" or "core"), - organization = (v:find("%.lua$") and "singleton" or "complex"), - mod_version = self.lite_xl.mod_version, - path = "plugins/" .. v, - version = "1.0", - description = (hash[name] and hash[name].description or nil) - })) + local path = plugin_path .. PATHSEP .. v + local matching = hash[name] and common.grep(hash[name], function(e) return not Plugin.is_path_different(path, e.local_path) end)[1] + if i == 2 or not hash[name] or not matching then + table.insert(t, Plugin.new(nil, { + name = name, + type = i == 2 and (hash[name] and "bundled" or "core"), + organization = (v:find("%.lua$") and "singleton" or "complex"), + mod_version = self.lite_xl.mod_version, + path = "plugins" .. PATHSEP .. v, + version = "1.0", + description = (hash[name] and hash[name][1].description or nil) + })) + end end end return t @@ -1439,7 +1477,7 @@ Usage: lpm COMMAND [...ARGUMENTS] [--json] [--userdir=directory] [--cachedir=directory] [--quiet] [--version] [--help] [--remotes] [--ssl_certs=directory/file] [--force] [--arch=]] .. _G.ARCH .. [[] [--assume-yes] [--no-install-optional] [--verbose] [--mod-version=3] - [--datadir=directory] + [--datadir=directory] [--binary=path] LPM is a package manager for `lite-xl`, written in C (and packed-in lua). @@ -1594,7 +1632,7 @@ Flags have the following effects: end end if system.stat(CACHEDIR .. PATHSEP .. "lite_xls" .. PATHSEP .. "locals.json") then - for i, lite_xl in ipairs(json.decode(io.open(CACHEDIR .. PATHSEP .. "lite_xls" .. PATHSEP .. "locals.json", "rb"):read("*all"))) do + for i, lite_xl in ipairs(json.decode(common.read(CACHEDIR .. PATHSEP .. "lite_xls" .. PATHSEP .. "locals.json"))) do table.insert(lite_xls, LiteXL.new(nil, { version = lite_xl.version, mod_version = lite_xl.mod_version, path = lite_xl.path, tags = { "local" } })) end end diff --git a/t/run.lua b/t/run.lua new file mode 100644 index 0000000..d99e417 --- /dev/null +++ b/t/run.lua @@ -0,0 +1,458 @@ +setmetatable(_G, { __index = function(t, k) if not rawget(t, k) then error("cannot get undefined global variable: " .. k, 2) end end, __newindex = function(t, k) error("cannot set global variable: " .. k, 2) end }) + +-- Begin rxi JSON library. +local json = { _version = "0.1.2" } +local encode +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + +local tmpdir = os.getenv("TMPDIR") or "/tmp" +local fast = os.getenv("FAST") +local last_command_result, last_command +local userdir = tmpdir .. "/lpmtest" +local function lpm(cmd) + last_command = "./lpm --quiet --json --userdir=" .. userdir .. " " .. cmd + local pipe = io.popen(last_command, "r") + last_command_result = json.decode(pipe:read("*all")) + local success = pipe:close() + if not success then error("error calling lpm", 2) end + return last_command_result +end + +local function assert_exists(path) + if not io.open(path, "rb") then error("assertion failed: file " .. path .. " does not exist", 2) end +end +local function assert_not_exists(path) + if io.open(path, "rb") then error("assertion failed: file " .. path .. " exists", 2) end +end + +local function run_tests(tests, arg) + local fail_count = 0 + local names = {} + if #arg == 0 then + for k,v in pairs(tests) do table.insert(names, k) end + else + names = arg + end + table.sort(names) + local max_name = 0 + for i,k in ipairs(names) do max_name = math.max(max_name, #k) end + for i,k in ipairs(names) do + local v = tests[k] + if fast then + os.execute("rm -rf " .. tmpdir .. "/lpmtest/plugins && mkdir -p " .. tmpdir .. "/lpmtest"); + else + os.execute("rm -rf " .. tmpdir .. "/lpmtest && mkdir -p " .. tmpdir .. "/lpmtest"); + end + io.stdout:write(string.format("test %-" .. (max_name + 1) .. "s: ", k)) + local failed = false + xpcall(v, function(err) + print("[FAIL]: " .. err) + print() + print() + print("Last Command: " .. last_command) + print(json.encode(last_command_result)) + fail_count = fail_count + 1 + failed = true + end) + if not failed then + print("[PASSED]") + end + end + os.exit(fail_count) +end + +local tests = { + ["00_install_singleton"] = function() + local plugins = lpm("list bracketmatch")["plugins"] + assert(#plugins == 1) + assert(plugins[1].organization == "singleton") + assert(plugins[1].status == "available") + local actions = lpm("install bracketmatch")["actions"] + assert(actions[1]:find("Installing singleton")) + assert_exists(userdir .. "/plugins/bracketmatch.lua") + actions = lpm("uninstall bracketmatch")["actions"] + assert_not_exists(userdir .. "/plugins/bracketmatch.lua") + end, + ["01_upgrade_singleton"] = function() + lpm("install bracketmatch") + local plugins = lpm("list bracketmatch")["plugins"] + assert(#plugins == 1) + assert(plugins[1].status == "installed") + assert_exists(plugins[1].path) + io.open(plugins[1].path, "ab"):write("-- this is a test comment to modify the checksum"):close() + plugins = lpm("list bracketmatch")["plugins"] + assert(#plugins == 2) + lpm("install bracketmatch") + plugins = lpm("list bracketmatch")["plugins"] + assert(#plugins == 1) + end, + ["02_install_complex"] = function() + local plugins = lpm("list plugin_manager")["plugins"] + assert(#plugins == 1) + assert(plugins[1].organization == "complex") + assert(plugins[1].status == "available") + local actions = lpm("install plugin_manager")["actions"] + assert_exists(userdir .. "/libraries/json.lua") + assert_exists(userdir .. "/plugins/plugin_manager") + assert_exists(userdir .. "/plugins/plugin_manager/init.lua") + actions = lpm("uninstall plugin_manager")["actions"] + assert_not_exists(userdir .. "/plugins/plugin_manager") + end, + ["03_upgrade_complex"] = function() + local actions = lpm("install plugin_manager") + local plugins = lpm("list plugin_manager")["plugins"] + assert(#plugins == 1) + assert(plugins[1].organization == "complex") + assert(plugins[1].status == "installed") + end, +} + + +run_tests(tests, arg) |