diff --git a/TESTS_README.md b/TESTS_README.md index 1103ca27..f71734b1 100644 --- a/TESTS_README.md +++ b/TESTS_README.md @@ -125,3 +125,19 @@ To test this in your `~/.config/nvim` configuration, try the suggested file stru lua/example/module.lua lua/spec/example/module_spec.lua ``` + +# Asynchronous testing + +Tests run in a coroutine, which can be yielded and resumed. This can be used to +test code that uses asynchronous Neovim functionalities. For example, this can +be done inside a test: + +```lua +local co = coroutine.running() +vim.defer_fn(function() + coroutine.resume(co) +end, 1000) +--The test will reach here immediately. +coroutine.yield() +--The test will only reach here after one second, when the deferred function runs. +``` diff --git a/lua/plenary/busted.lua b/lua/plenary/busted.lua index 6d1e91e1..b827ff82 100644 --- a/lua/plenary/busted.lua +++ b/lua/plenary/busted.lua @@ -218,9 +218,9 @@ mod.run = function(file) print("\n" .. HEADER) print("Testing: ", file) - local ok, msg = pcall(dofile, file) + local loaded, msg = loadfile(file) - if not ok then + if not loaded then print(HEADER) print "FAILED TO LOAD FILE" print(color_string("red", msg)) @@ -232,33 +232,37 @@ mod.run = function(file) end end - -- If nothing runs (empty file without top level describe) - if not results.pass then - if is_headless then - return vim.cmd "0cq" - else - return + coroutine.wrap(function() + loaded() + + -- If nothing runs (empty file without top level describe) + if not results.pass then + if is_headless then + return vim.cmd "0cq" + else + return + end end - end - mod.format_results(results) + mod.format_results(results) - if #results.errs ~= 0 then - print("We had an unexpected error: ", vim.inspect(results.errs), vim.inspect(results)) - if is_headless then - return vim.cmd "2cq" - end - elseif #results.fail > 0 then - print "Tests Failed. Exit: 1" + if #results.errs ~= 0 then + print("We had an unexpected error: ", vim.inspect(results.errs), vim.inspect(results)) + if is_headless then + return vim.cmd "2cq" + end + elseif #results.fail > 0 then + print "Tests Failed. Exit: 1" - if is_headless then - return vim.cmd "1cq" - end - else - if is_headless then - return vim.cmd "0cq" + if is_headless then + return vim.cmd "1cq" + end + else + if is_headless then + return vim.cmd "0cq" + end end - end + end)() end return mod diff --git a/tests/plenary/async_testing_spec.lua b/tests/plenary/async_testing_spec.lua new file mode 100644 index 00000000..c1c991be --- /dev/null +++ b/tests/plenary/async_testing_spec.lua @@ -0,0 +1,75 @@ +local Job = require "plenary.job" + +local Timing = {} + +function Timing:log(name) + self[name] = vim.loop.uptime() +end + +function Timing:check(from, to, min_elapsed) + assert(self[from], "did not log " .. from) + assert(self[to], "did not log " .. to) + local elapsed = self[to] - self[from] + assert( + min_elapsed <= elapsed, + string.format("only took %s to get from %s to %s - expected at least %s", elapsed, from, to, min_elapsed) + ) +end + +describe("Async test", function() + it("can resume testing with vim.defer_fn", function() + local co = coroutine.running() + assert(co, "not running inside a coroutine") + + local timing = setmetatable({}, { __index = Timing }) + + vim.defer_fn(function() + coroutine.resume(co) + end, 200) + timing:log "before" + coroutine.yield() + timing:log "after" + timing:check("before", "after", 0.1) + end) + + it("can resume testing from job callback", function() + local co = coroutine.running() + assert(co, "not running inside a coroutine") + + local timing = setmetatable({}, { __index = Timing }) + + Job:new({ + command = "bash", + args = { + "-ce", + [[ + sleep 0.2 + echo hello + sleep 0.2 + echo world + sleep 0.2 + exit 42 + ]], + }, + on_stdout = function(_, data) + timing:log(data) + end, + on_exit = function(_, exit_status) + timing:log "exit" + --This is required so that the rest of the test will run in a proper context + vim.schedule(function() + coroutine.resume(co, exit_status) + end) + end, + }):start() + timing:log "job started" + local exit_status = coroutine.yield() + timing:log "job finished" + assert.are.equal(exit_status, 42) + + timing:check("job started", "job finished", 0.3) + timing:check("job started", "hello", 0.1) + timing:check("hello", "world", 0.1) + timing:check("world", "job finished", 0.1) + end) +end)