From 399cecb8e7d8ea00cb4f79a7fe45ca9d79965ab5 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Mon, 19 Oct 2026 15:07:12 +0200 Subject: [PATCH 1/4] test: unreliable treesitter 'foldexpr' tests Problem: On slow CI runners (asan/tsan/macOS), fold_spec sometimes reads incomplete fold levels: FAILED test/functional/treesitter/fold_spec.lua @ 88: treesitter foldexpr recomputes fold levels after lines are added/removed Expected: { "0", "0", ">1", "1", "1", "0", "0", ">1", "1", "1", "1", "1", ">2", "2", "2", "1", "0" } Actual: { "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0", "0" } Folds are computed after an async parse, which runs in 3ms slices. `poke_eventloop()` only runs the first slice. Solution: Queue a parse callback (it runs after the fold update) and wait for it. --- test/functional/treesitter/fold_spec.lua | 47 ++++++++++++++++-------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/test/functional/treesitter/fold_spec.lua b/test/functional/treesitter/fold_spec.lua index c61bc851d3..9e9b5b0464 100644 --- a/test/functional/treesitter/fold_spec.lua +++ b/test/functional/treesitter/fold_spec.lua @@ -47,6 +47,21 @@ void ui_refresh(void) ) end + --- Waits for the async parse, and the fold update that follows it. + local function wait_folds() + exec_lua(function() + local done = false + -- Queued after the in-flight parse's callbacks (if any), so folds have updated when it runs. + vim.treesitter.get_parser(0):parse(nil, function() + done = true + end) + assert(vim.wait(5000, function() + return done + end)) + end) + poke_eventloop() + end + local function get_fold_levels() return exec_lua(function() local res = {} @@ -91,7 +106,7 @@ void ui_refresh(void) parse('c') command('1,2d') - poke_eventloop() + wait_folds() eq({ [1] = '0', @@ -114,7 +129,7 @@ void ui_refresh(void) }, get_fold_levels()) command('1put!') - poke_eventloop() + wait_folds() eq({ [1] = '>1', @@ -157,7 +172,7 @@ t2]]) }, get_fold_levels()) feed('2ggo') - poke_eventloop() + wait_folds() eq({ [1] = '>1', @@ -168,7 +183,7 @@ t2]]) }, get_fold_levels()) feed('dd') - poke_eventloop() + wait_folds() eq({ [1] = '>1', @@ -178,7 +193,7 @@ t2]]) }, get_fold_levels()) feed('2ggdd') - poke_eventloop() + wait_folds() eq({ [1] = '0', @@ -187,7 +202,7 @@ t2]]) }, get_fold_levels()) feed('u') - poke_eventloop() + wait_folds() eq({ [1] = '>1', @@ -197,7 +212,7 @@ t2]]) }, get_fold_levels()) feed('3ggdd') - poke_eventloop() + wait_folds() eq({ [1] = '>1', @@ -206,7 +221,7 @@ t2]]) }, get_fold_levels()) feed('u') - poke_eventloop() + wait_folds() eq({ [1] = '>1', @@ -217,7 +232,7 @@ t2]]) feed('3ggI#') parse() - poke_eventloop() + wait_folds() eq({ [1] = '>1', @@ -228,7 +243,7 @@ t2]]) feed('x') parse() - poke_eventloop() + wait_folds() eq({ [1] = '>1', @@ -264,7 +279,7 @@ end }, get_fold_levels()) command('1,4join') - poke_eventloop() + wait_folds() eq({ [1] = '0', @@ -272,7 +287,7 @@ end }, get_fold_levels()) feed('u') - poke_eventloop() + wait_folds() eq({ [1] = '>1', @@ -317,7 +332,7 @@ end]]) command('1,8join') feed('u') - poke_eventloop() + wait_folds() eq({ [1] = '>1', @@ -361,7 +376,7 @@ end]]) }, get_fold_levels()) command('2,6join') - poke_eventloop() + wait_folds() eq({ [1] = '>1', @@ -370,7 +385,7 @@ end]]) }, get_fold_levels()) feed('u') - poke_eventloop() + wait_folds() eq({ [1] = '>1', @@ -803,6 +818,7 @@ t2]]) [18] = '2', [19] = '1', } + wait_folds() eq(foldlevels, get_fold_levels()) -- only changing filetype should change the parser again @@ -810,6 +826,7 @@ t2]]) expect_no_folds() command('set ft=c') + wait_folds() eq(foldlevels, get_fold_levels()) end) From 3c196b8650b606e31c071d1a2e7e1e1a87a1eedd Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Mon, 19 Oct 2026 15:12:47 +0200 Subject: [PATCH 2/4] =?UTF-8?q?test:=20unreliable=20"TUI=20client=20exits?= =?UTF-8?q?=20=E2=80=A6=20with=20:cquit"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: On Windows, the remote TUI client sometimes exits 0 instead of 42: FAILED test/functional/terminal/tui_spec.lua @ 5179: TUI client exits when server quits with :cquit Failed to match any screen lines. Expected (anywhere): "Process exited 42" Actual: |[Process exited 0] | Analysis: Possibly the client drops the unread "error_exit" event when a write to the closed pipe fails. Solution: Set `retries = 2`. --- test/functional/terminal/tui_spec.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 8ec1ed158c..1e506d1d72 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -5200,7 +5200,7 @@ describe('TUI client', function() test_remote_tui_quit() end) - it('with :cquit', function() + it('with :cquit', { retries = 2 }, function() test_remote_tui_quit(42) end) end) From 139a376a6dc6faf9ccfb5d09ec6462d325dbdcac Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Mon, 19 Oct 2026 15:18:46 +0200 Subject: [PATCH 3/4] test: unreliable "detach! keeps UI attached, sets detach=true" Problem: On Windows, the server sometimes dies after the TUI client is killed: FAILED test/functional/terminal/tui_spec.lua @ 303: TUI :detach detach! keeps UI attached, sets detach=true retry() attempts: 149 test\client\uv_stream.lua:90: ENOENT Analysis: The server shares the client's console until rpc_close_event() swaps it to a hidden console (os_detach_ui_console). If the :terminal closes the console (on client exit) first, the server gets SIGHUP (CTRL_CLOSE_EVENT) and exits. Solution: Delay the :terminal's job-exit handling (`vim.wait(fast_only=true)`) until the server has dropped the UI. --- test/functional/terminal/tui_spec.lua | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 1e506d1d72..76fbff43ab 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -297,10 +297,19 @@ describe('TUI :detach', function() end) -- Simulate the host terminal disconnecting: SIGKILL the client so it can't shut down cleanly. - -- On Windows this also exercises the console hand-off in rpc_close_event(). - n.exec_lua(function(pid) + -- + -- On Windows this also exercises console hand-off in rpc_close_event(), which must finish + -- before the :terminal closes the console (client exit), else the server dies with it. So defer + -- this Nvim's job-exit handling (`vim.wait(fast_only=true)`) until the server drops the UI. + n.exec_lua(function(pid, addr) vim.uv.kill(pid, 'sigkill') - end, n.fn.jobpid(term_job)) + local chan = vim.fn.sockconnect('pipe', addr, { rpc = true }) + assert(vim.wait(4000, function() + return #vim.rpcrequest(chan, 'nvim_list_uis') == 0 + end, 10, true)) + vim.fn.chanclose(chan) + end, n.fn.jobpid(term_job), child_server) + screen:expect({ any = vim.pesc('[Process exited') }) retry(nil, 4000, function() eq(2, ({ n.connect(child_server):request('nvim_eval', '1+1') })[2]) From 9916944868c54aab93c00da6555be66f0b9ce630 Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Mon, 19 Oct 2026 18:50:29 +0200 Subject: [PATCH 4/4] =?UTF-8?q?ci(windows):=20MSYS=20tools=20sometimes=20c?= =?UTF-8?q?rash=20with=20"add=5Fitem=20=E2=80=A6=20failed"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: On Windows, a test that runs a Git `usr/bin` tool (`cat`, `diff`, …) sometimes stalls 15s and then fails: FAILED test/functional/vimscript/system_spec.lua @ 470: systemlist() passing string with linefeed characters as input splits the output on linefeed characters Expected: { "abc", "def", "ghi" } Actual: { ' 0 [main] cat (2736) C:\\Program Files\\Git\\usr\\bin\\cat.exe: *** fatal error - add_item ("\\??\\C:\\Program Files\\Git", "/", ...) failed, errno 1' } The MSYS runtime sets up per-user shared state on first use. If a test kills an MSYS process (CTRL-C) during that setup, the next MSYS process (in any parallel test group) waits 15s for it, then sets it up again and dies. Solution: Keep an MSYS process (`sleep infinity`) alive for the test run, so the setup happens only once. --- .github/workflows/test_windows.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test_windows.yml b/.github/workflows/test_windows.yml index b6a4bc96fe..290e829379 100644 --- a/.github/workflows/test_windows.yml +++ b/.github/workflows/test_windows.yml @@ -67,6 +67,10 @@ jobs: TEST_TIMEOUT: 600 run: | $ErrorActionPreference = 'Continue' + # Keep an MSYS process (Git's usr/bin) alive, so the MSYS runtime initializes its shared + # state only once. Else a test that kills an MSYS process during that init (CTRL-C) can + # make the next one stall 15s and die: "fatal error - add_item (...) failed, errno 1". + Start-Process -WindowStyle Hidden (Get-Command sleep.exe).Source infinity # `-k 0` tells ninja to continue running all targets even if some fail. cmake --build build --target functionaltest-parallel -j 2 -- -k 0 $exitCode = $LASTEXITCODE