From b6cf5d883d66a3cb7663b5311362afa32cad8ad6 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Thu, 18 Sep 2025 23:27:52 -0400 Subject: [PATCH 1/2] doc: readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 708bb48e..292963ef 100644 --- a/README.md +++ b/README.md @@ -75,15 +75,15 @@ Assuming you have luarocks installed: Install these dependencies for core testing: ``` -luarocks install moonscript +luarocks --local install moonscript ``` Then to reinstall and run tests: ``` -luarocks remove busted --force -luarocks make -busted spec +luarocks --local remove busted --force +luarocks --local make +~/.luarocks/bin/busted spec ``` Docker From bf3a5dd75aa05ecaf6fcfc9c1eddfa3ea62b24bf Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sun, 14 Sep 2025 19:12:34 -0400 Subject: [PATCH 2/2] feat: tests can opt-in to retries Problem: Tests have no way to retry without manually managing `before_each` and `after_each` setup/teardown. This is painful. And busted does a good job of guarding its internals and makes it impossible to extend it: AFAICT, consumers have no way to access the current "test context", or the current list of `before_each`/`after_each` hooks. Solution: Introduce `env.set_retries()`, which allows test authors to optionally retry a test: ```lua it('...', function() set_retries(2) -- If the test fails, it will be retried up to 2 times. end) ``` Testing: luarocks --local remove busted --force && luarocks --local make && ~/.luarocks/bin/busted --pattern=core --- busted/init.lua | 40 ++++++++++++++++++++++++---------- busted/outputHandlers/base.lua | 9 +++++++- spec/core_spec.lua | 23 +++++++++++++++++++ 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/busted/init.lua b/busted/init.lua index 0d368128..bc5bfab6 100644 --- a/busted/init.lua +++ b/busted/init.lua @@ -1,5 +1,5 @@ local function init(busted) - local block = require 'busted.block'(busted) + local block = require 'busted.block' (busted) local file = function(file) busted.wrap(file.run) @@ -20,6 +20,8 @@ local function init(busted) local it = function(element) local parent = busted.context.parent(element) local finally + local attempt = 1 + local max_attempts = 1 if not block.lazySetup(parent) then -- skip test if any setup failed @@ -31,24 +33,40 @@ local function init(busted) block.rejectAll(element) element.env.finally = function(fn) finally = fn end element.env.pending = busted.pending + element.env.set_retries = function(n) max_attempts = n + 1 end + local status = busted.status('success') local pass, ancestor = block.execAll('before_each', parent, true) + if pass and busted.safe_publish('test', { 'test', 'start' }, element, parent) then + while attempt <= max_attempts do + -- Run after_each from previous attempt before before_each (for retries) + if attempt > 1 then + block.dexecAll('after_each', ancestor, true) + pass, ancestor = block.execAll('before_each', parent, true) + end + local attempt_status = busted.safe('it', element.run, element) - if pass then - local status = busted.status('success') - if busted.safe_publish('test', { 'test', 'start' }, element, parent) then - status:update(busted.safe('it', element.run, element)) if finally then block.reject('pending', element) status:update(busted.safe('finally', finally, element)) end - else - status = busted.status('error') + + if attempt_status:success() then + status = busted.status('success') + break + else + status = attempt_status + end + + attempt = attempt + 1 end - busted.safe_publish('test', { 'test', 'end' }, element, parent, tostring(status)) - end - block.dexecAll('after_each', ancestor, true) + -- Run after_each after the last try. + block.dexecAll('after_each', ancestor, true) + else + status = busted.status('error') + end + busted.safe_publish('test', { 'test', 'end' }, element, parent, tostring(status)) end local pending = function(element) @@ -93,7 +111,7 @@ local function init(busted) local stub = busted.require 'luassert.stub' local match = busted.require 'luassert.match' - require 'busted.fixtures' -- just load into the environment, not exposing it + require 'busted.fixtures' -- just load into the environment, not exposing it busted.export('assert', assert) busted.export('spy', spy) diff --git a/busted/outputHandlers/base.lua b/busted/outputHandlers/base.lua index b69fe1c8..f236c48b 100644 --- a/busted/outputHandlers/base.lua +++ b/busted/outputHandlers/base.lua @@ -70,7 +70,8 @@ return function() name = handler.getFullName(element), message = message, randomseed = parent and parent.randomseed, - isError = isError + isError = isError, + id = tostring(element) } formatted.element.trace = element.trace or debug @@ -120,6 +121,12 @@ return function() local insertTable if status == 'success' then + -- Remove any failures for this test since it succeeded + for i = #handler.failures, 1, -1 do + if handler.failures[i].id == tostring(element) then + table.remove(handler.failures, i) + end + end insertTable = handler.successes handler.successesCount = handler.successesCount + 1 elseif status == 'pending' then diff --git a/spec/core_spec.lua b/spec/core_spec.lua index 4498539e..8a3aaff8 100644 --- a/spec/core_spec.lua +++ b/spec/core_spec.lua @@ -13,6 +13,29 @@ assert(type(mock) == 'table') assert(type(match) == 'table') assert(type(assert) == 'table') +describe('retry test', function() + local attempt_count = 0 + + before_each(function() + attempt_count = attempt_count + 1 + print("Before each for attempt " .. attempt_count) + end) + + after_each(function() + print("After each for attempt " .. attempt_count) + end) + + it('should succeed on second attempt', function() + set_retries(2) -- 3 total attempts + print("Running attempt " .. attempt_count) + if attempt_count < 3 then + assert.is_true(false, 'Failing attempt ' .. attempt_count) + else + assert.is_true(true, 'Succeeding on attempt ' .. attempt_count) + end + end) +end) + describe('Before each', function() local test_val = false