Skip to content

Commit 815c55a

Browse files
committed
test: add tests for attaching through socket, tcp, and stdio
1 parent 71d2d65 commit 815c55a

File tree

2 files changed

+157
-0
lines changed

2 files changed

+157
-0
lines changed

test/test_attach.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
"""Tests other session_types than subprocess Nvim."""
2+
3+
import contextlib
4+
import os.path
5+
import socket
6+
import subprocess
7+
import tempfile
8+
import time
9+
from typing import Generator
10+
11+
import pytest
12+
import pytest_timeout # pylint: disable=unused-import # noqa
13+
14+
import pynvim
15+
from pynvim.api import Nvim
16+
17+
# pylint: disable=consider-using-with
18+
# pylint: disable=redefined-outer-name
19+
20+
21+
skip_on_windows = pytest.mark.skipif(
22+
"os.name == 'nt'", reason="Not supported in Windows")
23+
24+
25+
@pytest.fixture
26+
def tmp_socket() -> Generator[str, None, None]:
27+
"""Get a temporary UNIX socket file."""
28+
# see cpython#93914
29+
addr = tempfile.mktemp(prefix="test_python_", suffix='.sock',
30+
dir=os.path.curdir)
31+
try:
32+
yield addr
33+
finally:
34+
if os.path.exists(addr):
35+
with contextlib.suppress(OSError):
36+
os.unlink(addr)
37+
38+
39+
@skip_on_windows
40+
def test_connect_socket(tmp_socket: str) -> None:
41+
"""Tests UNIX socket connection."""
42+
p = subprocess.Popen(["nvim", "--clean", "-n", "--headless",
43+
"--listen", tmp_socket])
44+
time.sleep(0.2) # wait a bit until nvim starts up
45+
46+
try:
47+
nvim: Nvim = pynvim.attach('socket', path=tmp_socket)
48+
assert 42 == nvim.eval('42')
49+
assert "?" == nvim.command_output('echo "?"')
50+
finally:
51+
with contextlib.suppress(OSError):
52+
p.terminate()
53+
54+
55+
@skip_on_windows
56+
def test_connect_socket_fail() -> None:
57+
"""Tests UNIX socket connection, when the sock file is not found."""
58+
with pytest.raises(FileNotFoundError):
59+
pynvim.attach('socket', path='/tmp/not-exist.socket')
60+
61+
62+
def find_free_port() -> int:
63+
"""Find a free, available port number."""
64+
with socket.socket() as sock:
65+
sock.bind(('', 0)) # Bind to a free port provided by the host.
66+
return sock.getsockname()[1]
67+
68+
69+
def test_connect_tcp() -> None:
70+
"""Tests TCP connection."""
71+
address = '127.0.0.1'
72+
port = find_free_port()
73+
p = subprocess.Popen(["nvim", "--clean", "-n", "--headless",
74+
"--listen", f"{address}:{port}"])
75+
time.sleep(0.2) # wait a bit until nvim starts up
76+
77+
try:
78+
nvim: Nvim = pynvim.attach('tcp', address=address, port=port)
79+
assert 42 == nvim.eval('42')
80+
assert "?" == nvim.command_output('echo "?"')
81+
finally:
82+
with contextlib.suppress(OSError):
83+
p.terminate()
84+
85+
86+
@pytest.mark.timeout(3.0)
87+
def test_connect_tcp_fail() -> None:
88+
"""Tests TCP socket connection, in some common failure scenarios."""
89+
port = find_free_port()
90+
91+
with pytest.raises(OSError): # ConnectionRefusedError
92+
pynvim.attach('tcp', address='127.0.0.1', port=port)
93+
94+
# Create a dummy TCP http server that is not a neovim.
95+
# It will never write an expected response in the handshaking communication
96+
# (i.e., requesting "nvim_get_api_info()")
97+
# TODO: On a client side, rather than blocking forever (hence Timeout),
98+
# pynvim should handle errors in an appropriate manner.
99+
pytest.skip(reason="Need to implement timeout in RPC")
100+
101+
p = subprocess.Popen( # type: ignore[unreachable]
102+
["python", "-m", "http.server", str(port)]
103+
)
104+
time.sleep(0.2)
105+
106+
try:
107+
# with pytest.raises(OSError): # ConnectionRefusedError
108+
pynvim.attach('tcp', address='127.0.0.1', port=port)
109+
110+
finally:
111+
with contextlib.suppress(OSError):
112+
p.terminate()
113+
114+
115+
@skip_on_windows
116+
def test_connect_stdio(vim: Nvim) -> None:
117+
"""Tests stdio connection, using jobstart(..., {'rpc': v:true})."""
118+
119+
def source(vim: Nvim, code: str) -> None:
120+
fd, fname = tempfile.mkstemp()
121+
try:
122+
with os.fdopen(fd, 'w') as f:
123+
f.write(code)
124+
vim.command('source ' + fname)
125+
finally:
126+
os.unlink(fname)
127+
128+
source(vim, """
129+
function! OutputHandler(j, lines, event_type)
130+
if a:event_type == 'stderr'
131+
for l:line in a:lines
132+
echom l:line
133+
endfor
134+
endif
135+
endfunction
136+
""")
137+
138+
remote_py_code = '\n'.join([
139+
'import pynvim',
140+
'nvim = pynvim.attach("stdio")',
141+
'print("rplugins can write to stdout")', # see #60
142+
'nvim.api.command("let g:success = 42")',
143+
])
144+
# see :help jobstart(), *jobstart-options* |msgpack-rpc|
145+
jobid = vim.funcs.jobstart([
146+
'python', '-c', remote_py_code,
147+
], {'rpc': True, 'on_stderr': 'OutputHandler'})
148+
assert jobid > 0
149+
exitcode = vim.funcs.jobwait([jobid], 500)[0]
150+
messages = vim.command_output('messages')
151+
assert exitcode == 0, ("the python process failed, :messages =>\n\n" +
152+
messages)
153+
154+
assert 42 == vim.eval('g:success')
155+
assert "rplugins can write to stdout" in messages

test/test_vim.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""Tests interaction with neovim via Nvim API (with child process)."""
2+
13
import os
24
import tempfile
35
from typing import Any

0 commit comments

Comments
 (0)