commit 7efc973f5a23013a56e60c210ee9cfa2c23e4705
parent d81114b2d21b1276fe35c7f0e7f1c56d050bc666
Author: phoebos <ben@bvnf.space>
Date: Tue, 14 Jan 2025 15:14:05 +0000
add tests
Diffstat:
11 files changed, 663 insertions(+), 2 deletions(-)
diff --git a/Makefile b/Makefile
@@ -29,4 +29,7 @@ doc:
rm -fr doc/
ldoc .
-.PHONY: all install clean doc
+test: all
+ $(LUA) mytest.lua
+
+.PHONY: all install clean doc test
diff --git a/mytest.lua b/mytest.lua
@@ -0,0 +1,83 @@
+--- Test suite
+-- @script test
+
+local t = require("testlib")
+local posix = require("posix")
+
+-- Set up a temporary directory for ROOT
+assert(posix.chdir("tests/"))
+local tempd = posix.mkdtemp("/tmp/bliss-test-XXXXXX")
+local cachedir = tempd .. "/cache"
+local pwd = posix.getcwd()
+local repo = pwd .. "/repo"
+posix.setenv("KISS_ROOT", tempd)
+posix.setenv("XDG_CACHE_HOME", cachedir)
+posix.setenv("KISS_PATH", repo)
+
+local bliss = require "bliss"
+local tsort = require "bliss.tsort"
+local env = bliss.setup()
+
+
+-- ARCHIVE
+-- B3SUM
+local ctx = bliss.b3sum.init()
+bliss.b3sum.update(ctx, "test\n")
+t.test("b3sum", "dea2b412aa90f1b43a06", bliss.b3sum.finalize, ctx, 10)
+-- BUILD
+-- CHECKSUM
+t.test("checksum_file", "dea2b412aa90f1b43a06ca5e8b8feafec45ae1357971322749480f4e1572eaa2ea",
+ bliss.checksum_file, "testfile.txt")
+-- DOWNLOAD
+-- INSTALL
+-- LIST
+t.test("list (no packages)", nil, bliss.list, env, {})
+t.test("list (all packages)", "package1 1-1\n", bliss.list, {sys_db = pwd .. "/repo/"}, {})
+t.test("list (one package)", "package1 1-1\n", bliss.list, {sys_db = pwd .. "/repo/"}, {"package1"})
+-- PKG
+t.test("read_lines", {{"test"}}, bliss.read_lines, "testfile.txt")
+t.test("find", pwd .. "/repo/package1", bliss.find, "package1", env.PATH)
+t.test("isinstalled (not)", false, bliss.isinstalled, env, "notinstalled")
+t.test("iscached (not)", false, bliss.iscached, env, "nonexistent", {0,0})
+local repo_dir = bliss.find("package1", env.PATH)
+t.test("find_version", {"1","1"},
+ bliss.find_version, "package1", repo_dir)
+
+t.test("find_sources", {{"files/testfile"}},
+ bliss.find_sources, "package1", repo_dir)
+
+t.test("resolve (file)", {repo_dir .. "/files/testfile"},
+ bliss.resolve, "package1", {{"files/testfile"}}, env, repo_dir)
+
+t.test("resolve (http)", {cachedir .. "/kiss/sources/package1/testpath.tar.gz"},
+ bliss.resolve, "package1", {{"https://example.com/a/b/testpath.tar.gz"}}, env)
+
+t.test("resolve (http with dest)", {cachedir .. "/kiss/sources/package1/dest/testpath.tar.gz"},
+ bliss.resolve, "package1", {{"https://example.com/a/b/testpath.tar.gz", "dest"}}, env)
+
+t.test("resolve (git)", {cachedir .. "/kiss/sources/package1/gitdir.git/"},
+ bliss.resolve, "package1", {{"git+https://example.com/a/b/gitdir.git"}}, env)
+
+t.test("resolve (git with branch)", {cachedir .. "/kiss/sources/package1/gitdir.git/"},
+ bliss.resolve, "package1", {{"git+https://example.com/a/b/gitdir.git@branch#commit"}}, env)
+
+-- SEARCH
+t.test("search", pwd .. "/repo/package1\n", bliss.search, env, {"package1"})
+t.test("search (glob)", pwd .. "/repo/package1\n", bliss.search, env, {"package*"})
+-- TSORT
+local sorter = tsort.new()
+sorter:add("a", {"b", "c"})
+sorter:add("b", {"c", "d"})
+sorter:add("c", {"d"})
+t.test("tsort", {"a", "b", "c", "d"}, tsort.sort, sorter)
+-- UTILS
+t.test("split", {"a", "b", "c"}, bliss.split, "a,b,,c,", ",")
+t.test("capture", {"foobar", "barfoo"}, bliss.capture, "echo foobar; echo barfoo")
+t.test("shallowcopy", {1,2,3}, bliss.shallowcopy, {1,2,3})
+t.test("am_not_owner", "root", bliss.am_not_owner, "/")
+
+local r = t.summarise()
+t.coverage(bliss)
+
+os.execute("rm -fr "..tempd)
+_exit(r)
diff --git a/pp.lua b/pp.lua
@@ -0,0 +1,422 @@
+
+--Recursive pretty printer with optional indentation and cycle detection.
+--Written by Cosmin Apreutesei. Public Domain.
+
+if not ... then require'pp_test'; return end
+
+local type, tostring = type, tostring
+local string_format, string_dump = string.format, string.dump
+local math_huge, floor = math.huge, math.floor
+
+--pretty printing for non-structured types -----------------------------------
+
+local escapes = { --don't add unpopular escapes here
+ ['\\'] = '\\\\',
+ ['\t'] = '\\t',
+ ['\n'] = '\\n',
+ ['\r'] = '\\r',
+}
+
+local function escape_byte_long(c1, c2)
+ return string_format('\\%03d%s', c1:byte(), c2)
+end
+local function escape_byte_short(c)
+ return string_format('\\%d', c:byte())
+end
+local function quote_string(s, quote)
+ s = s:gsub('[\\\t\n\r]', escapes)
+ s = s:gsub(quote, '\\%1')
+ s = s:gsub('([^\32-\126])([0-9])', escape_byte_long)
+ s = s:gsub('[^\32-\126]', escape_byte_short)
+ return s
+end
+
+local function format_string(s, quote)
+ return string_format('%s%s%s', quote, quote_string(s, quote), quote)
+end
+
+local function write_string(s, write, quote)
+ write(quote); write(quote_string(s, quote)); write(quote)
+end
+
+local keywords = {}
+for i,k in ipairs{
+ 'and', 'break', 'do', 'else', 'elseif', 'end',
+ 'false', 'for', 'function', 'goto', 'if', 'in',
+ 'local', 'nil', 'not', 'or', 'repeat', 'return',
+ 'then', 'true', 'until', 'while',
+} do
+ keywords[k] = true
+end
+
+local function is_stringable(v)
+ if type(v) == 'table' then
+ return getmetatable(v) and getmetatable(v).__tostring and true or false
+ else
+ return type(v) == 'string'
+ end
+end
+
+local function is_identifier(v)
+ if is_stringable(v) then
+ v = tostring(v)
+ return not keywords[v] and v:find('^[a-zA-Z_][a-zA-Z_0-9]*$') ~= nil
+ else
+ return false
+ end
+end
+
+local hasinf = math_huge == math_huge - 1
+local function format_number(v)
+ if v ~= v then
+ return '0/0' --NaN
+ elseif hasinf and v == math_huge then
+ return '1/0' --writing 'math.huge' would not make it portable, just wrong
+ elseif hasinf and v == -math_huge then
+ return '-1/0'
+ elseif v == floor(v) and v >= -2^31 and v <= 2^31-1 then
+ return string_format('%d', v) --printing with %d is faster
+ else
+ return string_format('%0.17g', v)
+ end
+end
+
+local function write_number(v, write)
+ write(format_number(v))
+end
+
+local function is_dumpable(f)
+ return type(f) == 'function' and debug.getinfo(f, 'Su').what ~= 'C'
+end
+
+local function format_function(f)
+ return string_format('loadstring(%s)', format_string(string_dump(f, true)))
+end
+
+local function write_function(f, write, quote)
+ write'loadstring('; write_string(string_dump(f, true), write, quote); write')'
+end
+
+local ffi, int64, uint64
+local function is_int64(v)
+ if type(v) ~= 'cdata' then return false end
+ if not int64 then
+ ffi = require'ffi'
+ int64 = ffi.typeof'int64_t'
+ uint64 = ffi.typeof'uint64_t'
+ end
+ return ffi.istype(v, int64) or ffi.istype(v, uint64)
+end
+
+local function format_int64(v)
+ return tostring(v)
+end
+
+local function write_int64(v, write)
+ write(format_int64(v))
+end
+
+local function format_value(v, quote)
+ quote = quote or "'"
+ if v == nil or type(v) == 'boolean' then
+ return tostring(v)
+ elseif type(v) == 'number' then
+ return format_number(v)
+ elseif is_stringable(v) then
+ return format_string(tostring(v), quote)
+ elseif is_dumpable(v) then
+ return format_function(v)
+ elseif is_int64(v) then
+ return format_int64(v)
+ else
+ assert(false)
+ end
+end
+
+local function is_serializable(v)
+ return type(v) == 'nil' or type(v) == 'boolean' or type(v) == 'number'
+ or is_stringable(v) or is_dumpable(v) or is_int64(v)
+end
+
+local function write_value(v, write, quote)
+ quote = quote or "'"
+ if v == nil or type(v) == 'boolean' then
+ write(tostring(v))
+ elseif type(v) == 'number' then
+ write_number(v, write)
+ elseif is_stringable(v) then
+ write_string(tostring(v), write, quote)
+ elseif is_dumpable(v) then
+ write_function(v, write, quote)
+ elseif is_int64(v) then
+ write_int64(v, write)
+ else
+ assert(false)
+ end
+end
+
+--pretty-printing for tables -------------------------------------------------
+
+local to_string --fw. decl.
+
+local cache = setmetatable({}, {__mode = 'kv'})
+local function cached_to_string(v, parents)
+ local s = cache[v]
+ if not s then
+ s = to_string(v, nil, parents, nil, nil, nil, true)
+ cache[v] = s
+ end
+ return s
+end
+
+local function virttype(v)
+ return is_stringable(v) and 'string' or type(v)
+end
+
+local type_order = {boolean = 1, number = 2, string = 3, table = 4}
+local function cmp_func(t, parents)
+ local function cmp(a, b)
+ local ta, tb = virttype(a), virttype(b)
+ if ta == tb then
+ if ta == 'boolean' then
+ return (a and 1 or 0) < (b and 1 or 0)
+ elseif ta == 'string' then
+ return tostring(a) < tostring(b)
+ elseif ta == 'number' then
+ return a < b
+ elseif a == nil then --can happen when comparing values
+ return false
+ else
+ local sa = cached_to_string(a, parents)
+ local sb = cached_to_string(b, parents)
+ if sa == sb then --keys look the same serialized, compare values
+ return cmp(t[a], t[b])
+ else
+ return sa < sb
+ end
+ end
+ else
+ return type_order[ta] < type_order[tb]
+ end
+ end
+ return cmp
+end
+
+local function sortedpairs(t, parents)
+ local keys = {}
+ for k in pairs(t) do
+ keys[#keys+1] = k
+ end
+ table.sort(keys, cmp_func(t, parents))
+ local i = 0
+ return function()
+ i = i + 1
+ return keys[i], t[keys[i]]
+ end
+end
+
+local function is_array_index_key(k, maxn)
+ return
+ maxn > 0
+ and type(k) == 'number'
+ and k == floor(k)
+ and k >= 1
+ and k <= maxn
+end
+
+local function pretty(v, write, depth, wwrapper, indent,
+ parents, quote, line_term, onerror, sort_keys, filter)
+
+ if not filter(v) then return end
+
+ if is_serializable(v) then
+
+ write_value(v, write, quote)
+
+ elseif getmetatable(v) and getmetatable(v).__pwrite then
+
+ wwrapper = wwrapper or function(v)
+ pretty(v, write, -1, wwrapper, nil,
+ parents, quote, line_term, onerror, sort_keys, filter)
+ end
+ getmetatable(v).__pwrite(v, write, wwrapper)
+
+ elseif type(v) == 'table' then
+
+ if indent == nil then indent = '\t' end
+
+ parents = parents or {}
+ if parents[v] then
+ write(onerror and onerror('cycle', v, depth) or 'nil --[[cycle]]')
+ return
+ end
+ parents[v] = true
+
+ write'{'
+
+ local first = true
+ local t = v
+
+ local maxn = 0
+ for k,v in ipairs(t) do
+ maxn = maxn + 1
+ if filter(v, k, t) then
+ if first then
+ first = false
+ else
+ write','
+ end
+ if indent then
+ write(line_term)
+ write(indent:rep(depth))
+ end
+ pretty(v, write, depth + 1, wwrapper, indent,
+ parents, quote, line_term, onerror, sort_keys, filter)
+ end
+ end
+
+ local pairs = sort_keys ~= false and sortedpairs or pairs
+ for k,v in pairs(t, parents) do
+ if not is_array_index_key(k, maxn) and filter(v, k, t) then
+ if first then
+ first = false
+ else
+ write','
+ end
+ if indent then
+ write(line_term)
+ write(indent:rep(depth))
+ end
+ if is_stringable(k) then
+ k = tostring(k)
+ end
+ if is_identifier(k) then
+ write(k); write'='
+ else
+ write'['
+ pretty(k, write, depth + 1, wwrapper, indent,
+ parents, quote, line_term, onerror, sort_keys, filter)
+ write']='
+ end
+ pretty(v, write, depth + 1, wwrapper, indent,
+ parents, quote, line_term, onerror, sort_keys, filter)
+ end
+ end
+
+ if indent then
+ write(line_term)
+ write(indent:rep(depth-1))
+ end
+
+ write'}'
+
+ parents[v] = nil
+
+ else
+ write(onerror and onerror('unserializable', v, depth) or
+ string_format('nil --[[unserializable %s]]', type(v)))
+ end
+end
+
+local function nofilter(v) return true end
+
+local function args(opt, ...)
+ local
+ indent, parents, quote, line_term, onerror,
+ sort_keys, filter
+ if type(opt) == 'table' then
+ indent, parents, quote, line_term, onerror,
+ sort_keys, filter =
+ opt.indent, opt.parents, opt.quote, opt.line_term, opt.onerror,
+ opt.sort_keys, opt.filter
+ else
+ indent, parents, quote, line_term, onerror,
+ sort_keys, filter = opt, ...
+ end
+ line_term = line_term or '\n'
+ filter = filter or nofilter
+ return
+ indent, parents, quote, line_term, onerror,
+ sort_keys, filter
+end
+
+local function to_sink(write, v, ...)
+ return pretty(v, write, 1, nil, args(...))
+end
+
+function to_string(v, ...) --fw. declared
+ local buf = {}
+ pretty(v, function(s) buf[#buf+1] = s end, 1, nil, args(...))
+ return table.concat(buf)
+end
+
+local function to_openfile(f, v, ...)
+ pretty(v, function(s) assert(f:write(s)) end, 1, nil, args(...))
+end
+
+local function to_file(file, v, ...)
+ local glue = require'glue'
+ return glue.writefile(file, coroutine.wrap(function(...)
+ coroutine.yield'return '
+ to_sink(coroutine.yield, v, ...)
+ end, ...))
+end
+
+local function to_stdout(v, ...)
+ return to_openfile(io.stdout, v, ...)
+end
+
+local pp_skip = {
+ __index = 1,
+ __newindex = 1,
+ __mode = 1,
+}
+local function filter(v, k, t) --don't show methods and inherited objects.
+ if type(v) == 'function' then return end --skip methods.
+ if getmetatable(t) == t and pp_skip[k] then return end --skip inherits.
+ return true
+end
+local function pp(...)
+ local n = select('#',...)
+ for i = 1, n do
+ local v = select(i,...)
+ if is_stringable(v) then
+ io.stdout:write(tostring(v))
+ else
+ to_openfile(io.stdout, v, nil, nil, nil, nil, nil, nil, filter)
+ end
+ if i < n then io.stdout:write'\t' end
+ end
+ io.stdout:write'\n'
+ io.stdout:flush()
+ return ...
+end
+
+return setmetatable({
+
+ --these can be exposed too if needed:
+ --
+ --is_identifier = is_identifier,
+ --is_dumpable = is_dumpable,
+ --is_serializable = is_serializable,
+ --is_stringable = is_stringable,
+ --
+ --format_value = format_value,
+ --write_value = write_value,
+
+ write = to_sink,
+ format = to_string,
+ stream = to_openfile,
+ save = to_file,
+ load = function(file)
+ local f, err = loadfile(file)
+ if not f then return nil, err end
+ local ok, v = pcall(f)
+ if not ok then return nil, v end
+ return v
+ end,
+ pp = pp, --old API
+
+}, {__call = function(self, ...)
+ return self.pp(...)
+end})
diff --git a/test.lua b/test.lua
@@ -1,15 +1,18 @@
bliss = require "bliss"
+pp = require "pp"
function tts(t) local s = "{ " local sep = "" for k,v in pairs(t) do s = s..sep..k.."="..v sep = ", " end return s .. " }" end
env = bliss.setup()
-for k,v in pairs(env) do
+pp(env)
+--[[for k,v in pairs(env) do
if "table" == type(v) then
print(k, tts(v))
else
print(k,v)
end
end
+--]]
local ctx = bliss.b3sum.init()
bliss.b3sum.update(ctx, "test\n")
diff --git a/testlib.lua b/testlib.lua
@@ -0,0 +1,133 @@
+--- Test module
+-- @module test
+
+local pp = require("pp")
+
+local test = {}
+local counter = {failed = 0, succeeded = 0}
+local tested_funcs = {}
+
+-- catch deaths
+_exit = os.exit
+os.exit = function(a)
+ error("os.exit was called\n" .. debug.traceback(nil, 2))
+end
+
+-- monkeypatch print
+local realprint = print
+local outputs = {}
+local function printtostring(...)
+ local s = ""
+ for i = 1, select("#", ...) do
+ local ss = tostring(select(i, ...))
+ if i > 1 then
+ s = s .. "\t"
+ end
+ s = s .. ss
+ end
+ s = s
+ return s
+end
+
+local function storeoutput(append_newline, ...)
+ local s = printtostring(...)
+ if append_newline then
+ s = s .. "\n"
+ end
+ table.insert(outputs, s)
+end
+
+print = function (...)
+ storeoutput(true, ...)
+ --realprint(...)
+end
+io.write = function (...)
+ storeoutput(false, ...)
+end
+
+local function log(...)
+ local n = select("#", ...)
+ if n > 1 then
+ io.stderr:write(string.format("%-30s", select(1, ...)))
+
+ end
+ io.stderr:write(printtostring(select(n > 1 and 2 or 1, ...)).."\n")
+end
+
+local cmp, tablecmp
+tablecmp = function (a, b)
+ if a == b then return true end
+ if #a ~= #b then
+ return false
+ end
+ for k,v in pairs(a) do
+ if not cmp(b[k], v) then
+ return false
+ end
+ end
+ for k,v in pairs(b) do
+ if not cmp(a[k], v) then
+ return false
+ end
+ end
+ return true
+end
+
+cmp = function (a, b)
+ if type(a) == "table" then
+ return tablecmp(a, b)
+ else
+ return a == b
+ end
+end
+
+function test.test(name, expected, fn, ...)
+ tested_funcs[fn] = true
+ local r, s = pcall(fn, ...)
+ if not r then
+ log(name, "FAIL: exception caught: ", s)
+ counter.failed = counter.failed + 1
+ else
+ -- if nil return value, use the last line of output.
+ -- false is a valid return value.
+ if s == nil then
+ s = outputs[#outputs]
+ end
+ if not cmp(s, expected) then
+ log(name, "FAIL: expected " .. pp.format(expected) .. " but got " .. pp.format(s))
+ counter.failed = counter.failed + 1
+ else
+ log(name, "success")
+ counter.succeeded = counter.succeeded + 1
+ end
+ end
+ -- reset outputs between tests
+ outputs = {}
+ return r
+end
+
+function test.summarise()
+ log("summary: " .. counter.succeeded .. "/" .. counter.succeeded + counter.failed .. " successes")
+ return counter.failed == 0
+end
+
+function test.coverage(t)
+ -- recursively count number of functions in table t and compare to number of functions tested so far
+ local function recur_count(t)
+ local count = 0
+ for _,v in pairs(t) do
+ if type(v) == "function" then
+ count = count + 1
+ elseif type(v) == "table" then
+ count = count + recur_count(v)
+ end
+ end
+ return count
+ end
+ local total = recur_count(t)
+ local tested = 0
+ for k,v in pairs(tested_funcs) do tested = tested + 1 end
+ log("test coverage: " .. tested .. "/" .. total .. " (" .. 100 * tested / total .. "%)")
+end
+
+return test
diff --git a/tests/repo/package1/build b/tests/repo/package1/build
@@ -0,0 +1,12 @@
+#!/bin/sh -e
+
+printf "build script was given these args:"
+printf " %s" "$@"
+printf "\n"
+
+if [ -e testfile ]; then
+ printf "testfile found.\n"
+else
+ printf "testfile not found!\n"
+ exit 1
+fi
diff --git a/tests/repo/package1/checksums b/tests/repo/package1/checksums
@@ -0,0 +1 @@
+053d890ec0e0108bbfe09cf9c902118412673870ccc6804df5e6499e26b45dc27d
diff --git a/tests/repo/package1/files/testfile b/tests/repo/package1/files/testfile
@@ -0,0 +1 @@
+Test file
diff --git a/tests/repo/package1/sources b/tests/repo/package1/sources
@@ -0,0 +1 @@
+files/testfile
diff --git a/tests/repo/package1/version b/tests/repo/package1/version
@@ -0,0 +1 @@
+1 1
diff --git a/tests/testfile.txt b/tests/testfile.txt
@@ -0,0 +1 @@
+test