diff --git a/.github/rockspec.template b/.github/rockspec.template new file mode 100644 index 0000000..63d9596 --- /dev/null +++ b/.github/rockspec.template @@ -0,0 +1,38 @@ +local git_ref = '$git_ref' +local modrev = '$modrev' +local specrev = '$specrev' + +local repo_url = '$repo_url' + +rockspec_format = '3.0' +package = '$package' +version = modrev ..'-'.. specrev + +description = { + summary = '$summary', + detailed = $detailed_description, + labels = $labels, + homepage = '$homepage', + $license +} + +dependencies = $dependencies + +test_dependencies = $test_dependencies + +source = { + url = repo_url .. '/archive/' .. git_ref .. '.zip', + dir = '$repo_name-' .. '$archive_dir_suffix', +} + +if modrev == 'scm' or modrev == 'dev' then + source = { + url = repo_url:gsub('https', 'git') + } +end + +build = { + type = 'builtin', + copy_directories = $copy_directories, + modules = { ["sh.env"] = "nix/env.c" } +} diff --git a/.github/workflows/luarocks.yml b/.github/workflows/luarocks.yml index dcb7ca6..1408302 100644 --- a/.github/workflows/luarocks.yml +++ b/.github/workflows/luarocks.yml @@ -23,10 +23,11 @@ jobs: - name: Get Version run: echo "LUAROCKS_VERSION=$(git describe --abbrev=0 --tags)" >> $GITHUB_ENV - name: Luarocks Upload - uses: nvim-neorocks/luarocks-tag-release@v7 + uses: lumen-oss/luarocks-tag-release@v7 env: LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }} with: + template: ".github/rockspec.template" version: ${{ env.LUAROCKS_VERSION }} detailed_description: | Tiny library with syntax sugar for shell scripting in Lua diff --git a/.github/workflows/nix-build.yml b/.github/workflows/nix-build.yml index 12b96f2..19af516 100644 --- a/.github/workflows/nix-build.yml +++ b/.github/workflows/nix-build.yml @@ -1,11 +1,22 @@ -name: "Nix build" +name: Tests + on: - pull_request: push: + branches: [ master ] + pull_request: + branches: [ master ] + jobs: - checks: - runs-on: ubuntu-latest + test: + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v6 - - uses: DeterminateSystems/nix-installer-action@v22 - - run: nix flake check -Lv + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + - name: Run checks + run: nix flake check -Lvv --log-format bar-with-logs diff --git a/.gitignore b/.gitignore index 09130e8..914c89e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ **/result +**/result-* **/repl-result-out +**/repl-result-* **/.nvim.lua **/.cache **/compile_commands.json diff --git a/README.md b/README.md index 05451b9..d398030 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ It is useful when you have a short build or wrapper script that needs to deal wi Especially when you have a lot of `json` and would rather use `cjson` and deal with tables than use `jq` and bash arrays +Alongside all that, it also includes `require('sh.env')` which works like `vim.env` + ## Install via luarocks: `luarocks install shelua` @@ -247,23 +249,15 @@ inputs.shelua = { The library is exported by the flake under `inputs.shelua.packages.${system}` as `default`, `shelua5_1`, `shelua5_2`, `shelua5_3`, `shelua5_4`, and `sheluajit_2_1`. -You may import any of them for any nixpkgs lua interpreter like this if you don't want to match them up. - -```nix -luaEnv = pkgs.lua5_2.withPackages (ps: [(inputs.shelua.packages.${system}.default.override { luapkgs = ps; })]); -``` - It also exports overlays. See the [flake](./flake.nix) for more details. ### In addition to the library: It exports a `inputs.shelua.legacyPackages.${system}.runLuaCommand` which is a lot like `pkgs.runCommand` except the command is in lua. -`runLuaCommand :: str -> str -> attrs or (n2l -> attrs) -> str or (n2l -> str) -> drv` - -where `n2l` is [this nix to lua library](https://github.com/BirdeeHub/nixToLua) +`runLuaCommand :: str -> str -> attrs -> str -> drv` -and the rest representing: +with those arguments representing: `runLuaCommand :: name -> lua_interpreter_path -> drvArgs -> lua_command -> drv` @@ -291,10 +285,9 @@ You should provide the interpreter path via something like this to get the most - `os.write_file(opts, filename, contents)` will be added where opts is `{ append = false, newline = true }` by default -- `os.env` will be added. Setting values in the table will set the environment variable +- `os.env` will be added as an alias for `require('sh.env')` Setting values in the table will set the environment variable in the process environment, setting one to nil will unset it, reading will return the environment variable's value. - Analogous to `vim.env` in neovim. - The path to the shell hooks will be added to the shelua library's metatable (via `sh.stdenv_shell_hooks_path = shell_hooks`), for use in redefining the existing transform which adds them if desired. diff --git a/default.nix b/default.nix index a4e2a81..37b6206 100644 --- a/default.nix +++ b/default.nix @@ -1,12 +1,4 @@ -{ - runCommand, - lua, - luapkgs ? lua.pkgs, - ... -}: luapkgs.luaLib.toLuaModule (runCommand "shelua" { - src = ./lua/sh.lua; - env_path = with builtins; (head (split "[\/][?]" (head luapkgs.lua.LuaPathSearchPaths))); -} /*bash*/ '' - mkdir -p "$out/$env_path" - cp "$src" "$out/$env_path/sh.lua" -'') +{ pkgs ? import {}, ... }: (import ./flake.nix).outputs { + self = builtins.path { path = ./.; }; + inherit pkgs; +} diff --git a/flake.lock b/flake.lock index c8d587b..e058c01 100644 --- a/flake.lock +++ b/flake.lock @@ -1,20 +1,5 @@ { "nodes": { - "n2l": { - "locked": { - "lastModified": 1748756411, - "narHash": "sha256-jmfRRsxCVV+/2KnDVrMG/RBKFOARDpSrYICqQ6vnSjw=", - "owner": "BirdeeHub", - "repo": "nixToLua", - "rev": "a701038ccfe76833f678e3c643a2b0f4291c1cea", - "type": "github" - }, - "original": { - "owner": "BirdeeHub", - "repo": "nixToLua", - "type": "github" - } - }, "nixpkgs": { "locked": { "lastModified": 1777826146, @@ -33,7 +18,6 @@ }, "root": { "inputs": { - "n2l": "n2l", "nixpkgs": "nixpkgs" } } diff --git a/flake.nix b/flake.nix index e1339c5..9143305 100644 --- a/flake.nix +++ b/flake.nix @@ -1,105 +1,98 @@ { - description = '' - Tiny lua module to write shell scripts with lua (inspired by zserge/luash) - - Also exports runLuaCommand, which is pkgs.runCommand but the command is in lua and uses shelua - ''; - inputs = { - nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; - n2l.url = "github:BirdeeHub/nixToLua"; - }; - outputs = { nixpkgs, n2l, ... }: let - forAllSys = nixpkgs.lib.genAttrs nixpkgs.lib.platforms.all; - overlay = final: prev: { - shelua = prev.callPackage ./. { lua = prev.lua5_2; }; - runLuaCommand = prev.callPackage ./nix { inherit n2l; }; - }; - overlay1 = final: prev: { - shelua = prev.callPackage ./. { lua = prev.lua5_2; }; + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + outputs = { self, ... }@inputs: let + lib = inputs.pkgs.lib or inputs.nixpkgs.lib or (import "${inputs.nixpkgs or }/lib"); + forAllSys = lib.genAttrs lib.platforms.all; + getPkgs = system: overlays: if inputs.pkgs.stdenv.hostPlatform.system or null == system then + if builtins.isList overlays && overlays != [] then + inputs.pkgs.appendOverlays overlays + else + inputs.pkgs + else + import (inputs.pkgs.path or inputs.nixpkgs or ) { + inherit system; + overlays = (if builtins.isList overlays then overlays else []) ++ inputs.pkgs.overlays or []; + config = inputs.pkgs.config or {}; + }; + mapAttrsToList = f: attrs: builtins.attrValues (builtins.mapAttrs f attrs); + l_pkg_enum = { + lua5_1 = "lua51Packages"; + lua5_2 = "lua52Packages"; + lua5_3 = "lua53Packages"; + lua5_4 = "lua54Packages"; + lua5_5 = "lua55Packages"; + luajit = "luajitPackages"; + lua = "luaPackages"; }; - overlay2 = final: prev: { - runLuaCommand = prev.callPackage ./nix { inherit n2l; }; + APPNAME = "shelua"; + overlay = final: prev: let + luaCallPackageFn = { buildLuarocksPackage, }: + buildLuarocksPackage { + pname = APPNAME; + version = "scm-1"; + src = self; + }; + # lua5_1 = prev.lua5_1.override { packageOverrides }; + l_pkg_main = builtins.mapAttrs ( + n: _: (prev.lib.attrByPath [ n "override" ] null prev) { + packageOverrides = luaself: luaprev: { + ${APPNAME} = luaself.callPackage luaCallPackageFn {}; + }; + } + ) l_pkg_enum; + # lua51Packages = final.lua5_1.pkgs; + l_pkg_sets = builtins.listToAttrs ( + mapAttrsToList ( + n: v: { + name = v; + value = prev.lib.attrByPath [ n "pkgs" ] null final; + } + ) l_pkg_enum + ); + in l_pkg_main // l_pkg_sets // { + vimPlugins = prev.vimPlugins // { + ${APPNAME} = (final.neovimUtils.buildNeovimPlugin { pname = APPNAME; }).overrideAttrs { + luarocksConfig = { + lua_modules_path = "lua"; + lib_modules_path = "lua"; + }; + }; + }; }; - in { - overlays.default = overlay; - overlays.shelua = overlay1; - overlays.runLuaCommand = overlay2; - legacyPackages = forAllSys (system: let - pkgs = import nixpkgs { inherit system; overlays = [ overlay2 ]; }; - in { - inherit (pkgs) runLuaCommand; - }); packages = forAllSys (system: let - pkgs = import nixpkgs { inherit system; overlays = [ overlay1 ]; }; - in nixpkgs.lib.pipe (with pkgs; [ lua5_1 lua5_2 lua5_3 lua5_4 luajit ]) [ - (builtins.map (li: { name = "she" + li.luaAttr; value = pkgs.shelua.override { lua = li; }; })) - builtins.listToAttrs - ] // { - default = pkgs.shelua; + pkgs = getPkgs system [ overlay ]; + in ( + with builtins; listToAttrs ( + map (n: { + name = "she${n}"; + value = pkgs.lib.attrByPath [ n "pkgs" APPNAME ] null pkgs; + }) (attrNames l_pkg_enum) + ) + ) // { + default = pkgs.vimPlugins.${APPNAME}; + "vimPlugins-${APPNAME}" = pkgs.vimPlugins.${APPNAME}; }); + runLuaCommandOverlay = final: prev: { runLuaCommand = final.callPackage ./nix {}; }; + in { + overlays.default = overlay; + overlays.runLuaCommand = runLuaCommandOverlay; + legacyPackages = forAllSys (system: { inherit (getPkgs system [ runLuaCommandOverlay ]) runLuaCommand; }); + inherit packages; + checks = forAllSys (system: import ./tests/tests.nix (getPkgs system [ overlay runLuaCommandOverlay ])); devShells = forAllSys (system: let - pkgs = import nixpkgs { inherit system; overlays = [ overlay ]; }; + pkgs = getPkgs system []; + lua = pkgs.luajit.withPackages (lp: [ lp.inspect lp.cjson lp.toml-edit lp.luarocks ]); in { default = pkgs.mkShell { - name = "testshell"; - packages = with pkgs; [ bear ]; - inputsFrom = [ ]; - luaInterpreter = (pkgs.lua5_2.withPackages (ps: with ps; [inspect])).interpreter; + name = "${APPNAME}-dev"; + packages = [ lua ]; + LUA_INCDIR = "${lua}/include"; + LUA = lua.interpreter; + BEAR = "${pkgs.bear}/bin/bear"; shellHook = '' - make_cc() { - pushd "$(git rev-parse --show-toplevel || echo ".")" - mkdir -p ./build - bear -- $CC -O2 -fPIC -shared -o ./build/env.so ./nix/env.c -I"$(dirname $luaInterpreter)/../include" "$@" - popd - } + [ "$(whoami)" == "birdee" ] && exec zsh ''; }; }); - checks = forAllSys (system: let - pkgs = import nixpkgs { inherit system; overlays = [ overlay ]; }; - mkBuildTest = lua: let - luapath = (lua.withPackages (ps: with ps; [inspect (pkgs.shelua.override { luapkgs = ps; })])).interpreter; - in pkgs.runCommand ("shelua_package_test-" + lua.luaAttr) {} '' - echo 'package.path = package.path .. ";${./tests}/?.lua"; require("test")' | ${luapath} - > "$out" - ''; - mkCmdTest = lua: pkgs.runLuaCommand ("runLuaCommand_test-" + lua.luaAttr) (lua.withPackages (ps: with ps; [inspect])).interpreter { - nativeBuildInputs = [ pkgs.makeWrapper ]; - passthru = { - testdata = [ "some" "values" ]; - notincluded = system: builtins.trace system system; - }; - } /*lua*/'' - local inspect = require('inspect') - local outbin = out .. "/bin" - local outfile = outbin .. "/testpkg" - local outdrv = outbin .. "/testdrv" - local outcat = outbin .. "/newcat" - local outecho = outbin .. "/newecho" - sh.mkdir("-p", outbin) - os.env.FRIEND = "everyone" - assert(os.getenv("FRIEND") == os.env.FRIEND, "os.env failed") - print(os.getenv("FRIEND")) - os.write_file({}, outfile, [[#!${pkgs.bash}/bin/bash]]) - os.write_file({ append = true, }, outfile, [[echo "hello world!"]]) - os.write_file({ append = true, }, outfile, [[cat ]] .. outdrv) - os.write_file({ append = true, }, outfile, outcat) - os.write_file({}, outdrv, inspect(drv)) - sh.escape_args = true - sh.makeWrapper([[${pkgs.writeShellScript "testscript" ''echo "$@"''}]], outecho, "--add-flags", "testingtesting '1 2' 3") - sh.escape_args = false - sh.makeWrapper([[${pkgs.coreutils}/bin/cat]], outcat, "--add-flags", outecho) - sh.chmod("+x", outfile) - dofile("${./example.lua}") - package.path = package.path .. ";${./tests}/?.lua" - require("test") - ''; - run_on = with pkgs; [ lua5_1 lua5_2 lua5_3 lua5_4 luajit ]; - in nixpkgs.lib.pipe run_on [ - (builtins.map (li: { name = "runLuaCommand-" + li.luaAttr; value = mkCmdTest li; })) - builtins.listToAttrs - ] // (nixpkgs.lib.pipe run_on [ - (builtins.map (li: { name = "withPackages-" + li.luaAttr; value = mkBuildTest li; })) - builtins.listToAttrs - ])); }; } diff --git a/nix/default.nix b/nix/default.nix index a3b8e2b..c3c024b 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -1,25 +1,16 @@ { lib , stdenv -, n2l ? import (builtins.fetchGit (let - lock = (builtins.fromJSON (builtins.readFile ../flake.lock)).nodes.n2l.locked; - in { - url = "https://github.com/${lock.owner}/${lock.repo}.git"; - rev = lock.rev; - })) , ... }: name: interpreter: env: text: -stdenv.mkDerivation (finalAttrs: let - derivationArgs = if lib.isFunction env then env n2l else env; -in { - enableParallelBuilding = true; +stdenv.mkDerivation (finalAttrs: { inherit name; luaInterpreter = interpreter; - luaBuilder = if lib.isFunction text then text n2l else text; - luaBuilderData = lib.pipe (finalAttrs.passthru or {}) [ + luaBuilder = text; + luaBuilderData = lib.pipe (finalAttrs.passthru.info or {}) [ (lib.filterAttrsRecursive (n: v: ! lib.isFunction v)) - (v: "return ${n2l.toLua v}") + (v: "return ${lib.generators.toLua { } v}") ]; - passAsFile = [ "luaBuilder" "luaBuilderData" ] ++ (derivationArgs.passAsFile or [ ]); + passAsFile = [ "luaBuilder" "luaBuilderData" ] ++ (env.passAsFile or [ ]); buildCommand = /*bash*/ '' TEMPDIR=$(mktemp -d) mkdir -p "$TEMPDIR" @@ -27,7 +18,7 @@ in { declare -f > "$STDENV_SHELL_HOOKS" envdir=$(mktemp -d) mkdir -p "$envdir" - $CC -O2 -fPIC -shared -o "$envdir/env.so" '${./env.c}' -I"$(dirname $luaInterpreter)/../include" + $CC -O2 -fPIC ${if stdenv.isDarwin then "-bundle -undefined dynamic_lookup" else "-shared"} -o "$envdir/sh.so" '${./env.c}' -I"$(dirname $luaInterpreter)/../include" echo "package.cpath = '$envdir/?.so;' .. package.cpath package.path = '${../lua}/?.lua;' .. package.path local ok, val = pcall(dofile, '${./nix.lua}') @@ -36,13 +27,13 @@ in { assert(ok, val) " | "$luaInterpreter" - ''; -} // lib.optionalAttrs (!derivationArgs ? meta) { +} // lib.optionalAttrs (!env ? meta) { pos = let - args = builtins.attrNames derivationArgs; + args = builtins.attrNames env; in if builtins.length args > 0 then - builtins.unsafeGetAttrPos (builtins.head args) derivationArgs + builtins.unsafeGetAttrPos (builtins.head args) env else null; -} // (builtins.removeAttrs derivationArgs [ "passAsFile" ])) +} // (removeAttrs env [ "passAsFile" "buildCommand" ])) diff --git a/nix/env.c b/nix/env.c index 9c72348..af7b74d 100644 --- a/nix/env.c +++ b/nix/env.c @@ -42,7 +42,7 @@ static int env__index(lua_State *L) { return 1; } -int luaopen_env(lua_State *L) { +int luaopen_sh_env(lua_State *L) { lua_newtable(L); // module table lua_newtable(L); // metatable lua_pushcfunction(L, env__index); diff --git a/nix/nix.lua b/nix/nix.lua index 870306c..62e0438 100644 --- a/nix/nix.lua +++ b/nix/nix.lua @@ -2,7 +2,7 @@ return function(outdir, tempdir, shell_hooks) _G.out = outdir _G.temp = tempdir - os.env = require("env") + os.env = require("sh.env") _G.sh = require("sh") local sh_settings = getmetatable(sh) string.escapeShellArg = sh_settings.repr.posix.escape diff --git a/shelua-scm-1.rockspec b/shelua-scm-1.rockspec new file mode 100644 index 0000000..2c48515 --- /dev/null +++ b/shelua-scm-1.rockspec @@ -0,0 +1,23 @@ +local _MODREV, _SPECREV = 'scm', '-1' +rockspec_format = '3.0' +package = "shelua" +version = _MODREV .. _SPECREV + +source = { + url = "https://github.com/BirdeeHub/"..package, +} + +description = { + summary = "Tiny lua module to write shell scripts with lua (inspired by zserge/luash)", + homepage = "https://github.com/BirdeeHub/"..package, + license = "MIT" +} + +dependencies = { + "lua >= 5.1" +} + +build = { + type = "builtin", + modules = { ["sh.env"] = "nix/env.c" } +} diff --git a/example.lua b/tests/example.lua similarity index 100% rename from example.lua rename to tests/example.lua diff --git a/tests/gambiarra.lua b/tests/gambiarra.lua index 8b653fa..e66f01c 100644 --- a/tests/gambiarra.lua +++ b/tests/gambiarra.lua @@ -1,126 +1,422 @@ -local function TERMINAL_HANDLER(e, test, msg) - if e == 'pass' then - print("✔ "..test..': '..msg) - elseif e == 'fail' then - print("✘ "..test..': '..msg) - elseif e == 'except' then - print("✘ "..test..': '..msg) - end +-- https://zserge.com/posts/minimal-testing/ + +---@class GambiarraConstructedSpyType +---@field off? fun() +---@field called table[] +---@field errors table[] +---@field called_with fun(...): boolean +---@operator call(...): any + +---@class GambiarraSpyType +---@field on fun(t: table, k: any): GambiarraConstructedSpyType +---@operator call(function): GambiarraConstructedSpyType + +---@class GambiarraTestEnv +---@field ok fun(cond:(boolean|fun():string), msg?: string, should_fail?: boolean) +---@field spy GambiarraSpyType +---@field eq fun(a: any, b: any, msg?: string?): boolean, string? + +---@type GambiarraSpyType +_G.spy = nil +---@type fun(cond: (boolean|fun():string?), msg?: string, should_fail?: boolean) +_G.ok = nil +---@type fun(a: any, b: any, msg?: string): boolean, string? +_G.eq = nil + +local function printable(typename) + if typename == "nil" or typename == "boolean" or typename == "number" or typename == "string" then + return true + else + return false + end end -local function deepeq(a, b) - -- Different types: false - if type(a) ~= type(b) then return false end - -- Functions - if type(a) == 'function' then - return string.dump(a) == string.dump(b) - end - -- Primitives and equal pointers - if a == b then return true end - -- Only equal tables could have passed previous tests - if type(a) ~= 'table' then return false end - -- Compare tables field by field - for k,v in pairs(a) do - if b[k] == nil or not deepeq(v, b[k]) then return false end - end - for k,v in pairs(b) do - if a[k] == nil or not deepeq(v, a[k]) then return false end - end - return true +-- https://github.com/luvit/luvit/blob/master/tests/libs/deep-equal.lua +local function deepeq(expected, actual, path, msg) + if expected == actual then + return true, msg + end + local prefix = path and (path .. ": ") or "" + local expectedType = type(expected) + local actualType = type(actual) + if expectedType ~= actualType then + return false, + prefix + .. "Expected type " + .. expectedType + .. " but found " + .. actualType + .. ", expected value " + .. (printable(expectedType) and tostring(expected)) + .. ", but found value " + .. (printable(actualType) and tostring(actual)) + end + if expectedType ~= "table" then + return false, prefix .. "Expected " .. tostring(expected) .. " but found " .. tostring(actual) + end + local expectedLength = #expected + local actualLength = #actual + for key in pairs(expected) do + if actual[key] == nil then + return false, prefix .. "Missing table key " .. key + end + local newPath = path and (path .. "." .. key) or key + local same, message = deepeq(expected[key], actual[key], newPath, msg) + if not same then + return same, message + end + end + if expectedLength ~= actualLength then + return false, prefix .. "Expected table length " .. expectedLength .. " but found " .. actualLength + end + for key in pairs(actual) do + if expected[key] == nil then + return false, prefix .. "Unexpected table key " .. key + end + end + return true, msg end -- Compatibility for Lua 5.1 and Lua 5.2 local function args(...) - return {n=select('#', ...), ...} + return { n = select("#", ...), ... } end -local function spy(f) - local s = {} - setmetatable(s, {__call = function(s, ...) - s.called = s.called or {} - local a = args(...) - table.insert(s.called, {...}) - if f then - local r - r = args(pcall(f, (unpack or table.unpack)(a, 1, a.n))) - if not r[1] then - s.errors = s.errors or {} - s.errors[#s.called] = r[2] - else - return (unpack or table.unpack)(r, 2, r.n) - end - end - end}) - return s +local function mkspy(...) + local mkspyargs = args(...) + local f, t, k = mkspyargs[1], mkspyargs[2], mkspyargs[3] + local sp = {} + if mkspyargs.n > 1 then + function sp.off() + t[k] = f + end + end + sp.called = {} + sp.errors = {} + setmetatable(sp, { + __index = function(self, key) + if key == "called_with" then + return function(...) + local a = { ... } + for _, v in ipairs(self.called) do + if deepeq(v, a) then + return true + end + end + return false + end + end + end, + __call = function(s, ...) + s.called = s.called or {} + local a = args(...) + table.insert(s.called, { ... }) + if f then + local r + r = args(pcall(f, (unpack or table.unpack)(a, 1, a.n))) + if not r[1] then + s.errors = s.errors or {} + s.errors[#s.called] = r[2] + else + return (unpack or table.unpack)(r, 2, r.n) + end + end + end, + }) + if mkspyargs.n > 1 then + t[k] = function(...) + sp(...) + end + end + return sp end +local spy = setmetatable({}, { + __index = { + on = function(t, k) + return mkspy(t[k], t, k) + end, + }, + __call = function(_, f) + return mkspy(f) + end, +}) -local pendingtests = {} -local env = _G -local gambiarrahandler = TERMINAL_HANDLER - -local function runpending() - if pendingtests[1] ~= nil then pendingtests[1](runpending) end +-- Returned dir always has trailing slash +local function cwd() + local sep = package.config:sub(1, 1) + local info = debug.getinfo(1, "S") + local source = info.source + if source:sub(1, 1) == "@" then + local realpath = ((vim or {}).uv or (vim or {}).loop or {}).fs_realpath + if not realpath then + local ok, luv = pcall(require, "luv") + if ok then + realpath = luv.fs_realpath + end + end + local path = source:sub(2) + path = realpath and realpath(path) or path + local dir, file = path:match("^(.*[" .. sep .. "])([^" .. sep .. "]+)$") + return dir or ("." .. sep), file + end + return "." .. sep, nil end -return function(name, f, async) - if type(name) == 'function' then - gambiarrahandler = name - env = f or _G - return - end - - local testfn = function(next) - - local prev = { - ok = env.ok, - spy = env.spy, - eq = env.eq - } - - local restore = function() - env.ok = prev.ok - env.spy = prev.spy - env.eq = prev.eq - gambiarrahandler('end', name) - table.remove(pendingtests, 1) - if next then next() end - end - - local handler = gambiarrahandler - - env.eq = deepeq - env.spy = spy - env.ok = function(cond, msg) - if not msg then - msg = debug.getinfo(2, 'S').short_src..":"..debug.getinfo(2, 'l').currentline - end - if cond then - handler('pass', name, msg) - else - handler('fail', name, msg) - end - end - - handler('begin', name); - local ok, err = pcall(f, restore) - if not ok then - handler('except', name, err) - end - - if not async then - handler('end', name); - env.ok = prev.ok; - env.spy = prev.spy; - env.eq = prev.eq; - end - end - - if not async then - testfn() - else - table.insert(pendingtests, testfn) - if #pendingtests == 1 then - runpending() - end - end +-- Requires dir to have trailing slash +local function read_dir(dir, filter) + local uv = (vim or {}).uv or (vim or {}).loop + if not uv then + local ok, luv = pcall(require, "luv") + if ok then + uv = luv + end + end + if uv then + local files = {} + local handle = uv.fs_scandir(dir) + while handle do + local name, ty = uv.fs_scandir_next(handle) + if not name then + break + end + local path = dir .. name + ty = ty or (uv.fs_stat(path) or {}).type + if ty == "file" or ty == "link" then + if not filter or filter(name) then + table.insert(files, name) + end + end + end + return files + end + + local ok, lfs = pcall(require, "lfs") + if ok then + local files = {} + for name in lfs.dir(dir) do + if name ~= "." and name ~= ".." then + local path = dir .. name + local attr = lfs.attributes(path) + + if attr and (attr.mode == "file" or attr.mode == "link") then + if not filter or filter(name) then + table.insert(files, name) + end + end + end + end + return files + end + + local command = package.config:sub(1, 1) == "\\" and ('dir "' .. dir .. '" /b') or ('ls -1 "' .. dir .. '"') + local handle = io.popen(command) + local files = {} + if not handle then + return files + end + for filename in handle:lines() do + if not filter or filter(filename) then + table.insert(files, filename) + end + end + handle:close() + return files end + +return setmetatable({ + read_dir = read_dir, + cwd = cwd, + spy = spy, + eq = function(a, b, msg) + return deepeq(a, b, nil, msg) + end, + icons = { + pass = "✔", + fail = "✘", + except = "‼", + begin = "▶", + _end = "◀", + }, + tests_passed = 0, + tests_failed = 0, + pendingtests = {}, + await_callbacks = {}, + ---@type GambiarraTestEnv|any + env = _G, + gambiarrahandler = function(self, e, async, desc, msg, extra) + local suffix = (async and (desc .. " ") or "") .. tostring(msg) + if e == "pass" then + io.stdout:write("\n " .. self.icons.pass .. " " .. suffix .. (extra and ("\n " .. extra) or "")) + elseif e == "fail" then + io.stdout:write( + "\n " .. self.icons.fail .. " " .. suffix .. (extra and "\n (with error: " .. extra .. ")" or "") + ) + elseif e == "except" then + io.stdout:write( + "\n " .. self.icons.except .. " " .. suffix .. (extra and "\n (with error: " .. extra .. ")" or "") + ) + elseif e == "begin" then + io.stdout:write("\n " .. self.icons.begin .. " " .. desc .. " " .. self.icons.begin) + -- elseif e == "end" then + -- io.stdout:write("\n " .. self.icons._end .. " " .. desc .. " " .. self.icons._end) + end + end, +}, { + __index = function(self, key) + if key == "reset_count" then + return function() + self.tests_passed = 0 + self.tests_failed = 0 + end + elseif key == "report" then + return function() + io.stdout:write( + "\n " + .. self.icons.begin + .. " Tests ran: " + .. tostring((self.tests_failed or 0) + (self.tests_passed or 0)) + .. "\n" + ) + io.stdout:write(" " .. self.icons.pass .. " Tests passed: " .. tostring(self.tests_passed) .. "\n") + if (self.tests_failed or 0) > 0 then + io.stdout:write(" " .. self.icons.fail .. " Tests failed: " .. tostring(self.tests_failed) .. "\n") + end + end + elseif key == "await" then + return function(f) + if #self.pendingtests == 0 then + f(self) + else + table.insert(self.await_callbacks, function() + f(self) + end) + end + end + elseif key == "runpending" then + return function() + if self.pendingtests[1] ~= nil then + self.pendingtests[1](self.runpending) + else + for _, f in ipairs(self.await_callbacks) do + f() + end + self.await_callbacks = {} + end + end + end + end, + __call = function(self, name, f, async) + if type(name) == "function" then + self.gambiarrahandler = name + self.env = f or _G + return + end + + local testfn = function(next) + local prev = { + ok = self.env.ok, + spy = self.env.spy, + eq = self.env.eq, + } + + local handler = function(...) + local e = ({ ... })[2] + if e == "pass" then + self.tests_passed = (self.tests_passed or 0) + 1 + elseif e == "end" then + self.tests_passed = (self.tests_passed or 0) + 1 + elseif e == "fail" then + self.tests_failed = (self.tests_failed or 0) + 1 + elseif e == "except" then + self.tests_failed = (self.tests_failed or 0) + 1 + end + self.gambiarrahandler(...) + end + local was_restored = false + local function restore() + was_restored = true + self.env.ok = prev.ok + self.env.spy = prev.spy + self.env.eq = prev.eq + table.remove(self.pendingtests, 1) + if next then + next() + end + end + local usernext = function(fn, ...) + if fn then + local res = args(pcall(fn, ...)) + if res[1] then + return (unpack or table.unpack)(res, 2, res.n) + else + handler(self, "except", async, name, res[2]) + end + else + handler(self, "end", async, name) + end + restore() + end + + self.env.eq = function(a, b, msg) + return deepeq(a, b, nil, msg) + end + self.env.spy = spy + self.env.ok = function(cond, msg, should_fail) + if not msg then + msg = debug.getinfo(2, "S").short_src .. ":" .. debug.getinfo(2, "l").currentline + end + if type(cond) == "function" then + local ok, value = pcall(cond) + if should_fail and not ok or not should_fail and ok then + handler(self, "pass", async, name, msg, value) + else + handler( + self, + "fail", + async, + name, + msg, + not should_fail and tostring(value) + or "Task failed successfully. No error, that is the problem." + ) + end + elseif should_fail and not cond or not should_fail and cond then + handler(self, "pass", async, name, msg) + else + handler(self, "fail", async, name, msg) + end + end + + handler(self, "begin", async, name) + local ok, err + if async then + ok, err = pcall(f, usernext) + else + ok, err = pcall(f) + end + if not ok then + handler(self, "except", async, name, err) + if async and not was_restored then + restore() + end + elseif not async then + handler(self, "end", async, name) + end + + if not async then + self.env.ok = prev.ok + self.env.spy = prev.spy + self.env.eq = prev.eq + end + end + + if async then + table.insert(self.pendingtests, testfn) + if #self.pendingtests == 1 then + self.runpending() + end + else + testfn() + end + end, +}) diff --git a/tests/test.lua b/tests/test.lua index 8d77fd0..c6a10a8 100644 --- a/tests/test.lua +++ b/tests/test.lua @@ -1,18 +1,3 @@ -local tests_passed = 0 -local tests_failed = 0 -require('gambiarra')(function(e, test, msg) - if e == 'pass' then - print("✔ " .. test .. ': ' .. msg) - tests_passed = tests_passed + 1 - elseif e == 'fail' then - print("✘ " .. test .. ': ' .. msg) - tests_failed = tests_failed + 1 - elseif e == 'except' then - print("✘ " .. test .. ': ' .. msg) - tests_failed = tests_failed + 1 - end -end) - local sh = require('sh')(function(s) s.assert_zero = true s.repr.test = {} @@ -87,4 +72,11 @@ test('Check concat command results', function() ok(tostring(r) == 'Goodbye WorldGoodbye Lua', 'concat commands with commands') end) -if tests_failed > 0 then os.exit(1) end +test.await(function(self) + self.report() + if (self.tests_failed or 0) > 0 then + os.exit(1) + else + os.exit(0) + end +end) diff --git a/tests/tests.nix b/tests/tests.nix new file mode 100644 index 0000000..72aea21 --- /dev/null +++ b/tests/tests.nix @@ -0,0 +1,49 @@ +pkgs: let + mkBuildTest = lua: let + luapath = (lua.withPackages (ps: with ps; [ inspect shelua ])).interpreter; + in pkgs.runCommand ("shelua_package_test-" + lua.luaAttr) {} '' + echo 'package.path = package.path .. ";${./.}/?.lua"; require("test")' | ${luapath} - > "$out" + ''; + mkCmdTest = lua: + pkgs.runLuaCommand ("runLuaCommand_test-" + lua.luaAttr) (lua.withPackages (ps: with ps; [ inspect ])).interpreter { + nativeBuildInputs = [ pkgs.makeWrapper ]; + passthru.info = { + testdata = [ "some" "values" ]; + notincluded = system: builtins.trace system system; + }; + } /*lua*/ + '' + local inspect = require('inspect') + local outbin = out .. "/bin" + local outfile = outbin .. "/testpkg" + local outdrv = outbin .. "/testdrv" + local outcat = outbin .. "/newcat" + local outecho = outbin .. "/newecho" + sh.mkdir("-p", outbin) + os.env.FRIEND = "everyone" + assert(os.getenv("FRIEND") == os.env.FRIEND, "os.env failed") + print(os.getenv("FRIEND")) + os.write_file({}, outfile, [[#!${pkgs.bash}/bin/bash]]) + os.write_file({ append = true, }, outfile, [[echo "hello world!"]]) + os.write_file({ append = true, }, outfile, [[cat ]] .. outdrv) + os.write_file({ append = true, }, outfile, outcat) + os.write_file({}, outdrv, inspect(drv)) + sh.escape_args = true + sh.makeWrapper([[${pkgs.writeShellScript "testscript" ''echo "$@"''}]], outecho, "--add-flags", "testingtesting '1 2' 3") + sh.escape_args = false + sh.makeWrapper([[${pkgs.coreutils}/bin/cat]], outcat, "--add-flags", outecho) + sh.chmod("+x", outfile) + package.path = package.path .. ";${./.}/?.lua" + require("example") + require("test") + ''; + run_on = with pkgs; [ lua5_1 lua5_2 lua5_3 lua5_4 lua5_5 luajit ]; +in pkgs.lib.pipe run_on [ + (map (li: { name = "runLuaCommand-" + li.luaAttr; value = mkCmdTest li; })) + builtins.listToAttrs +] // ( + pkgs.lib.pipe run_on [ + (map (li: { name = "withPackages-" + li.luaAttr; value = mkBuildTest li; })) + builtins.listToAttrs + ] +)