Skip to content

Commit bf3a5dd

Browse files
committed
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
1 parent b6cf5d8 commit bf3a5dd

File tree

3 files changed

+60
-12
lines changed

3 files changed

+60
-12
lines changed

busted/init.lua

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
local function init(busted)
2-
local block = require 'busted.block'(busted)
2+
local block = require 'busted.block' (busted)
33

44
local file = function(file)
55
busted.wrap(file.run)
@@ -20,6 +20,8 @@ local function init(busted)
2020
local it = function(element)
2121
local parent = busted.context.parent(element)
2222
local finally
23+
local attempt = 1
24+
local max_attempts = 1
2325

2426
if not block.lazySetup(parent) then
2527
-- skip test if any setup failed
@@ -31,24 +33,40 @@ local function init(busted)
3133
block.rejectAll(element)
3234
element.env.finally = function(fn) finally = fn end
3335
element.env.pending = busted.pending
36+
element.env.set_retries = function(n) max_attempts = n + 1 end
3437

38+
local status = busted.status('success')
3539
local pass, ancestor = block.execAll('before_each', parent, true)
40+
if pass and busted.safe_publish('test', { 'test', 'start' }, element, parent) then
41+
while attempt <= max_attempts do
42+
-- Run after_each from previous attempt before before_each (for retries)
43+
if attempt > 1 then
44+
block.dexecAll('after_each', ancestor, true)
45+
pass, ancestor = block.execAll('before_each', parent, true)
46+
end
47+
local attempt_status = busted.safe('it', element.run, element)
3648

37-
if pass then
38-
local status = busted.status('success')
39-
if busted.safe_publish('test', { 'test', 'start' }, element, parent) then
40-
status:update(busted.safe('it', element.run, element))
4149
if finally then
4250
block.reject('pending', element)
4351
status:update(busted.safe('finally', finally, element))
4452
end
45-
else
46-
status = busted.status('error')
53+
54+
if attempt_status:success() then
55+
status = busted.status('success')
56+
break
57+
else
58+
status = attempt_status
59+
end
60+
61+
attempt = attempt + 1
4762
end
48-
busted.safe_publish('test', { 'test', 'end' }, element, parent, tostring(status))
49-
end
5063

51-
block.dexecAll('after_each', ancestor, true)
64+
-- Run after_each after the last try.
65+
block.dexecAll('after_each', ancestor, true)
66+
else
67+
status = busted.status('error')
68+
end
69+
busted.safe_publish('test', { 'test', 'end' }, element, parent, tostring(status))
5270
end
5371

5472
local pending = function(element)
@@ -93,7 +111,7 @@ local function init(busted)
93111
local stub = busted.require 'luassert.stub'
94112
local match = busted.require 'luassert.match'
95113

96-
require 'busted.fixtures' -- just load into the environment, not exposing it
114+
require 'busted.fixtures' -- just load into the environment, not exposing it
97115

98116
busted.export('assert', assert)
99117
busted.export('spy', spy)

busted/outputHandlers/base.lua

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ return function()
7070
name = handler.getFullName(element),
7171
message = message,
7272
randomseed = parent and parent.randomseed,
73-
isError = isError
73+
isError = isError,
74+
id = tostring(element)
7475
}
7576
formatted.element.trace = element.trace or debug
7677

@@ -120,6 +121,12 @@ return function()
120121
local insertTable
121122

122123
if status == 'success' then
124+
-- Remove any failures for this test since it succeeded
125+
for i = #handler.failures, 1, -1 do
126+
if handler.failures[i].id == tostring(element) then
127+
table.remove(handler.failures, i)
128+
end
129+
end
123130
insertTable = handler.successes
124131
handler.successesCount = handler.successesCount + 1
125132
elseif status == 'pending' then

spec/core_spec.lua

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,29 @@ assert(type(mock) == 'table')
1313
assert(type(match) == 'table')
1414
assert(type(assert) == 'table')
1515

16+
describe('retry test', function()
17+
local attempt_count = 0
18+
19+
before_each(function()
20+
attempt_count = attempt_count + 1
21+
print("Before each for attempt " .. attempt_count)
22+
end)
23+
24+
after_each(function()
25+
print("After each for attempt " .. attempt_count)
26+
end)
27+
28+
it('should succeed on second attempt', function()
29+
set_retries(2) -- 3 total attempts
30+
print("Running attempt " .. attempt_count)
31+
if attempt_count < 3 then
32+
assert.is_true(false, 'Failing attempt ' .. attempt_count)
33+
else
34+
assert.is_true(true, 'Succeeding on attempt ' .. attempt_count)
35+
end
36+
end)
37+
end)
38+
1639
describe('Before each', function()
1740
local test_val = false
1841

0 commit comments

Comments
 (0)