mirror of
git://git.sv.gnu.org/emacs.git
synced 2026-06-14 04:21:24 +00:00
Jsonrpc: add new tests using Python subprocesses
Most of these tests are for the scontrol/"anxious continuation" mechanism The new ERT tests use Python subprocesses via stdin/stdout pipe as JSONRPC endpoints. A shared framing library lives in jsonrpc-resources/common.py. * test/lisp/jsonrpc-tests.el (jsonrpc--test-dir): New constant. (jsonrpc--with-python-fixture): New macro. (scontrol-remote-during-sync): New test. (scontrol-anxious-nested): New test. (scontrol-remote-error): New test. (shutdown-clean-after-notification): New test. * test/lisp/jsonrpc-resources/common.py: New file. * test/lisp/jsonrpc-resources/server-remote-during-sync.py: New file. * test/lisp/jsonrpc-resources/server-anxious-nested.py: New file. * test/lisp/jsonrpc-resources/server-remote-error.py: New file. * test/lisp/jsonrpc-resources/server-harakiri.py: New file.
This commit is contained in:
parent
6800aba396
commit
e09d8c4d78
6 changed files with 326 additions and 0 deletions
37
test/lisp/jsonrpc-resources/common.py
Normal file
37
test/lisp/jsonrpc-resources/common.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
"""Common JSONRPC framing helpers for jsonrpc.el test servers."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
def read_msg():
|
||||
"""Read one Content-Length-framed JSON-RPC message from stdin."""
|
||||
headers = {}
|
||||
while True:
|
||||
line = sys.stdin.buffer.readline()
|
||||
if not line:
|
||||
return None
|
||||
text = line.decode('utf-8').rstrip('\r\n')
|
||||
if not text:
|
||||
break
|
||||
if ':' in text:
|
||||
k, _, v = text.partition(':')
|
||||
headers[k.strip()] = v.strip()
|
||||
n = int(headers.get('Content-Length', 0))
|
||||
if not n:
|
||||
return None
|
||||
return json.loads(sys.stdin.buffer.read(n).decode('utf-8'))
|
||||
|
||||
|
||||
def write_msg(msg):
|
||||
"""Write one Content-Length-framed JSON-RPC message to stdout."""
|
||||
body = json.dumps(msg, ensure_ascii=False).encode('utf-8')
|
||||
sys.stdout.buffer.write(
|
||||
f'Content-Length: {len(body)}\r\n\r\n'.encode('utf-8') + body
|
||||
)
|
||||
sys.stdout.buffer.flush()
|
||||
|
||||
|
||||
def log(text):
|
||||
"""Write a log line to stderr."""
|
||||
print(f'[test-server] {text}', file=sys.stderr, flush=True)
|
||||
58
test/lisp/jsonrpc-resources/server-anxious-nested.py
Normal file
58
test/lisp/jsonrpc-resources/server-anxious-nested.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Test server for scontrol-anxious-nested.
|
||||
|
||||
Choreography (exercises the anxious-continuation mechanism):
|
||||
|
||||
client -> server: taskA (id=1)
|
||||
server -> client: callBack (id=1000)
|
||||
server -> client: response taskA "done" <- anxious: arrives while followUp pending
|
||||
client -> server: followUp (id=2) <- sent by rdispatcher for callBack
|
||||
server -> client: response followUp "fw-ok"
|
||||
client -> server: response callBack <- rdispatcher return value
|
||||
|
||||
scontrol stack at deepest point:
|
||||
((:local 2) (:remote 1000) (:local 1))
|
||||
|
||||
The response-to-taskA arrives while (:local followUp-id) is the head,
|
||||
so it is queued as an anxious continuation. When followUp completes,
|
||||
the anxious entry is rescheduled via run-at-time and taskA resolves.
|
||||
"""
|
||||
import os, sys
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from common import read_msg, write_msg, log
|
||||
|
||||
|
||||
def main():
|
||||
while True:
|
||||
msg = read_msg()
|
||||
if msg is None:
|
||||
break
|
||||
mid = msg.get('id')
|
||||
method = msg.get('method')
|
||||
log(f'<- {method or "(response)"} id={mid}')
|
||||
if method == 'harakiri':
|
||||
log('-> harakiri: exiting cleanly')
|
||||
break
|
||||
elif method == 'taskA':
|
||||
# Send callBack, then immediately respond to taskA without awaiting
|
||||
# anything. The response-to-taskA will be queued as anxious on the
|
||||
# client while its rdispatcher blocks waiting for followUp.
|
||||
write_msg({'jsonrpc': '2.0', 'id': 1000,
|
||||
'method': 'callBack', 'params': {}})
|
||||
log('-> callBack id=1000')
|
||||
write_msg({'jsonrpc': '2.0', 'id': mid, 'result': 'done'})
|
||||
log(f'-> (response taskA) id={mid}')
|
||||
# followUp arrives next: the client's rdispatcher for callBack
|
||||
# issues it as a nested sync request.
|
||||
follow = read_msg()
|
||||
fid = follow.get('id') if follow else None
|
||||
log(f'<- followUp id={fid}')
|
||||
write_msg({'jsonrpc': '2.0', 'id': fid, 'result': 'fw-ok'})
|
||||
log(f'-> (response followUp) id={fid}')
|
||||
# Finally collect the callBack response (rdispatcher return value).
|
||||
cb_resp = read_msg()
|
||||
log(f'<- (response callBack) id={cb_resp.get("id") if cb_resp else None}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
24
test/lisp/jsonrpc-resources/server-harakiri.py
Normal file
24
test/lisp/jsonrpc-resources/server-harakiri.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Test server for shutdown-clean test.
|
||||
|
||||
Waits for a 'harakiri' notification and then exits cleanly.
|
||||
"""
|
||||
import os, sys
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from common import read_msg, log
|
||||
|
||||
|
||||
def main():
|
||||
while True:
|
||||
msg = read_msg()
|
||||
if msg is None:
|
||||
break
|
||||
method = msg.get('method')
|
||||
log(f'<- {method or "(response)"} id={msg.get("id")}')
|
||||
if method == 'harakiri':
|
||||
log('-> harakiri: exiting cleanly')
|
||||
break
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
43
test/lisp/jsonrpc-resources/server-remote-during-sync.py
Normal file
43
test/lisp/jsonrpc-resources/server-remote-during-sync.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Test server for scontrol-remote-during-sync.
|
||||
|
||||
Choreography (tests bug#80623):
|
||||
|
||||
client -> server: taskA (id=1)
|
||||
server -> client: showInfo (id=1000) <- before responding to taskA
|
||||
server -> client: response taskA "done"
|
||||
client -> server: response showInfo <- from rdispatcher
|
||||
|
||||
The (:remote 1000) entry on scontrol defers the response-to-taskA
|
||||
continuation until showInfo dispatch completes.
|
||||
"""
|
||||
import os, sys
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from common import read_msg, write_msg, log
|
||||
|
||||
|
||||
def main():
|
||||
while True:
|
||||
msg = read_msg()
|
||||
if msg is None:
|
||||
break
|
||||
mid = msg.get('id')
|
||||
method = msg.get('method')
|
||||
log(f'<- {method or "(response)"} id={mid}')
|
||||
if method == 'harakiri':
|
||||
log('-> harakiri: exiting cleanly')
|
||||
break
|
||||
elif method == 'taskA':
|
||||
# Send showInfo request BEFORE responding to taskA.
|
||||
write_msg({'jsonrpc': '2.0', 'id': 1000,
|
||||
'method': 'showInfo', 'params': {}})
|
||||
log('-> showInfo id=1000')
|
||||
write_msg({'jsonrpc': '2.0', 'id': mid, 'result': 'done'})
|
||||
log(f'-> (response taskA) id={mid}')
|
||||
# Collect the client's response to showInfo.
|
||||
resp = read_msg()
|
||||
log(f'<- (response showInfo) id={resp.get("id") if resp else None}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
44
test/lisp/jsonrpc-resources/server-remote-error.py
Normal file
44
test/lisp/jsonrpc-resources/server-remote-error.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Test server for scontrol-remote-error.
|
||||
|
||||
Choreography (anxious continuation survives an rdispatcher error):
|
||||
|
||||
client -> server: taskA (id=1)
|
||||
server -> client: badMethod (id=1000) <- rdispatcher signals jsonrpc-error
|
||||
server -> client: response taskA "ok" <- anxious during badMethod dispatch
|
||||
client -> server: error response badMethod {code: -32601}
|
||||
|
||||
Even though the remote-request dispatch produces an error reply, the
|
||||
anxious continuation for taskA must still fire and resolve to "ok".
|
||||
"""
|
||||
import os, sys
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from common import read_msg, write_msg, log
|
||||
|
||||
|
||||
def main():
|
||||
while True:
|
||||
msg = read_msg()
|
||||
if msg is None:
|
||||
break
|
||||
mid = msg.get('id')
|
||||
method = msg.get('method')
|
||||
log(f'<- {method or "(response)"} id={mid}')
|
||||
if method == 'harakiri':
|
||||
log('-> harakiri: exiting cleanly')
|
||||
break
|
||||
elif method == 'taskA':
|
||||
# Send badMethod BEFORE responding to taskA; the client rdispatcher
|
||||
# will signal a jsonrpc-error for it.
|
||||
write_msg({'jsonrpc': '2.0', 'id': 1000,
|
||||
'method': 'badMethod', 'params': {}})
|
||||
log('-> badMethod id=1000')
|
||||
write_msg({'jsonrpc': '2.0', 'id': mid, 'result': 'ok'})
|
||||
log(f'-> (response taskA) id={mid}')
|
||||
# Collect the error response to badMethod.
|
||||
err = read_msg()
|
||||
log(f'<- (error response badMethod): {err}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
@ -252,5 +252,125 @@
|
|||
(should (eq 2 n-deferred-2))
|
||||
(should (eq 0 (hash-table-count (jsonrpc--deferred-actions conn)))))))
|
||||
|
||||
|
||||
;;; Tests using Python subprocesses (scontrol / anxious mechanism)
|
||||
;;;
|
||||
|
||||
(defconst jsonrpc--test-dir
|
||||
(file-name-directory (or load-file-name buffer-file-name))
|
||||
"Directory of this test file, captured at load time.")
|
||||
|
||||
(cl-defmacro jsonrpc--with-python-fixture ((script conn &rest initargs) &body body)
|
||||
"Start SCRIPT under python3 as a pipe subprocess, bind connection to CONN.
|
||||
SCRIPT is a path relative to this file's directory.
|
||||
INITARGS are passed to `make-instance' for `jsonrpc-process-connection'."
|
||||
(declare (indent 1))
|
||||
`(let ((,conn nil))
|
||||
(unwind-protect
|
||||
(progn
|
||||
(setq ,conn
|
||||
(make-instance
|
||||
'jsonrpc-process-connection
|
||||
:name "jsonrpc-python-test"
|
||||
:process (make-process
|
||||
:name "jsonrpc-python-test"
|
||||
:command (list "python3"
|
||||
(expand-file-name
|
||||
,script
|
||||
jsonrpc--test-dir))
|
||||
:connection-type 'pipe
|
||||
:noquery t)
|
||||
,@initargs))
|
||||
(with-timeout (5
|
||||
(when ,conn
|
||||
(let ((buf (jsonrpc--events-buffer ,conn)))
|
||||
(when (buffer-live-p buf)
|
||||
(if noninteractive
|
||||
(progn
|
||||
(message "contents of `%s':" (buffer-name buf))
|
||||
(princ (with-current-buffer buf (buffer-string))
|
||||
#'external-debugging-output))
|
||||
(message "Preserved for inspection: %s"
|
||||
(buffer-name buf))))))
|
||||
(ert-fail "Test timed out after 5s"))
|
||||
,@body))
|
||||
(when ,conn
|
||||
(ignore-errors
|
||||
(jsonrpc-notify ,conn 'harakiri nil)
|
||||
(kill-buffer (jsonrpc--events-buffer ,conn))
|
||||
(jsonrpc-shutdown ,conn))))))
|
||||
|
||||
(ert-deftest scontrol-remote-during-sync ()
|
||||
"Server sends a remote request before the sync-request response (bug#80623).
|
||||
The (:remote ID) entry on scontrol defers the response-to-taskA throw
|
||||
until the showInfo dispatch completes, preventing a spurious -32603."
|
||||
(skip-unless (executable-find "python3"))
|
||||
(skip-when (eq system-type 'windows-nt))
|
||||
(jsonrpc--with-python-fixture
|
||||
("jsonrpc-resources/server-remote-during-sync.py" conn
|
||||
:request-dispatcher
|
||||
(lambda (_conn method _params)
|
||||
(pcase method
|
||||
('showInfo "ack")
|
||||
(_ (error "unexpected method: %s" method)))))
|
||||
(should (equal "done" (jsonrpc-request conn 'taskA [] :timeout 5)))))
|
||||
|
||||
(ert-deftest scontrol-anxious-nested ()
|
||||
"Anxious continuation: rdispatcher issues a nested sync request.
|
||||
The outer sync response arrives while the inner (followUp) is pending,
|
||||
gets queued as anxious, and is rescheduled via run-at-time once the
|
||||
inner completes. Exercises the full three-deep scontrol stack:
|
||||
((:local followUp-id) (:remote callBack-id) (:local taskA-id))"
|
||||
(skip-unless (executable-find "python3"))
|
||||
(skip-when (eq system-type 'windows-nt))
|
||||
(let (followup-result)
|
||||
(jsonrpc--with-python-fixture
|
||||
("jsonrpc-resources/server-anxious-nested.py" conn
|
||||
:request-dispatcher
|
||||
(lambda (conn method _params)
|
||||
(pcase method
|
||||
('callBack
|
||||
(setq followup-result
|
||||
(jsonrpc-request conn 'followUp [] :timeout 5))
|
||||
followup-result)
|
||||
(_ (error "unexpected method: %s" method)))))
|
||||
(should (equal "done" (jsonrpc-request conn 'taskA [] :timeout 5)))
|
||||
(should (equal "fw-ok" followup-result)))))
|
||||
|
||||
(ert-deftest scontrol-remote-error ()
|
||||
"Anxious continuation fires even when the rdispatcher signals a jsonrpc-error.
|
||||
The (:remote ID) unwind-protect calls jsonrpc--continue regardless of
|
||||
whether dispatch succeeded or produced an error reply."
|
||||
(skip-unless (executable-find "python3"))
|
||||
(skip-when (eq system-type 'windows-nt))
|
||||
(jsonrpc--with-python-fixture
|
||||
("jsonrpc-resources/server-remote-error.py" conn
|
||||
:request-dispatcher
|
||||
(lambda (_conn method _params)
|
||||
(pcase method
|
||||
('badMethod
|
||||
(signal 'jsonrpc-error
|
||||
'((jsonrpc-error-message . "method not allowed")
|
||||
(jsonrpc-error-code . -32601))))
|
||||
(_ (error "unexpected method: %s" method)))))
|
||||
(should (equal "ok" (jsonrpc-request conn 'taskA [] :timeout 5)))))
|
||||
|
||||
(ert-deftest shutdown-clean-after-notification ()
|
||||
"Server exits cleanly after harakiri notification.
|
||||
`jsonrpc-shutdown' should not emit a \"Sentinel hasn't run\" warning."
|
||||
(skip-unless (executable-find "python3"))
|
||||
(skip-when (eq system-type 'windows-nt))
|
||||
(let (warned)
|
||||
(cl-letf (((symbol-function 'jsonrpc--warn)
|
||||
(lambda (fmt &rest args)
|
||||
(setq warned (apply #'format fmt args)))))
|
||||
(jsonrpc--with-python-fixture
|
||||
("jsonrpc-resources/server-harakiri.py" conn)
|
||||
(jsonrpc-notify conn 'harakiri nil)
|
||||
;; Give the server time to exit before shutdown checks the sentinel.
|
||||
(accept-process-output nil 0.3)
|
||||
(jsonrpc-shutdown conn)))
|
||||
(should-not warned)))
|
||||
|
||||
(provide 'jsonrpc-tests)
|
||||
;;; jsonrpc-tests.el ends here
|
||||
|
|
|
|||
Loading…
Reference in a new issue