From 4c3c5f27ecdf8ca36a8001e1606ec281f2a597c7 Mon Sep 17 00:00:00 2001 From: Idan Arye Date: Wed, 7 Dec 2022 01:23:19 +0200 Subject: [PATCH 1/2] Close #424 - make tests run in coroutine to support asynchronous testing --- TESTS_README.md | 16 +++++++ lua/plenary/busted.lua | 52 +++++++++++---------- tests/plenary/async_testing_spec.lua | 69 ++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 24 deletions(-) create mode 100644 tests/plenary/async_testing_spec.lua 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..45fbc1d3 --- /dev/null +++ b/tests/plenary/async_testing_spec.lua @@ -0,0 +1,69 @@ +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) From 33b3b9f4a00d29cc5053c379552b0245135a60df Mon Sep 17 00:00:00 2001 From: Simon Hauser Date: Fri, 6 Jan 2023 15:18:43 +0100 Subject: [PATCH 2/2] fix: stylua --- tests/plenary/async_testing_spec.lua | 32 +++++++++++++++++----------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/tests/plenary/async_testing_spec.lua b/tests/plenary/async_testing_spec.lua index 45fbc1d3..c1c991be 100644 --- a/tests/plenary/async_testing_spec.lua +++ b/tests/plenary/async_testing_spec.lua @@ -10,7 +10,10 @@ 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)) + 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() @@ -18,14 +21,14 @@ describe("Async test", function() local co = coroutine.running() assert(co, "not running inside a coroutine") - local timing = setmetatable({}, {__index = Timing}) + local timing = setmetatable({}, { __index = Timing }) vim.defer_fn(function() coroutine.resume(co) end, 200) - timing:log("before") + timing:log "before" coroutine.yield() - timing:log("after") + timing:log "after" timing:check("before", "after", 0.1) end) @@ -33,32 +36,35 @@ describe("Async test", function() local co = coroutine.running() assert(co, "not running inside a coroutine") - local timing = setmetatable({}, {__index = Timing}) + local timing = setmetatable({}, { __index = Timing }) - Job:new { + Job:new({ command = "bash", - args = { "-ce", [[ + 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") + 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") + end, + }):start() + timing:log "job started" local exit_status = coroutine.yield() - timing:log("job finished") + timing:log "job finished" assert.are.equal(exit_status, 42) timing:check("job started", "job finished", 0.3)