Skip to content

Commit a6bfd69

Browse files
committed
fix: do not leak resources across tests so as to prevent side effects
Problem: Across unit tests, custom path_hook reigstered globally by ScriptHost globally was not being cleared up properly. This results in leakage of internal resources and dangling access to them (such as asyncio event loops that was already closed and no longer valid in other test case instances). More specifically, asyncio EventLoop's were never closed, which can result in "Event loop is closed" errors upon garbage collection during cleaning up pytest sessions. Solution: (1) Always call ScriptHost.teardown() when done using it. (2) Make sure all internal resources (event loops, transport channels, etc.) by closing the embedded neovim instance.
1 parent 260a0b9 commit a6bfd69

File tree

3 files changed

+34
-9
lines changed

3 files changed

+34
-9
lines changed

pynvim/msgpack_rpc/event_loop/asyncio.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ def _close(self) -> None:
145145
if self._raw_transport is not None:
146146
self._raw_transport.close()
147147
self._loop.close()
148+
if os.name != 'nt':
149+
self._child_watcher.close()
148150

149151
def _threadsafe_call(self, fn: Callable[[], Any]) -> None:
150152
self._loop.call_soon_threadsafe(fn)

test/conftest.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
"""Configs for pytest."""
2+
3+
import gc
14
import json
25
import os
6+
from typing import Generator
37

48
import pytest
59

@@ -9,7 +13,10 @@
913

1014

1115
@pytest.fixture
12-
def vim() -> pynvim.Nvim:
16+
def vim() -> Generator[pynvim.Nvim, None, None]:
17+
"""Create an embedded, sub-process Nvim fixture instance."""
18+
editor: pynvim.Nvim
19+
1320
child_argv = os.environ.get('NVIM_CHILD_ARGV')
1421
listen_address = os.environ.get('NVIM_LISTEN_ADDRESS')
1522
if child_argv is None and listen_address is None:
@@ -28,4 +35,13 @@ def vim() -> pynvim.Nvim:
2835
assert listen_address is not None and listen_address != ''
2936
editor = pynvim.attach('socket', path=listen_address)
3037

31-
return editor
38+
try:
39+
yield editor
40+
41+
finally:
42+
# Ensure all internal resources (pipes, transports, etc.) are always
43+
# closed properly. Otherwise, during GC finalizers (__del__) will raise
44+
# "Event loop is closed" error.
45+
editor.close()
46+
47+
gc.collect() # force-run GC, to early-detect potential leakages

test/test_host.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,19 @@
1111

1212
def test_host_imports(vim):
1313
h = ScriptHost(vim)
14-
assert h.module.__dict__['vim']
15-
assert h.module.__dict__['vim'] == h.legacy_vim
16-
assert h.module.__dict__['sys']
14+
try:
15+
assert h.module.__dict__['vim']
16+
assert h.module.__dict__['vim'] == h.legacy_vim
17+
assert h.module.__dict__['sys']
18+
finally:
19+
h.teardown()
1720

1821

1922
def test_host_import_rplugin_modules(vim):
2023
# Test whether a Host can load and import rplugins (#461).
2124
# See also $VIMRUNTIME/autoload/provider/pythonx.vim.
2225
h = Host(vim)
26+
2327
plugins: Sequence[str] = [ # plugin paths like real rplugins
2428
os.path.join(__PATH__, "./fixtures/simple_plugin/rplugin/python3/simple_nvim.py"),
2529
os.path.join(__PATH__, "./fixtures/module_plugin/rplugin/python3/mymodule/"),
@@ -56,7 +60,10 @@ def test_host_async_error(vim):
5660

5761
def test_legacy_vim_eval(vim):
5862
h = ScriptHost(vim)
59-
assert h.legacy_vim.eval('1') == '1'
60-
assert h.legacy_vim.eval('v:null') is None
61-
assert h.legacy_vim.eval('v:true') is True
62-
assert h.legacy_vim.eval('v:false') is False
63+
try:
64+
assert h.legacy_vim.eval('1') == '1'
65+
assert h.legacy_vim.eval('v:null') is None
66+
assert h.legacy_vim.eval('v:true') is True
67+
assert h.legacy_vim.eval('v:false') is False
68+
finally:
69+
h.teardown()

0 commit comments

Comments
 (0)