bliss

KISS in Lua
git clone git://bvnf.space/bliss.git
Log | Files | Refs | README | LICENSE

commit 7efc973f5a23013a56e60c210ee9cfa2c23e4705
parent d81114b2d21b1276fe35c7f0e7f1c56d050bc666
Author: phoebos <ben@bvnf.space>
Date:   Tue, 14 Jan 2025 15:14:05 +0000

add tests

Diffstat:
MMakefile | 5++++-
Amytest.lua | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
App.lua | 422+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest.lua | 5++++-
Atestlib.lua | 133+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/repo/package1/build | 12++++++++++++
Atests/repo/package1/checksums | 1+
Atests/repo/package1/files/testfile | 1+
Atests/repo/package1/sources | 1+
Atests/repo/package1/version | 1+
Atests/testfile.txt | 1+
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