aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/build.yml3
-rw-r--r--.gitignore1
-rw-r--r--src/lpm.lua70
-rw-r--r--t/run.lua458
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: |
diff --git a/.gitignore b/.gitignore
index 52dce0a..839171d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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)