From 5199ed485b87dbedd382000ef15ef8172e368fe0 Mon Sep 17 00:00:00 2001 From: TJ Kolleh Date: Sun, 19 Apr 2026 00:02:37 -0400 Subject: [PATCH 1/7] fix: address edge cases in bracket and test-coverage recipe --- justfile | 11 ++++ lua/roda/util.lua | 26 ++++++--- spec/util_spec.lua | 140 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+), 9 deletions(-) create mode 100644 spec/util_spec.lua diff --git a/justfile b/justfile index ec370e1..f83ea12 100644 --- a/justfile +++ b/justfile @@ -94,6 +94,17 @@ test-unit: [group('test')] test: test-unit +luacov_src := `find .lux/5.5/test_dependencies/5.5 -maxdepth 1 -name '*luacov*' -type d -print -quit 2>/dev/null` / "src" + +[doc("Run unit tests with coverage and generate report")] +[group('test')] +test-coverage: + @echo "Running unit tests with coverage..." + lx --lua-version 5.5 --lua-dir {{ lua_prefix }} --variables "WITH_SHARED_LIBUV=OFF" test -- --coverage + @echo "Generating coverage report..." + lx exec --no-loader lua -- -e "package.path = package.path .. ';{{ luacov_src }}/?.lua'; local r = require('luacov.runner'); r.run_report(r.load_config())" + @echo "Coverage report written to luacov.report.out" + [doc("Run unit tests for CI (Lua 5.4)")] [group('ci')] test-ci: diff --git a/lua/roda/util.lua b/lua/roda/util.lua index e6d213e..179f147 100644 --- a/lua/roda/util.lua +++ b/lua/roda/util.lua @@ -15,16 +15,24 @@ function M.bracket(acquire, release) return function(use) return function(on_complete) local resource = acquire() - use( - resource, - -- Release the resource and trigger the final continuation - function(...) - release(resource) - if on_complete then - on_complete(...) - end + local released = false + + local function safe_release(...) + if released then + return + end + released = true + release(resource) + if on_complete then + on_complete(...) end - ) + end + + local success, err = pcall(use, resource, safe_release) + if not success then + safe_release() + error(err, 0) + end end end end diff --git a/spec/util_spec.lua b/spec/util_spec.lua new file mode 100644 index 0000000..ee50edc --- /dev/null +++ b/spec/util_spec.lua @@ -0,0 +1,140 @@ +---@diagnostic disable: undefined-global +local fp = require("roda.util") + +describe("roda.util", function() + describe("bracket", function() + it("should acquire resource and pass it to use", function() + local acquired = false + local resource_value = "test-resource" + local received_resource = nil + + local bracket = fp.bracket(function() + acquired = true + return resource_value + end, function() end) + + local use_fn = bracket(function(resource, done) + received_resource = resource + done() + end) + + use_fn(function() end) + + assert.is_true(acquired) + assert.equals(resource_value, received_resource) + end) + + it("should release resource via release callback", function() + local released = false + local released_resource = nil + + local bracket = fp.bracket(function() + return "res" + end, function(r) + released = true + released_resource = r + end) + + local use_fn = bracket(function(resource, done) + done("result") + end) + + use_fn(function() end) + + assert.is_true(released) + assert.equals("res", released_resource) + end) + + it("should pass callback values to on_complete", function() + local on_complete_result = nil + + local bracket = fp.bracket(function() + return "res" + end, function() end) + + local use_fn = bracket(function(resource, done) + done("value1", "value2") + end) + + use_fn(function(...) + on_complete_result = { ... } + end) + + assert.equals("value1", on_complete_result[1]) + assert.equals("value2", on_complete_result[2]) + end) + + it("should call release before on_complete", function() + local call_order = {} + + local bracket = fp.bracket(function() + return "res" + end, function() + table.insert(call_order, "release") + end) + + local use_fn = bracket(function(resource, done) + done() + end) + + use_fn(function() + table.insert(call_order, "on_complete") + end) + + assert.equals("release", call_order[1]) + assert.equals("on_complete", call_order[2]) + end) + + it("should not call on_complete when it is nil", function() + local bracket = fp.bracket(function() + return "res" + end, function() end) + + local use_fn = bracket(function(resource, done) + done("result") + end) + + assert.has_no.errors(function() + use_fn(nil) + end) + end) + + it("should call release only once even if done is called multiple times", function() + local release_count = 0 + local bracket = fp.bracket(function() + return "res" + end, function() + release_count = release_count + 1 + end) + + local use_fn = bracket(function(resource, done) + done() + done() + done() + end) + + use_fn(function() end) + + assert.equals(1, release_count) + end) + + it("should call release and re-throw if use throws a synchronous error", function() + local released = false + local bracket = fp.bracket(function() + return "res" + end, function() + released = true + end) + + local use_fn = bracket(function(resource, done) + error("synchronous error") + end) + + assert.has_error(function() + use_fn(function() end) + end, "synchronous error") + + assert.is_true(released) + end) + end) +end) From 39e163f6bf912434cdedf62e55bc8816777f4d70 Mon Sep 17 00:00:00 2001 From: TJ Kolleh Date: Sun, 19 Apr 2026 00:21:27 -0400 Subject: [PATCH 2/7] test: add luacov config and tests for coverage Added .luacov configuration, luacov dependency in lux.toml, issue-14-plan.md documenting the plan, and test cases for roda.roda alias and render edge cases in spec/roda_spec.lua. --- .luacov | 9 ++++++ issue-14-plan.md | 50 ++++++++++++++++++++++++++++++++ lux.toml | 1 + spec/roda_spec.lua | 72 +++++++++++++++++++++++++++++++++++++++------- 4 files changed, 121 insertions(+), 11 deletions(-) create mode 100644 .luacov create mode 100644 issue-14-plan.md diff --git a/.luacov b/.luacov new file mode 100644 index 0000000..60a4e26 --- /dev/null +++ b/.luacov @@ -0,0 +1,9 @@ +return { + include = { + "lua/roda/.*" + }, + exclude = { + "spec/.*", + "%.lux/.*" + } +} \ No newline at end of file diff --git a/issue-14-plan.md b/issue-14-plan.md new file mode 100644 index 0000000..12f04c2 --- /dev/null +++ b/issue-14-plan.md @@ -0,0 +1,50 @@ +# Plan to Enable Code Coverage and Reach 80%+ Floor (Issue #14) + +## 1. Enable `luacov` with `busted` +1. **Update Dependencies:** ~~Add `luacov` to the `[test_dependencies]` section in `lux.toml`.~~ Already present. +2. **Configure `luacov`:** ~~Create a `.luacov` file~~ Already present. +3. **Update `justfile`:** Add a `test-coverage` recipe (done). + ```justfile + luacov_src := `find .lux/5.5/test_dependencies/5.5 -maxdepth 1 -name '*luacov*' -type d -print -quit 2>/dev/null` / "src" + + [doc("Run unit tests with coverage and generate report")] + [group('test')] + test-coverage: + @echo "Running unit tests with coverage..." + lx --lua-version 5.5 --lua-dir {{ lua_prefix }} --variables "WITH_SHARED_LIBUV=OFF" test -- --coverage + @echo "Generating coverage report..." + LUA_PATH="{{ luacov_src }}/?.lua;;" lua5.5 -e "local r = require('luacov.runner'); r.run_report(r.load_config())" + @echo "Coverage report written to luacov.report.out" + ``` + Note: `lx exec --no-loader luacov` does not work. The report is generated by running `lua5.5 -e` with `LUA_PATH` pointing to the luacov source in `.lux/`. + +## 2. Coverage Results + +| File | Before | After | +|------|--------|-------| +| `lua/roda/ansi.lua` | 91.18% | 91.18% | +| `lua/roda/init.lua` | 72.93% | 74.67% | +| `lua/roda/spinners.lua` | 100.00% | 100.00% | +| `lua/roda/symbols.lua` | 100.00% | 100.00% | +| `lua/roda/util.lua` | 37.50% | **100.00%** | +| **Total** | **80.98%** | **83.47%** | + +Remaining uncovered code in `init.lua` is the async execution chain (`with_safe_timer`, `make_process_bracket`, `Spinner:execute`, `M.run`) which requires `uv.run("default")` and real subprocess execution to exercise. No integration tests written per decision. + +Coverage floor: **80%**. + +## 3. Tests Added +1. **`spec/util_spec.lua`** (new file): + + - `bracket` passes acquired resource to `use` + - `bracket` calls `release` with the resource + - `bracket` passes callback values through to `on_complete` + - `bracket` calls `release` before `on_complete` + - `bracket` tolerates `nil` `on_complete` without error + +2. **`spec/roda_spec.lua`** (new describe blocks): + + - `roda.roda` alias creates a spinner identical to `roda.new` + - `render` returns self and writes nothing when not spinning + - Frame index wraps around after reaching the end + - `setText` triggers a render while spinning \ No newline at end of file diff --git a/lux.toml b/lux.toml index 349e008..d03c86b 100644 --- a/lux.toml +++ b/lux.toml @@ -24,6 +24,7 @@ luv = ">= 1.44.2" [test_dependencies] busted = "2.3.0-1" +luacov = ">= 0.15.0" [test] type = "busted" diff --git a/spec/roda_spec.lua b/spec/roda_spec.lua index d98e107..c652988 100644 --- a/spec/roda_spec.lua +++ b/spec/roda_spec.lua @@ -1,18 +1,17 @@ ---@diagnostic disable: undefined-global local roda = require("roda") -describe("roda module", function() - -- Mock stream to capture output without affecting terminal - local function mock_stream() - return { - output = {}, - write = function(self, str) - table.insert(self.output, str) - end, - flush = function() end, - } - end +local function mock_stream() + return { + output = {}, + write = function(self, str) + table.insert(self.output, str) + end, + flush = function() end, + } +end +describe("roda module", function() describe("constructor", function() it("should create spinner with string text", function() local spinner = roda("Loading...") @@ -367,6 +366,57 @@ describe("roda.promise", function() end) end) +describe("roda.roda alias", function() + it("should create a spinner when called directly", function() + local spinner = roda.roda("Direct call") + assert.is_not_nil(spinner) + assert.equals("Direct call", spinner:getText()) + end) + + it("should behave identically to roda.new", function() + local s1 = roda.roda({ text = "test", color = "green" }) + local s2 = roda.new({ text = "test", color = "green" }) + assert.equals(s1:getText(), s2:getText()) + assert.equals(s1:getColor(), s2:getColor()) + end) +end) + +describe("render edge cases", function() + it("should return self when render called while not spinning", function() + local stream = mock_stream() + local spinner = roda("Test") + spinner._stream = stream + local result = spinner:render() + assert.equals(spinner, result) + assert.equals(0, #stream.output) + end) + + it("should wrap frame index after reaching the end", function() + local stream = mock_stream() + local spinner = roda({ spinner = "line" }) + spinner._stream = stream + spinner:start() + local frames_count = #spinner._spinner.frames + for _ = 1, frames_count - 1 do + spinner:render() + end + assert.equals(1, spinner._frame_index) + spinner:stop() + end) + + it("should render when setText is called while spinning", function() + local stream = mock_stream() + local spinner = roda("Original") + spinner._stream = stream + spinner:start() + local count_before = #stream.output + spinner:setText("Updated") + assert.is_true(#stream.output > count_before) + assert.equals("Updated", spinner:getText()) + spinner:stop() + end) +end) + describe("submodule exports", function() it("should export ansi submodule", function() assert.is_table(roda.ansi) From 3c3e81ec688f59441d31a7fa98ab0a798ff25935 Mon Sep 17 00:00:00 2001 From: TJ Kolleh Date: Sun, 19 Apr 2026 00:26:18 -0400 Subject: [PATCH 3/7] chore: remove issue-14-plan.md --- issue-14-plan.md | 50 ------------------------------------------------ 1 file changed, 50 deletions(-) delete mode 100644 issue-14-plan.md diff --git a/issue-14-plan.md b/issue-14-plan.md deleted file mode 100644 index 12f04c2..0000000 --- a/issue-14-plan.md +++ /dev/null @@ -1,50 +0,0 @@ -# Plan to Enable Code Coverage and Reach 80%+ Floor (Issue #14) - -## 1. Enable `luacov` with `busted` -1. **Update Dependencies:** ~~Add `luacov` to the `[test_dependencies]` section in `lux.toml`.~~ Already present. -2. **Configure `luacov`:** ~~Create a `.luacov` file~~ Already present. -3. **Update `justfile`:** Add a `test-coverage` recipe (done). - ```justfile - luacov_src := `find .lux/5.5/test_dependencies/5.5 -maxdepth 1 -name '*luacov*' -type d -print -quit 2>/dev/null` / "src" - - [doc("Run unit tests with coverage and generate report")] - [group('test')] - test-coverage: - @echo "Running unit tests with coverage..." - lx --lua-version 5.5 --lua-dir {{ lua_prefix }} --variables "WITH_SHARED_LIBUV=OFF" test -- --coverage - @echo "Generating coverage report..." - LUA_PATH="{{ luacov_src }}/?.lua;;" lua5.5 -e "local r = require('luacov.runner'); r.run_report(r.load_config())" - @echo "Coverage report written to luacov.report.out" - ``` - Note: `lx exec --no-loader luacov` does not work. The report is generated by running `lua5.5 -e` with `LUA_PATH` pointing to the luacov source in `.lux/`. - -## 2. Coverage Results - -| File | Before | After | -|------|--------|-------| -| `lua/roda/ansi.lua` | 91.18% | 91.18% | -| `lua/roda/init.lua` | 72.93% | 74.67% | -| `lua/roda/spinners.lua` | 100.00% | 100.00% | -| `lua/roda/symbols.lua` | 100.00% | 100.00% | -| `lua/roda/util.lua` | 37.50% | **100.00%** | -| **Total** | **80.98%** | **83.47%** | - -Remaining uncovered code in `init.lua` is the async execution chain (`with_safe_timer`, `make_process_bracket`, `Spinner:execute`, `M.run`) which requires `uv.run("default")` and real subprocess execution to exercise. No integration tests written per decision. - -Coverage floor: **80%**. - -## 3. Tests Added -1. **`spec/util_spec.lua`** (new file): - - - `bracket` passes acquired resource to `use` - - `bracket` calls `release` with the resource - - `bracket` passes callback values through to `on_complete` - - `bracket` calls `release` before `on_complete` - - `bracket` tolerates `nil` `on_complete` without error - -2. **`spec/roda_spec.lua`** (new describe blocks): - - - `roda.roda` alias creates a spinner identical to `roda.new` - - `render` returns self and writes nothing when not spinning - - Frame index wraps around after reaching the end - - `setText` triggers a render while spinning \ No newline at end of file From 65d1482f23970a9fa0fcc0f717ebf29466fdc89c Mon Sep 17 00:00:00 2001 From: TJ Kolleh Date: Sun, 19 Apr 2026 00:30:35 -0400 Subject: [PATCH 4/7] fix: use system lua for ci tests and linting --- justfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/justfile b/justfile index a67fe2a..98c9bc3 100644 --- a/justfile +++ b/justfile @@ -94,7 +94,7 @@ lint: [doc("Lint for CI (Lua 5.4)")] [group('ci')] lint-ci: - lx --lua-version 5.4 lint + lx --lua-version 5.4 --lua-dir {{ lua_prefix }} lint [doc("Run code quality checks")] [group('dev')] @@ -116,9 +116,9 @@ luacov_src := `find .lux/5.5/test_dependencies/5.5 -maxdepth 1 -name '*luacov*' [doc("Run unit tests with coverage and generate report")] [group('test')] -test-coverage: +test-coverage: build-luv @echo "Running unit tests with coverage..." - lx --lua-version 5.5 --lua-dir {{ lua_prefix }} --variables "WITH_SHARED_LIBUV=OFF" test -- --coverage + LUA_CPATH="{{ build_dir }}/?.so;;" lx --lua-version 5.5 --lua-dir {{ lua_prefix }} test -- --coverage @echo "Generating coverage report..." lx exec --no-loader lua -- -e "package.path = package.path .. ';{{ luacov_src }}/?.lua'; local r = require('luacov.runner'); r.run_report(r.load_config())" @echo "Coverage report written to luacov.report.out" @@ -127,7 +127,7 @@ test-coverage: [group('ci')] test-ci: build-luv @echo "Running unit tests for CI..." - LUA_CPATH="{{ build_dir }}/?.so;;" lx --lua-version 5.4 test + LUA_CPATH="{{ build_dir }}/?.so;;" lx --lua-version 5.4 --lua-dir {{ lua_prefix }} test # --- Build --- From 13471b64b6e186a79c3d4130d903bf8b39deb735 Mon Sep 17 00:00:00 2001 From: TJ Kolleh Date: Sun, 19 Apr 2026 00:33:08 -0400 Subject: [PATCH 5/7] fix: correct bash syntax in test-coverage recipe --- justfile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/justfile b/justfile index 98c9bc3..985b262 100644 --- a/justfile +++ b/justfile @@ -112,15 +112,14 @@ test-unit: build-luv [group('test')] test: test-unit -luacov_src := `find .lux/5.5/test_dependencies/5.5 -maxdepth 1 -name '*luacov*' -type d -print -quit 2>/dev/null` / "src" - [doc("Run unit tests with coverage and generate report")] [group('test')] test-coverage: build-luv @echo "Running unit tests with coverage..." LUA_CPATH="{{ build_dir }}/?.so;;" lx --lua-version 5.5 --lua-dir {{ lua_prefix }} test -- --coverage @echo "Generating coverage report..." - lx exec --no-loader lua -- -e "package.path = package.path .. ';{{ luacov_src }}/?.lua'; local r = require('luacov.runner'); r.run_report(r.load_config())" + @luacov_src=$(find .lux/5.5/test_dependencies/5.5 -maxdepth 1 -name '*luacov*' -type d -print -quit 2>/dev/null)/src; \ + lx exec --no-loader lua -- -e "package.path = package.path .. ';"$luacov_src"/?.lua'; local r = require('luacov.runner'); r.run_report(r.load_config())" @echo "Coverage report written to luacov.report.out" [doc("Run unit tests for CI (Lua 5.4)")] From 9529c54cc9b43dfa397785268a8af1f44170cacf Mon Sep 17 00:00:00 2001 From: TJ Kolleh Date: Sun, 19 Apr 2026 00:34:51 -0400 Subject: [PATCH 6/7] ci: install lua5.4 and create symlink for lux --- .github/workflows/tests.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b9998df..fd2796c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,6 +23,7 @@ jobs: run: | sudo apt-get update sudo apt-get install -y lua5.4 liblua5.4-dev cmake + sudo ln -s /usr/bin/lua5.4 /usr/bin/lua - uses: extractions/setup-just@v2 @@ -43,9 +44,12 @@ jobs: - uses: extractions/setup-just@v2 - - name: Install GitHub CLI (if missing) + - name: Install GitHub CLI and Lua (if missing) run: | which gh || sudo apt-get install -y gh + sudo apt-get update + sudo apt-get install -y lua5.4 liblua5.4-dev + sudo ln -s /usr/bin/lua5.4 /usr/bin/lua - name: Check formatting run: | From 8e3043f453111edf6e0d9982f86bc4690f7b0728 Mon Sep 17 00:00:00 2001 From: TJ Kolleh Date: Sun, 19 Apr 2026 00:35:51 -0400 Subject: [PATCH 7/7] ci: force symlink for lua5.4 --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fd2796c..d276633 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,7 +23,7 @@ jobs: run: | sudo apt-get update sudo apt-get install -y lua5.4 liblua5.4-dev cmake - sudo ln -s /usr/bin/lua5.4 /usr/bin/lua + sudo ln -sf /usr/bin/lua5.4 /usr/bin/lua - uses: extractions/setup-just@v2 @@ -49,7 +49,7 @@ jobs: which gh || sudo apt-get install -y gh sudo apt-get update sudo apt-get install -y lua5.4 liblua5.4-dev - sudo ln -s /usr/bin/lua5.4 /usr/bin/lua + sudo ln -sf /usr/bin/lua5.4 /usr/bin/lua - name: Check formatting run: |