diff options
| -rw-r--r-- | lisp/jsonrpc.el | 738 | ||||
| -rw-r--r-- | test/lisp/jsonrpc-tests.el | 242 |
2 files changed, 980 insertions, 0 deletions
diff --git a/lisp/jsonrpc.el b/lisp/jsonrpc.el new file mode 100644 index 00000000000..70044320b44 --- /dev/null +++ b/lisp/jsonrpc.el | |||
| @@ -0,0 +1,738 @@ | |||
| 1 | ;;; jsonrpc.el --- JSON-RPC library -*- lexical-binding: t; -*- | ||
| 2 | |||
| 3 | ;; Copyright (C) 2018 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | ;; Author: João Távora <joaotavora@gmail.com> | ||
| 6 | ;; Maintainer: João Távora <joaotavora@gmail.com> | ||
| 7 | ;; Keywords: processes, languages, extensions | ||
| 8 | |||
| 9 | ;; This program is free software; you can redistribute it and/or modify | ||
| 10 | ;; it under the terms of the GNU General Public License as published by | ||
| 11 | ;; the Free Software Foundation, either version 3 of the License, or | ||
| 12 | ;; (at your option) any later version. | ||
| 13 | |||
| 14 | ;; This program is distributed in the hope that it will be useful, | ||
| 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 17 | ;; GNU General Public License for more details. | ||
| 18 | |||
| 19 | ;; You should have received a copy of the GNU General Public License | ||
| 20 | ;; along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
| 21 | |||
| 22 | ;;; Commentary: | ||
| 23 | |||
| 24 | ;; (Library originally extracted from eglot.el, an Emacs LSP client) | ||
| 25 | ;; | ||
| 26 | ;; This library implements the JSONRPC 2.0 specification as described | ||
| 27 | ;; in http://www.jsonrpc.org/. As the name suggests, JSONRPC is a | ||
| 28 | ;; generic Remote Procedure Call protocol designed around JSON | ||
| 29 | ;; objects. | ||
| 30 | ;; | ||
| 31 | ;; Quoting from the spec: "[JSONRPC] is transport agnostic in that the | ||
| 32 | ;; concepts can be used within the same process, over sockets, over | ||
| 33 | ;; http, or in many various message passing environments." | ||
| 34 | ;; | ||
| 35 | ;; To model this agnosticism, jsonrpc.el uses objects derived from a | ||
| 36 | ;; base `jsonrpc-connection' class, which is "abstract" or "virtual" | ||
| 37 | ;; (in modern OO parlance) and represents the connection to the remote | ||
| 38 | ;; JSON endpoint. Around this class we can define two distinct APIs: | ||
| 39 | ;; | ||
| 40 | ;; 1) A user interface to the JSONRPC _application_, whereby the | ||
| 41 | ;; `jsonrpc-connection' object is instantiated and used to communicate | ||
| 42 | ;; with the remote JSONRPC enpoint. | ||
| 43 | ;; | ||
| 44 | ;; In this scenario, the JSONRPC application makes the object using | ||
| 45 | ;; `make-instance' and initiates contacts to the remove endpoint by | ||
| 46 | ;; passing it to `jsonrpc-notify', `jsonrpc-request' and | ||
| 47 | ;; `jsonrpc-async-request'. For handling remotely initiated contacts, | ||
| 48 | ;; which generally come in asynchronously, the `make-instance' | ||
| 49 | ;; invocation should include `:request-dispatcher' and | ||
| 50 | ;; `:notification-dispatcher' initargs, which are two functions | ||
| 51 | ;; receiving the connection object, a symbol naming the JSONRPC | ||
| 52 | ;; method, and a JSONRPC "params" object. | ||
| 53 | ;; | ||
| 54 | ;; The function passed as `:request-dispatcher' handles the remote | ||
| 55 | ;; endpoint's requests, which expect a reply from the local endpoint. | ||
| 56 | ;; The function may return locally or non-locally (error). A local | ||
| 57 | ;; return value should be a JSON object which determines a success | ||
| 58 | ;; response and is serialized in the JSONRPC "result" object forwarded | ||
| 59 | ;; to the server. If, however, it uses the `jsonrpc-error' function | ||
| 60 | ;; to exit non-locally, this responds to the server with a JSONRPC | ||
| 61 | ;; "error" object instead, the details of which are filled out with | ||
| 62 | ;; the arguments with whatever was passed to `jsonrpc-error'. A | ||
| 63 | ;; suitable error reponse is also sent to the server if the function | ||
| 64 | ;; error unexpectedly with any other error that doesn't originate in a | ||
| 65 | ;; deliberate call to `jsonrpc-error'. | ||
| 66 | ;; | ||
| 67 | ;; 2) A inheritance-based interface to the JSONPRPC _transport | ||
| 68 | ;; implementation_, whereby `jsonrpc-connection' is subclassed so | ||
| 69 | ;; users of the user interface can communicate with JSONRPC endpoints | ||
| 70 | ;; using different underlying transport strategies. | ||
| 71 | ;; | ||
| 72 | ;; There are mandatory and optional parts to this API. | ||
| 73 | ;; | ||
| 74 | ;; For initiating contacts to the endpoint and replying to it, that | ||
| 75 | ;; subclass of `jsonrpc-connection' must implement | ||
| 76 | ;; `jsonrpc-connection-send' method. | ||
| 77 | ;; | ||
| 78 | ;; Likewise, for handling the three types remote endpoint's contacts | ||
| 79 | ;; (responses to requests, remotely initiated requests and remotely | ||
| 80 | ;; initiated notifications) the transport implementation must arrange | ||
| 81 | ;; for the function `jsonrpc-connection-receive' to be called after | ||
| 82 | ;; noticing a new JSONRPC message on the wire (whatever that "wire" | ||
| 83 | ;; may be). | ||
| 84 | ;; | ||
| 85 | ;; Finally, and optionally, the `jsonrpc-connection' subclass should | ||
| 86 | ;; implement `jsonrpc-shutdown' and `jsonrpc-running-p' if these | ||
| 87 | ;; concepts apply to the transport. | ||
| 88 | ;; | ||
| 89 | ;; For convenience, jsonrpc.el comes built-in with a | ||
| 90 | ;; `jsonrpc-process-connection' transport implementation that can talk | ||
| 91 | ;; to local subprocesses (through stdin/stdout) and TCP hosts using | ||
| 92 | ;; sockets. This uses some basic HTTP-style enveloping headers for | ||
| 93 | ;; JSON objects sent over the wire. For an example of an application | ||
| 94 | ;; using this transport scheme on top of JSONRPC, see the Language | ||
| 95 | ;; Server Protocol | ||
| 96 | ;; (https://microsoft.github.io/language-server-protocol/specification). | ||
| 97 | ;; `jsonrpc-process-connection' also implements `jsonrpc-shutdown', | ||
| 98 | ;; `jsonrpc-running-p'. | ||
| 99 | ;; | ||
| 100 | ;;;; About deferred requests and `jsonrpc-connection-p': | ||
| 101 | ;; | ||
| 102 | ;; In the user API. | ||
| 103 | ;; | ||
| 104 | ;;;; JSON object format: | ||
| 105 | ;; | ||
| 106 | ;; JSON objects are exchanged as keyword-value plists: plists are | ||
| 107 | ;; handed to the dispatcher functions and, likewise, plists should be | ||
| 108 | ;; given to `jsonrpc-notify', `jsonrpc-request' and | ||
| 109 | ;; `jsonrpc-async-request'. | ||
| 110 | ;; | ||
| 111 | ;; To facilitate handling plists, this library make liberal use of | ||
| 112 | ;; cl-lib.el and suggests (but doesn't force) its clients to do the | ||
| 113 | ;; same. A macro `jsonrpc-lambda' can be used to create a lambda for | ||
| 114 | ;; destructuring a JSON-object like in this example: | ||
| 115 | ;; | ||
| 116 | ;; (jsonrpc-async-request | ||
| 117 | ;; myproc :frobnicate `(:foo "trix") | ||
| 118 | ;; :success-fn (jsonrpc-lambda (&key bar baz &allow-other-keys) | ||
| 119 | ;; (message "Server replied back %s and %s!" | ||
| 120 | ;; bar baz)) | ||
| 121 | ;; :error-fn (jsonrpc-lambda (&key code message _data) | ||
| 122 | ;; (message "Sadly, server reports %s: %s" | ||
| 123 | ;; code message))) | ||
| 124 | ;; | ||
| 125 | ;;; Code: | ||
| 126 | |||
| 127 | (require 'cl-lib) | ||
| 128 | (require 'json) | ||
| 129 | (require 'eieio) | ||
| 130 | (require 'subr-x) | ||
| 131 | (require 'warnings) | ||
| 132 | (require 'pcase) | ||
| 133 | (require 'ert) ; to escape a `condition-case-unless-debug' | ||
| 134 | (require 'array) ; xor | ||
| 135 | |||
| 136 | |||
| 137 | ;;; Public API | ||
| 138 | ;;; | ||
| 139 | ;;;###autoload | ||
| 140 | (defclass jsonrpc-connection () | ||
| 141 | ((name | ||
| 142 | :accessor jsonrpc-name | ||
| 143 | :initarg :name | ||
| 144 | :documentation "A name for the connection") | ||
| 145 | (-request-dispatcher | ||
| 146 | :accessor jsonrpc--request-dispatcher | ||
| 147 | :initform #'ignore | ||
| 148 | :initarg :request-dispatcher | ||
| 149 | :documentation "Dispatcher for remotely invoked requests.") | ||
| 150 | (-notification-dispatcher | ||
| 151 | :accessor jsonrpc--notification-dispatcher | ||
| 152 | :initform #'ignore | ||
| 153 | :initarg :notification-dispatcher | ||
| 154 | :documentation "Dispatcher for remotely invoked notifications.") | ||
| 155 | (last-error | ||
| 156 | :accessor jsonrpc-last-error | ||
| 157 | :documentation "Last JSONRPC error message received from endpoint.") | ||
| 158 | (-request-continuations | ||
| 159 | :initform (make-hash-table) | ||
| 160 | :accessor jsonrpc--request-continuations | ||
| 161 | :documentation "A hash table of request ID to continuation lambdas.") | ||
| 162 | (-events-buffer | ||
| 163 | :accessor jsonrpc--events-buffer | ||
| 164 | :documentation "A buffer pretty-printing the JSON-RPC RPC events") | ||
| 165 | (-deferred-actions | ||
| 166 | :initform (make-hash-table :test #'equal) | ||
| 167 | :accessor jsonrpc--deferred-actions | ||
| 168 | :documentation "Map (DEFERRED BUF) to (FN TIMER ID). FN is\ | ||
| 169 | a saved DEFERRED `async-request' from BUF, to be sent not later\ | ||
| 170 | than TIMER as ID.") | ||
| 171 | (-next-request-id | ||
| 172 | :initform 0 | ||
| 173 | :accessor jsonrpc--next-request-id | ||
| 174 | :documentation "Next number used for a request")) | ||
| 175 | :documentation "Base class representing a JSONRPC connection. | ||
| 176 | The following initargs are accepted: | ||
| 177 | |||
| 178 | :NAME (mandatory), a string naming the connection | ||
| 179 | |||
| 180 | :REQUEST-DISPATCHER (optional), a function of three | ||
| 181 | arguments (CONN METHOD PARAMS) for handling JSONRPC requests. | ||
| 182 | CONN is a `jsonrpc-connection' object, method is a symbol, and | ||
| 183 | PARAMS is a plist representing a JSON object. The function is | ||
| 184 | expected to return a JSONRPC result, a plist of (:result | ||
| 185 | RESULT) or signal an error of type `jsonrpc-error'. | ||
| 186 | |||
| 187 | :NOTIFICATION-DISPATCHER (optional), a function of three | ||
| 188 | arguments (CONN METHOD PARAMS) for handling JSONRPC | ||
| 189 | notifications. CONN, METHOD and PARAMS are the same as in | ||
| 190 | :REQUEST-DISPATCHER.") | ||
| 191 | |||
| 192 | ;;; API mandatory | ||
| 193 | (cl-defgeneric jsonrpc-connection-send (conn &key id method params result error) | ||
| 194 | "Send a JSONRPC message to connection CONN. | ||
| 195 | ID, METHOD, PARAMS, RESULT and ERROR. ") | ||
| 196 | |||
| 197 | ;;; API optional | ||
| 198 | (cl-defgeneric jsonrpc-shutdown (conn) | ||
| 199 | "Shutdown the JSONRPC connection CONN.") | ||
| 200 | |||
| 201 | ;;; API optional | ||
| 202 | (cl-defgeneric jsonrpc-running-p (conn) | ||
| 203 | "Tell if the JSONRPC connection CONN is still running.") | ||
| 204 | |||
| 205 | ;;; API optional | ||
| 206 | (cl-defgeneric jsonrpc-connection-ready-p (connection what) | ||
| 207 | "Tell if CONNECTION is ready for WHAT in current buffer. | ||
| 208 | If it isn't, a deferrable `jsonrpc-async-request' will be | ||
| 209 | deferred to the future. By default, all connections are ready | ||
| 210 | for sending requests immediately." | ||
| 211 | (:method (_s _what) ;; by default all connections are ready | ||
| 212 | t)) | ||
| 213 | |||
| 214 | |||
| 215 | ;;; Convenience | ||
| 216 | ;;; | ||
| 217 | (cl-defmacro jsonrpc-lambda (cl-lambda-list &body body) | ||
| 218 | (declare (indent 1) (debug (sexp &rest form))) | ||
| 219 | (let ((e (gensym "jsonrpc-lambda-elem"))) | ||
| 220 | `(lambda (,e) (apply (cl-function (lambda ,cl-lambda-list ,@body)) ,e)))) | ||
| 221 | |||
| 222 | (defun jsonrpc-events-buffer (connection) | ||
| 223 | "Get or create JSONRPC events buffer for CONNECTION." | ||
| 224 | (let* ((probe (jsonrpc--events-buffer connection)) | ||
| 225 | (buffer (or (and (buffer-live-p probe) | ||
| 226 | probe) | ||
| 227 | (let ((buffer (get-buffer-create | ||
| 228 | (format "*%s events*" | ||
| 229 | (jsonrpc-name connection))))) | ||
| 230 | (with-current-buffer buffer | ||
| 231 | (buffer-disable-undo) | ||
| 232 | (read-only-mode t) | ||
| 233 | (setf (jsonrpc--events-buffer connection) buffer)) | ||
| 234 | buffer)))) | ||
| 235 | buffer)) | ||
| 236 | |||
| 237 | (defun jsonrpc-forget-pending-continuations (connection) | ||
| 238 | "Stop waiting for responses from the current JSONRPC CONNECTION." | ||
| 239 | (clrhash (jsonrpc--request-continuations connection))) | ||
| 240 | |||
| 241 | (defun jsonrpc-connection-receive (connection message) | ||
| 242 | "Process MESSAGE just received from CONNECTION. | ||
| 243 | This function will destructure MESSAGE and call the appropriate | ||
| 244 | dispatcher in CONNECTION." | ||
| 245 | (cl-destructuring-bind (&key method id error params result _jsonrpc) | ||
| 246 | message | ||
| 247 | (let (continuations) | ||
| 248 | (jsonrpc--log-event connection message 'server) | ||
| 249 | (setf (jsonrpc-last-error connection) error) | ||
| 250 | (cond | ||
| 251 | (;; A remote request | ||
| 252 | (and method id) | ||
| 253 | (let* ((debug-on-error (and debug-on-error (not (ert-running-test)))) | ||
| 254 | (reply | ||
| 255 | (condition-case-unless-debug _ignore | ||
| 256 | (condition-case oops | ||
| 257 | `(:result ,(funcall (jsonrpc--request-dispatcher connection) | ||
| 258 | connection (intern method) params)) | ||
| 259 | (jsonrpc-error | ||
| 260 | `(:error | ||
| 261 | (:code | ||
| 262 | ,(or (alist-get 'jsonrpc-error-code (cdr oops)) -32603) | ||
| 263 | :message ,(or (alist-get 'jsonrpc-error-message | ||
| 264 | (cdr oops)) | ||
| 265 | "Internal error"))))) | ||
| 266 | (error | ||
| 267 | `(:error (:code -32603 :message "Internal error")))))) | ||
| 268 | (apply #'jsonrpc--reply connection id reply))) | ||
| 269 | (;; A remote notification | ||
| 270 | method | ||
| 271 | (funcall (jsonrpc--notification-dispatcher connection) | ||
| 272 | connection (intern method) params)) | ||
| 273 | (;; A remote response | ||
| 274 | (setq continuations | ||
| 275 | (and id (gethash id (jsonrpc--request-continuations connection)))) | ||
| 276 | (let ((timer (nth 2 continuations))) | ||
| 277 | (when timer (cancel-timer timer))) | ||
| 278 | (remhash id (jsonrpc--request-continuations connection)) | ||
| 279 | (if error (funcall (nth 1 continuations) error) | ||
| 280 | (funcall (nth 0 continuations) result))) | ||
| 281 | (;; An abnormal situation | ||
| 282 | id (jsonrpc--warn "No continuation for id %s" id))) | ||
| 283 | (jsonrpc--call-deferred connection)))) | ||
| 284 | |||
| 285 | |||
| 286 | ;;; Contacting the remote endpoint | ||
| 287 | ;;; | ||
| 288 | (defun jsonrpc-error (&rest args) | ||
| 289 | "Error out with FORMAT and ARGS. | ||
| 290 | If invoked inside a dispatcher function, this function is suitable | ||
| 291 | for replying to the remote endpoint with an error message. | ||
| 292 | |||
| 293 | ARGS can be of the form (FORMAT-STRING . MOREARGS) for replying | ||
| 294 | with a -32603 error code and a message formed by formatting | ||
| 295 | FORMAT-STRING with MOREARGS. | ||
| 296 | |||
| 297 | Alternatively ARGS can be plist representing a JSONRPC error | ||
| 298 | object, using the keywords `:code', `:message' and `:data'." | ||
| 299 | (if (stringp (car args)) | ||
| 300 | (let ((msg | ||
| 301 | (apply #'format-message (car args) (cdr args)))) | ||
| 302 | (signal 'jsonrpc-error | ||
| 303 | `(,msg | ||
| 304 | (jsonrpc-error-code . ,32603) | ||
| 305 | (jsonrpc-error-message . ,msg)))) | ||
| 306 | (cl-destructuring-bind (&key code message data) args | ||
| 307 | (signal 'jsonrpc-error | ||
| 308 | `(,(format "[jsonrpc] error ") | ||
| 309 | (jsonrpc-error-code . ,code) | ||
| 310 | (jsonrpc-error-message . ,message) | ||
| 311 | (jsonrpc-error-data . ,data)))))) | ||
| 312 | |||
| 313 | (cl-defun jsonrpc-async-request (connection | ||
| 314 | method | ||
| 315 | params | ||
| 316 | &rest args | ||
| 317 | &key _success-fn _error-fn | ||
| 318 | _timeout-fn | ||
| 319 | _timeout _deferred) | ||
| 320 | "Make a request to CONNECTION, expecting a reply, return immediately. | ||
| 321 | The JSONRPC request is formed by METHOD, a symbol, and PARAMS a | ||
| 322 | JSON object. | ||
| 323 | |||
| 324 | The caller can expect SUCCESS-FN or ERROR-FN to be called with a | ||
| 325 | JSONRPC `:result' or `:error' object, respectively. If this | ||
| 326 | doesn't happen after TIMEOUT seconds (defaults to | ||
| 327 | `jsonrpc-request-timeout'), the caller can expect TIMEOUT-FN to be | ||
| 328 | called with no arguments. The default values of SUCCESS-FN, | ||
| 329 | ERROR-FN and TIMEOUT-FN simply log the events into | ||
| 330 | `jsonrpc-events-buffer'. | ||
| 331 | |||
| 332 | If DEFERRED is non-nil, maybe defer the request to a future time | ||
| 333 | when the server is thought to be ready according to | ||
| 334 | `jsonrpc-connection-ready-p' (which see). The request might | ||
| 335 | never be sent at all, in case it is overridden in the meantime by | ||
| 336 | a new request with identical DEFERRED and for the same buffer. | ||
| 337 | However, in that situation, the original timeout is kept. | ||
| 338 | |||
| 339 | Returns nil." | ||
| 340 | (apply #'jsonrpc--async-request-1 connection method params args) | ||
| 341 | nil) | ||
| 342 | |||
| 343 | (cl-defun jsonrpc-request (connection method params &key deferred timeout) | ||
| 344 | "Make a request to CONNECTION, wait for a reply. | ||
| 345 | Like `jsonrpc-async-request' for CONNECTION, METHOD and PARAMS, but | ||
| 346 | synchronous, i.e. doesn't exit until anything | ||
| 347 | interesting (success, error or timeout) happens. Furthermore, | ||
| 348 | only exit locally (and return the JSONRPC result object) if the | ||
| 349 | request is successful, otherwise exit non-locally with an error | ||
| 350 | of type `jsonrpc-error'. | ||
| 351 | |||
| 352 | DEFERRED is passed to `jsonrpc-async-request', which see." | ||
| 353 | (let* ((tag (cl-gensym "jsonrpc-request-catch-tag")) id-and-timer | ||
| 354 | (retval | ||
| 355 | (unwind-protect ; protect against user-quit, for example | ||
| 356 | (catch tag | ||
| 357 | (setq | ||
| 358 | id-and-timer | ||
| 359 | (jsonrpc--async-request-1 | ||
| 360 | connection method params | ||
| 361 | :success-fn (lambda (result) (throw tag `(done ,result))) | ||
| 362 | :error-fn | ||
| 363 | (jsonrpc-lambda | ||
| 364 | (&key code message data) | ||
| 365 | (throw tag `(error (jsonrpc-error-code . ,code) | ||
| 366 | (jsonrpc-error-message . ,message) | ||
| 367 | (jsonrpc-error-data . ,data)))) | ||
| 368 | :timeout-fn | ||
| 369 | (lambda () | ||
| 370 | (throw tag '(error (jsonrpc-error-message . "Timed out")))) | ||
| 371 | :deferred deferred | ||
| 372 | :timeout timeout)) | ||
| 373 | (while t (accept-process-output nil 30))) | ||
| 374 | (pcase-let* ((`(,id ,timer) id-and-timer)) | ||
| 375 | (remhash id (jsonrpc--request-continuations connection)) | ||
| 376 | (remhash (list deferred (current-buffer)) | ||
| 377 | (jsonrpc--deferred-actions connection)) | ||
| 378 | (when timer (cancel-timer timer)))))) | ||
| 379 | (when (eq 'error (car retval)) | ||
| 380 | (signal 'jsonrpc-error | ||
| 381 | (cons | ||
| 382 | (format "request id=%s failed:" (car id-and-timer)) | ||
| 383 | (cdr retval)))) | ||
| 384 | (cadr retval))) | ||
| 385 | |||
| 386 | (cl-defun jsonrpc-notify (connection method params) | ||
| 387 | "Notify CONNECTION of something, don't expect a reply." | ||
| 388 | (jsonrpc-connection-send connection | ||
| 389 | :method method | ||
| 390 | :params params)) | ||
| 391 | |||
| 392 | (defconst jrpc-default-request-timeout 10 | ||
| 393 | "Time in seconds before timing out a JSONRPC request.") | ||
| 394 | |||
| 395 | |||
| 396 | ;;; Specfic to `jsonrpc-process-connection' | ||
| 397 | ;;; | ||
| 398 | ;;;###autoload | ||
| 399 | (defclass jsonrpc-process-connection (jsonrpc-connection) | ||
| 400 | ((-process | ||
| 401 | :initarg :process :accessor jsonrpc--process | ||
| 402 | :documentation "Process object wrapped by the this connection.") | ||
| 403 | (-expected-bytes | ||
| 404 | :accessor jsonrpc--expected-bytes | ||
| 405 | :documentation "How many bytes declared by server") | ||
| 406 | (-on-shutdown | ||
| 407 | :accessor jsonrpc--on-shutdown | ||
| 408 | :initform #'ignore | ||
| 409 | :initarg :on-shutdown | ||
| 410 | :documentation "Function run when the process dies.")) | ||
| 411 | :documentation "A JSONRPC connection over an Emacs process. | ||
| 412 | The following initargs are accepted: | ||
| 413 | |||
| 414 | :PROCESS (mandatory), a live running Emacs process object or a | ||
| 415 | function of no arguments producing one such object. The process | ||
| 416 | represents either a pipe connection to locally running process or | ||
| 417 | a stream connection to a network host. The remote endpoint is | ||
| 418 | expected to understand JSONRPC messages with basic HTTP-style | ||
| 419 | enveloping headers such as \"Content-Length:\". | ||
| 420 | |||
| 421 | :ON-SHUTDOWN (optional), a function of one argument, the | ||
| 422 | connection object, called when the process dies .") | ||
| 423 | |||
| 424 | (cl-defmethod initialize-instance ((conn jsonrpc-process-connection) slots) | ||
| 425 | (cl-call-next-method) | ||
| 426 | (let* ((proc (plist-get slots :process)) | ||
| 427 | (proc (if (functionp proc) (funcall proc) proc)) | ||
| 428 | (buffer (get-buffer-create (format "*%s output*" (process-name proc)))) | ||
| 429 | (stderr (get-buffer-create (format "*%s stderr*" (process-name proc))))) | ||
| 430 | (setf (jsonrpc--process conn) proc) | ||
| 431 | (set-process-buffer proc buffer) | ||
| 432 | (process-put proc 'jsonrpc-stderr stderr) | ||
| 433 | (set-process-filter proc #'jsonrpc--process-filter) | ||
| 434 | (set-process-sentinel proc #'jsonrpc--process-sentinel) | ||
| 435 | (with-current-buffer (process-buffer proc) | ||
| 436 | (set-marker (process-mark proc) (point-min)) | ||
| 437 | (let ((inhibit-read-only t)) (erase-buffer) (read-only-mode t) proc)) | ||
| 438 | (process-put proc 'jsonrpc-connection conn))) | ||
| 439 | |||
| 440 | (cl-defmethod jsonrpc-connection-send ((connection jsonrpc-process-connection) | ||
| 441 | &rest args | ||
| 442 | &key | ||
| 443 | _id | ||
| 444 | method | ||
| 445 | _params | ||
| 446 | _result | ||
| 447 | _error | ||
| 448 | _partial) | ||
| 449 | "Send MESSAGE, a JSON object, to CONNECTION." | ||
| 450 | (when method | ||
| 451 | (plist-put args :method | ||
| 452 | (cond ((keywordp method) (substring (symbol-name method) 1)) | ||
| 453 | ((and method (symbolp method)) (symbol-name method))))) | ||
| 454 | (let* ( (message `(:jsonrpc "2.0" ,@args)) | ||
| 455 | (json (jsonrpc--json-encode message)) | ||
| 456 | (headers | ||
| 457 | `(("Content-Length" . ,(format "%d" (string-bytes json))) | ||
| 458 | ;; ("Content-Type" . "application/vscode-jsonrpc; charset=utf-8") | ||
| 459 | ))) | ||
| 460 | (process-send-string | ||
| 461 | (jsonrpc--process connection) | ||
| 462 | (cl-loop for (header . value) in headers | ||
| 463 | concat (concat header ": " value "\r\n") into header-section | ||
| 464 | finally return (format "%s\r\n%s" header-section json))) | ||
| 465 | (jsonrpc--log-event connection message 'client))) | ||
| 466 | |||
| 467 | (defun jsonrpc-process-type (conn) | ||
| 468 | "Return the `process-type' of JSONRPC connection CONN." | ||
| 469 | (process-type (jsonrpc--process conn))) | ||
| 470 | |||
| 471 | (cl-defmethod jsonrpc-running-p ((conn jsonrpc-process-connection)) | ||
| 472 | "Return non-nil if JSONRPC connection CONN is running." | ||
| 473 | (process-live-p (jsonrpc--process conn))) | ||
| 474 | |||
| 475 | (cl-defmethod jsonrpc-shutdown ((conn jsonrpc-process-connection)) | ||
| 476 | "Shutdown the JSONRPC connection CONN." | ||
| 477 | (cl-loop | ||
| 478 | with proc = (jsonrpc--process conn) | ||
| 479 | do | ||
| 480 | (delete-process proc) | ||
| 481 | (accept-process-output nil 0.1) | ||
| 482 | while (not (process-get proc 'jsonrpc-sentinel-done)) | ||
| 483 | do (jsonrpc--warn | ||
| 484 | "Sentinel for %s still hasn't run, deleting it!" proc))) | ||
| 485 | |||
| 486 | (defun jsonrpc-stderr-buffer (conn) | ||
| 487 | "Get CONN's standard error buffer, if any." | ||
| 488 | (process-get (jsonrpc--process conn) 'jsonrpc-stderr)) | ||
| 489 | |||
| 490 | |||
| 491 | ;;; Private stuff | ||
| 492 | ;;; | ||
| 493 | (define-error 'jsonrpc-error "jsonrpc-error") | ||
| 494 | |||
| 495 | (defun jsonrpc--json-read () | ||
| 496 | "Read JSON object in buffer, move point to end of buffer." | ||
| 497 | ;; TODO: I guess we can make these macros if/when jsonrpc.el | ||
| 498 | ;; goes into Emacs core. | ||
| 499 | (cond ((fboundp 'json-parse-buffer) (json-parse-buffer | ||
| 500 | :object-type 'plist | ||
| 501 | :null-object nil | ||
| 502 | :false-object :json-false)) | ||
| 503 | (t (let ((json-object-type 'plist)) | ||
| 504 | (json-read))))) | ||
| 505 | |||
| 506 | (defun jsonrpc--json-encode (object) | ||
| 507 | "Encode OBJECT into a JSON string." | ||
| 508 | (cond ((fboundp 'json-serialize) (json-serialize | ||
| 509 | object | ||
| 510 | :false-object :json-false | ||
| 511 | :null-object nil)) | ||
| 512 | (t (let ((json-false :json-false) | ||
| 513 | (json-null nil)) | ||
| 514 | (json-encode object))))) | ||
| 515 | |||
| 516 | (cl-defun jsonrpc--reply (connection id &key (result nil result-supplied-p) error) | ||
| 517 | "Reply to CONNECTION's request ID with RESULT or ERROR." | ||
| 518 | (jsonrpc-connection-send connection :id id :result result :error error)) | ||
| 519 | |||
| 520 | (defun jsonrpc--call-deferred (connection) | ||
| 521 | "Call CONNECTION's deferred actions, who may again defer themselves." | ||
| 522 | (when-let ((actions (hash-table-values (jsonrpc--deferred-actions connection)))) | ||
| 523 | (jsonrpc--debug connection `(:maybe-run-deferred ,(mapcar #'caddr actions))) | ||
| 524 | (mapc #'funcall (mapcar #'car actions)))) | ||
| 525 | |||
| 526 | (defun jsonrpc--process-sentinel (proc change) | ||
| 527 | "Called when PROC undergoes CHANGE." | ||
| 528 | (let ((connection (process-get proc 'jsonrpc-connection))) | ||
| 529 | (jsonrpc--debug connection `(:message "Connection state changed" :change ,change)) | ||
| 530 | (when (not (process-live-p proc)) | ||
| 531 | (with-current-buffer (jsonrpc-events-buffer connection) | ||
| 532 | (let ((inhibit-read-only t)) | ||
| 533 | (insert "\n----------b---y---e---b---y---e----------\n"))) | ||
| 534 | ;; Cancel outstanding timers | ||
| 535 | (maphash (lambda (_id triplet) | ||
| 536 | (pcase-let ((`(,_success ,_error ,timeout) triplet)) | ||
| 537 | (when timeout (cancel-timer timeout)))) | ||
| 538 | (jsonrpc--request-continuations connection)) | ||
| 539 | (unwind-protect | ||
| 540 | ;; Call all outstanding error handlers | ||
| 541 | (maphash (lambda (_id triplet) | ||
| 542 | (pcase-let ((`(,_success ,error ,_timeout) triplet)) | ||
| 543 | (funcall error `(:code -1 :message "Server died")))) | ||
| 544 | (jsonrpc--request-continuations connection)) | ||
| 545 | (jsonrpc--message "Server exited with status %s" (process-exit-status proc)) | ||
| 546 | (process-put proc 'jsonrpc-sentinel-done t) | ||
| 547 | (delete-process proc) | ||
| 548 | (funcall (jsonrpc--on-shutdown connection) connection))))) | ||
| 549 | |||
| 550 | (defun jsonrpc--process-filter (proc string) | ||
| 551 | "Called when new data STRING has arrived for PROC." | ||
| 552 | (when (buffer-live-p (process-buffer proc)) | ||
| 553 | (with-current-buffer (process-buffer proc) | ||
| 554 | (let* ((inhibit-read-only t) | ||
| 555 | (connection (process-get proc 'jsonrpc-connection)) | ||
| 556 | (expected-bytes (jsonrpc--expected-bytes connection))) | ||
| 557 | ;; Insert the text, advancing the process marker. | ||
| 558 | ;; | ||
| 559 | (save-excursion | ||
| 560 | (goto-char (process-mark proc)) | ||
| 561 | (insert string) | ||
| 562 | (set-marker (process-mark proc) (point))) | ||
| 563 | ;; Loop (more than one message might have arrived) | ||
| 564 | ;; | ||
| 565 | (unwind-protect | ||
| 566 | (let (done) | ||
| 567 | (while (not done) | ||
| 568 | (cond | ||
| 569 | ((not expected-bytes) | ||
| 570 | ;; Starting a new message | ||
| 571 | ;; | ||
| 572 | (setq expected-bytes | ||
| 573 | (and (search-forward-regexp | ||
| 574 | "\\(?:.*: .*\r\n\\)*Content-Length: \ | ||
| 575 | *\\([[:digit:]]+\\)\r\n\\(?:.*: .*\r\n\\)*\r\n" | ||
| 576 | (+ (point) 100) | ||
| 577 | t) | ||
| 578 | (string-to-number (match-string 1)))) | ||
| 579 | (unless expected-bytes | ||
| 580 | (setq done :waiting-for-new-message))) | ||
| 581 | (t | ||
| 582 | ;; Attempt to complete a message body | ||
| 583 | ;; | ||
| 584 | (let ((available-bytes (- (position-bytes (process-mark proc)) | ||
| 585 | (position-bytes (point))))) | ||
| 586 | (cond | ||
| 587 | ((>= available-bytes | ||
| 588 | expected-bytes) | ||
| 589 | (let* ((message-end (byte-to-position | ||
| 590 | (+ (position-bytes (point)) | ||
| 591 | expected-bytes)))) | ||
| 592 | (unwind-protect | ||
| 593 | (save-restriction | ||
| 594 | (narrow-to-region (point) message-end) | ||
| 595 | (let* ((json-message | ||
| 596 | (condition-case-unless-debug oops | ||
| 597 | (jsonrpc--json-read) | ||
| 598 | (error | ||
| 599 | (jsonrpc--warn "Invalid JSON: %s %s" | ||
| 600 | (cdr oops) (buffer-string)) | ||
| 601 | nil)))) | ||
| 602 | (when json-message | ||
| 603 | ;; Process content in another | ||
| 604 | ;; buffer, shielding proc buffer from | ||
| 605 | ;; tamper | ||
| 606 | (with-temp-buffer | ||
| 607 | (jsonrpc-connection-receive connection | ||
| 608 | json-message))))) | ||
| 609 | (goto-char message-end) | ||
| 610 | (delete-region (point-min) (point)) | ||
| 611 | (setq expected-bytes nil)))) | ||
| 612 | (t | ||
| 613 | ;; Message is still incomplete | ||
| 614 | ;; | ||
| 615 | (setq done :waiting-for-more-bytes-in-this-message)))))))) | ||
| 616 | ;; Saved parsing state for next visit to this filter | ||
| 617 | ;; | ||
| 618 | (setf (jsonrpc--expected-bytes connection) expected-bytes)))))) | ||
| 619 | |||
| 620 | (cl-defun jsonrpc--async-request-1 (connection | ||
| 621 | method | ||
| 622 | params | ||
| 623 | &rest args | ||
| 624 | &key success-fn error-fn timeout-fn | ||
| 625 | (timeout jrpc-default-request-timeout) | ||
| 626 | (deferred nil)) | ||
| 627 | "Does actual work for `jsonrpc-async-request'. | ||
| 628 | |||
| 629 | Return a list (ID TIMER). ID is the new request's ID, or nil if | ||
| 630 | the request was deferred. TIMER is a timer object set (or nil, if | ||
| 631 | TIMEOUT is nil)." | ||
| 632 | (pcase-let* ((buf (current-buffer)) (point (point)) | ||
| 633 | (`(,_ ,timer ,old-id) | ||
| 634 | (and deferred (gethash (list deferred buf) | ||
| 635 | (jsonrpc--deferred-actions connection)))) | ||
| 636 | (id (or old-id (cl-incf (jsonrpc--next-request-id connection)))) | ||
| 637 | (make-timer | ||
| 638 | (lambda ( ) | ||
| 639 | (when timeout | ||
| 640 | (run-with-timer | ||
| 641 | timeout nil | ||
| 642 | (lambda () | ||
| 643 | (remhash id (jsonrpc--request-continuations connection)) | ||
| 644 | (remhash (list deferred buf) | ||
| 645 | (jsonrpc--deferred-actions connection)) | ||
| 646 | (if timeout-fn (funcall timeout-fn) | ||
| 647 | (jsonrpc--debug | ||
| 648 | connection `(:timed-out ,method :id ,id | ||
| 649 | :params ,params))))))))) | ||
| 650 | (when deferred | ||
| 651 | (if (jsonrpc-connection-ready-p connection deferred) | ||
| 652 | ;; Server is ready, we jump below and send it immediately. | ||
| 653 | (remhash (list deferred buf) (jsonrpc--deferred-actions connection)) | ||
| 654 | ;; Otherwise, save in `eglot--deferred-actions' and exit non-locally | ||
| 655 | (unless old-id | ||
| 656 | (jsonrpc--debug connection `(:deferring ,method :id ,id :params | ||
| 657 | ,params))) | ||
| 658 | (puthash (list deferred buf) | ||
| 659 | (list (lambda () | ||
| 660 | (when (buffer-live-p buf) | ||
| 661 | (with-current-buffer buf | ||
| 662 | (save-excursion (goto-char point) | ||
| 663 | (apply #'jsonrpc-async-request | ||
| 664 | connection | ||
| 665 | method params args))))) | ||
| 666 | (or timer (setq timer (funcall make-timer))) id) | ||
| 667 | (jsonrpc--deferred-actions connection)) | ||
| 668 | (cl-return-from jsonrpc--async-request-1 (list id timer)))) | ||
| 669 | ;; Really send it | ||
| 670 | ;; | ||
| 671 | (jsonrpc-connection-send connection | ||
| 672 | :id id | ||
| 673 | :method method | ||
| 674 | :params params) | ||
| 675 | (puthash id | ||
| 676 | (list (or success-fn | ||
| 677 | (jsonrpc-lambda (&rest _ignored) | ||
| 678 | (jsonrpc--debug | ||
| 679 | connection (list :message "success ignored" | ||
| 680 | :id id)))) | ||
| 681 | (or error-fn | ||
| 682 | (jsonrpc-lambda (&key code message &allow-other-keys) | ||
| 683 | (jsonrpc--debug | ||
| 684 | connection (list | ||
| 685 | :message | ||
| 686 | (format "error ignored, status set (%s)" | ||
| 687 | message) | ||
| 688 | :id id :error code)))) | ||
| 689 | (setq timer (funcall make-timer))) | ||
| 690 | (jsonrpc--request-continuations connection)) | ||
| 691 | (list id timer))) | ||
| 692 | |||
| 693 | (defun jsonrpc--message (format &rest args) | ||
| 694 | "Message out with FORMAT with ARGS." | ||
| 695 | (message "[jsonrpc] %s" (apply #'format format args))) | ||
| 696 | |||
| 697 | (defun jsonrpc--debug (server format &rest args) | ||
| 698 | "Debug message for SERVER with FORMAT and ARGS." | ||
| 699 | (jsonrpc--log-event | ||
| 700 | server (if (stringp format)`(:message ,(format format args)) format))) | ||
| 701 | |||
| 702 | (defun jsonrpc--warn (format &rest args) | ||
| 703 | "Warning message with FORMAT and ARGS." | ||
| 704 | (apply #'jsonrpc--message (concat "(warning) " format) args) | ||
| 705 | (let ((warning-minimum-level :error)) | ||
| 706 | (display-warning 'jsonrpc | ||
| 707 | (apply #'format format args) | ||
| 708 | :warning))) | ||
| 709 | |||
| 710 | (defun jsonrpc--log-event (connection message &optional type) | ||
| 711 | "Log a JSONRPC-related event. | ||
| 712 | CONNECTION is the current connection. MESSAGE is a JSON-like | ||
| 713 | plist. TYPE is a symbol saying if this is a client or server | ||
| 714 | originated." | ||
| 715 | (with-current-buffer (jsonrpc-events-buffer connection) | ||
| 716 | (cl-destructuring-bind (&key method id error &allow-other-keys) message | ||
| 717 | (let* ((inhibit-read-only t) | ||
| 718 | (subtype (cond ((and method id) 'request) | ||
| 719 | (method 'notification) | ||
| 720 | (id 'reply) | ||
| 721 | (t 'message))) | ||
| 722 | (type | ||
| 723 | (concat (format "%s" (or type 'internal)) | ||
| 724 | (if type | ||
| 725 | (format "-%s" subtype))))) | ||
| 726 | (goto-char (point-max)) | ||
| 727 | (let ((msg (format "%s%s%s %s:\n%s\n" | ||
| 728 | type | ||
| 729 | (if id (format " (id:%s)" id) "") | ||
| 730 | (if error " ERROR" "") | ||
| 731 | (current-time-string) | ||
| 732 | (pp-to-string message)))) | ||
| 733 | (when error | ||
| 734 | (setq msg (propertize msg 'face 'error))) | ||
| 735 | (insert-before-markers msg)))))) | ||
| 736 | |||
| 737 | (provide 'jsonrpc) | ||
| 738 | ;;; jsonrpc.el ends here | ||
diff --git a/test/lisp/jsonrpc-tests.el b/test/lisp/jsonrpc-tests.el new file mode 100644 index 00000000000..bfdb513ada4 --- /dev/null +++ b/test/lisp/jsonrpc-tests.el | |||
| @@ -0,0 +1,242 @@ | |||
| 1 | ;;; jsonrpc-tests.el --- tests for jsonrpc.el -*- lexical-binding: t; -*- | ||
| 2 | |||
| 3 | ;; Copyright (C) 2018 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | ;; Author: João Távora <joaotavora@gmail.com> | ||
| 6 | ;; Maintainer: João Távora <joaotavora@gmail.com> | ||
| 7 | ;; Keywords: tests | ||
| 8 | |||
| 9 | ;; This program is free software; you can redistribute it and/or modify | ||
| 10 | ;; it under the terms of the GNU General Public License as published by | ||
| 11 | ;; the Free Software Foundation, either version 3 of the License, or | ||
| 12 | ;; (at your option) any later version. | ||
| 13 | |||
| 14 | ;; This program is distributed in the hope that it will be useful, | ||
| 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 17 | ;; GNU General Public License for more details. | ||
| 18 | |||
| 19 | ;; You should have received a copy of the GNU General Public License | ||
| 20 | ;; along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
| 21 | |||
| 22 | ;;; Commentary: | ||
| 23 | |||
| 24 | ;; About "deferred" tests, `jsonrpc--test-client' has a flag that we | ||
| 25 | ;; test this flag in the this `jsonrpc-connection-ready-p' API method. | ||
| 26 | ;; It holds any `jsonrpc-request's and `jsonrpc-async-request's | ||
| 27 | ;; explicitly passed `:deferred'. After clearing the flag, the held | ||
| 28 | ;; requests are actually sent to the server in the next opportunity | ||
| 29 | ;; (when receiving or sending something to the server). | ||
| 30 | |||
| 31 | ;;; Code: | ||
| 32 | |||
| 33 | (require 'ert) | ||
| 34 | (require 'jsonrpc) | ||
| 35 | (require 'eieio) | ||
| 36 | |||
| 37 | (defclass jsonrpc--test-endpoint (jsonrpc-process-connection) | ||
| 38 | ((scp :accessor jsonrpc--shutdown-complete-p))) | ||
| 39 | |||
| 40 | (defclass jsonrpc--test-client (jsonrpc--test-endpoint) | ||
| 41 | ((hold-deferred :initform t :accessor jsonrpc--hold-deferred))) | ||
| 42 | |||
| 43 | (cl-defmacro jsonrpc--with-emacsrpc-fixture ((endpoint-sym) &body body) | ||
| 44 | (declare (indent 1) (debug t)) | ||
| 45 | (let ((server (gensym "server-")) (listen-server (gensym "listen-server-"))) | ||
| 46 | `(let* (,server | ||
| 47 | (,listen-server | ||
| 48 | (make-network-process | ||
| 49 | :name "Emacs RPC server" :server t :host "localhost" | ||
| 50 | :service 0 | ||
| 51 | :log (lambda (_server client _message) | ||
| 52 | (setq ,server | ||
| 53 | (make-instance | ||
| 54 | 'jsonrpc--test-endpoint | ||
| 55 | :name (process-name client) | ||
| 56 | :process client | ||
| 57 | :request-dispatcher | ||
| 58 | (lambda (_endpoint method params) | ||
| 59 | (unless (memq method '(+ - * / vconcat append | ||
| 60 | sit-for ignore)) | ||
| 61 | (signal 'jsonrpc-error | ||
| 62 | `((jsonrpc-error-message | ||
| 63 | . "Sorry, this isn't allowed") | ||
| 64 | (jsonrpc-error-code . -32601)))) | ||
| 65 | (apply method (append params nil))) | ||
| 66 | :on-shutdown | ||
| 67 | (lambda (conn) | ||
| 68 | (setf (jsonrpc--shutdown-complete-p conn) t))))))) | ||
| 69 | (,endpoint-sym (make-instance | ||
| 70 | 'jsonrpc--test-client | ||
| 71 | "Emacs RPC client" | ||
| 72 | :process | ||
| 73 | (open-network-stream "JSONRPC test tcp endpoint" | ||
| 74 | nil "localhost" | ||
| 75 | (process-contact ,listen-server | ||
| 76 | :service)) | ||
| 77 | :on-shutdown | ||
| 78 | (lambda (conn) | ||
| 79 | (setf (jsonrpc--shutdown-complete-p conn) t))))) | ||
| 80 | (unwind-protect | ||
| 81 | (progn | ||
| 82 | (cl-assert ,endpoint-sym) | ||
| 83 | ,@body | ||
| 84 | (kill-buffer (jsonrpc--events-buffer ,endpoint-sym)) | ||
| 85 | (when ,server | ||
| 86 | (kill-buffer (jsonrpc--events-buffer ,server)))) | ||
| 87 | (unwind-protect | ||
| 88 | (jsonrpc-shutdown ,endpoint-sym) | ||
| 89 | (unwind-protect | ||
| 90 | (jsonrpc-shutdown ,server) | ||
| 91 | (cl-loop do (delete-process ,listen-server) | ||
| 92 | while (progn (accept-process-output nil 0.1) | ||
| 93 | (process-live-p ,listen-server)) | ||
| 94 | do (jsonrpc--message | ||
| 95 | "test listen-server is still running, waiting")))))))) | ||
| 96 | |||
| 97 | (ert-deftest returns-3 () | ||
| 98 | "A basic test for adding two numbers in our test RPC." | ||
| 99 | (jsonrpc--with-emacsrpc-fixture (conn) | ||
| 100 | (should (= 3 (jsonrpc-request conn '+ [1 2]))))) | ||
| 101 | |||
| 102 | (ert-deftest errors-with--32601 () | ||
| 103 | "Errors with -32601" | ||
| 104 | (jsonrpc--with-emacsrpc-fixture (conn) | ||
| 105 | (condition-case err | ||
| 106 | (progn | ||
| 107 | (jsonrpc-request conn 'delete-directory "~/tmp") | ||
| 108 | (ert-fail "A `jsonrpc-error' should have been signalled!")) | ||
| 109 | (jsonrpc-error | ||
| 110 | (should (= -32601 (cdr (assoc 'jsonrpc-error-code (cdr err))))))))) | ||
| 111 | |||
| 112 | (ert-deftest signals-an--32603-JSONRPC-error () | ||
| 113 | "Signals an -32603 JSONRPC error." | ||
| 114 | (jsonrpc--with-emacsrpc-fixture (conn) | ||
| 115 | (condition-case err | ||
| 116 | (progn | ||
| 117 | (jsonrpc-request conn '+ ["a" 2]) | ||
| 118 | (ert-fail "A `jsonrpc-error' should have been signalled!")) | ||
| 119 | (jsonrpc-error | ||
| 120 | (should (= -32603 (cdr (assoc 'jsonrpc-error-code (cdr err))))))))) | ||
| 121 | |||
| 122 | (ert-deftest times-out () | ||
| 123 | "Request for 3-sec sit-for with 1-sec timeout times out." | ||
| 124 | (jsonrpc--with-emacsrpc-fixture (conn) | ||
| 125 | (should-error | ||
| 126 | (jsonrpc-request conn 'sit-for [3] :timeout 1)))) | ||
| 127 | |||
| 128 | (ert-deftest doesnt-time-out () | ||
| 129 | :tags '(:expensive-test) | ||
| 130 | "Request for 1-sec sit-for with 2-sec timeout succeeds." | ||
| 131 | (jsonrpc--with-emacsrpc-fixture (conn) | ||
| 132 | (jsonrpc-request conn 'sit-for [1] :timeout 2))) | ||
| 133 | |||
| 134 | (ert-deftest stretching-it-but-works () | ||
| 135 | "Vector of numbers or vector of vector of numbers are serialized." | ||
| 136 | (jsonrpc--with-emacsrpc-fixture (conn) | ||
| 137 | ;; (vconcat [1 2 3] [3 4 5]) => [1 2 3 3 4 5] which can be | ||
| 138 | ;; serialized. | ||
| 139 | (should (equal | ||
| 140 | [1 2 3 3 4 5] | ||
| 141 | (jsonrpc-request conn 'vconcat [[1 2 3] [3 4 5]]))))) | ||
| 142 | |||
| 143 | (ert-deftest json-el-cant-serialize-this () | ||
| 144 | "Can't serialize a response that is half-vector/half-list." | ||
| 145 | (jsonrpc--with-emacsrpc-fixture (conn) | ||
| 146 | (should-error | ||
| 147 | ;; (append [1 2 3] [3 4 5]) => (1 2 3 . [3 4 5]), which can't be | ||
| 148 | ;; serialized | ||
| 149 | (jsonrpc-request conn 'append [[1 2 3] [3 4 5]])))) | ||
| 150 | |||
| 151 | (cl-defmethod jsonrpc-connection-ready-p | ||
| 152 | ((conn jsonrpc--test-client) what) | ||
| 153 | (and (cl-call-next-method) | ||
| 154 | (or (not (string-match "deferred" what)) | ||
| 155 | (not (jsonrpc--hold-deferred conn))))) | ||
| 156 | |||
| 157 | (ert-deftest deferred-action-toolate () | ||
| 158 | :tags '(:expensive-test) | ||
| 159 | "Deferred request fails because noone clears the flag." | ||
| 160 | (jsonrpc--with-emacsrpc-fixture (conn) | ||
| 161 | (should-error | ||
| 162 | (jsonrpc-request conn '+ [1 2] | ||
| 163 | :deferred "deferred-testing" :timeout 0.5) | ||
| 164 | :type 'jsonrpc-error) | ||
| 165 | (should | ||
| 166 | (= 3 (jsonrpc-request conn '+ [1 2] | ||
| 167 | :timeout 0.5))))) | ||
| 168 | |||
| 169 | (ert-deftest deferred-action-intime () | ||
| 170 | :tags '(:expensive-test) | ||
| 171 | "Deferred request barely makes it after event clears a flag." | ||
| 172 | ;; Send an async request, which returns immediately. However the | ||
| 173 | ;; success fun which sets the flag only runs after some time. | ||
| 174 | (jsonrpc--with-emacsrpc-fixture (conn) | ||
| 175 | (jsonrpc-async-request conn | ||
| 176 | 'sit-for [0.5] | ||
| 177 | :success-fn | ||
| 178 | (lambda (_result) | ||
| 179 | (setf (jsonrpc--hold-deferred conn) nil))) | ||
| 180 | ;; Now wait for an answer to this request, which should be sent as | ||
| 181 | ;; soon as the previous one is answered. | ||
| 182 | (should | ||
| 183 | (= 3 (jsonrpc-request conn '+ [1 2] | ||
| 184 | :deferred "deferred" | ||
| 185 | :timeout 1))))) | ||
| 186 | |||
| 187 | (ert-deftest deferred-action-complex-tests () | ||
| 188 | :tags '(:expensive-test) | ||
| 189 | "Test a more complex situation with deferred requests." | ||
| 190 | (jsonrpc--with-emacsrpc-fixture (conn) | ||
| 191 | (let (n-deferred-1 | ||
| 192 | n-deferred-2 | ||
| 193 | second-deferred-went-through-p) | ||
| 194 | ;; This returns immediately | ||
| 195 | (jsonrpc-async-request | ||
| 196 | conn | ||
| 197 | 'sit-for [0.1] | ||
| 198 | :success-fn | ||
| 199 | (lambda (_result) | ||
| 200 | ;; this only gets runs after the "first deferred" is stashed. | ||
| 201 | (setq n-deferred-1 | ||
| 202 | (hash-table-count (jsonrpc--deferred-actions conn))))) | ||
| 203 | (should-error | ||
| 204 | ;; This stashes the request and waits. It will error because | ||
| 205 | ;; no-one clears the "hold deferred" flag. | ||
| 206 | (jsonrpc-request conn 'ignore ["first deferred"] | ||
| 207 | :deferred "first deferred" | ||
| 208 | :timeout 0.5) | ||
| 209 | :type 'jsonrpc-error) | ||
| 210 | ;; The error means the deferred actions stash is now empty | ||
| 211 | (should (zerop (hash-table-count (jsonrpc--deferred-actions conn)))) | ||
| 212 | ;; Again, this returns immediately. | ||
| 213 | (jsonrpc-async-request | ||
| 214 | conn | ||
| 215 | 'sit-for [0.1] | ||
| 216 | :success-fn | ||
| 217 | (lambda (_result) | ||
| 218 | ;; This gets run while "third deferred" below is waiting for | ||
| 219 | ;; a reply. Notice that we clear the flag in time here. | ||
| 220 | (setq n-deferred-2 (hash-table-count (jsonrpc--deferred-actions conn))) | ||
| 221 | (setf (jsonrpc--hold-deferred conn) nil))) | ||
| 222 | ;; This again stashes a request and returns immediately. | ||
| 223 | (jsonrpc-async-request conn 'ignore ["second deferred"] | ||
| 224 | :deferred "second deferred" | ||
| 225 | :timeout 1 | ||
| 226 | :success-fn | ||
| 227 | (lambda (_result) | ||
| 228 | (setq second-deferred-went-through-p t))) | ||
| 229 | ;; And this also stashes a request, but waits. Eventually the | ||
| 230 | ;; flag is cleared in time and both requests go through. | ||
| 231 | (jsonrpc-request conn 'ignore ["third deferred"] | ||
| 232 | :deferred "third deferred" | ||
| 233 | :timeout 1) | ||
| 234 | (should second-deferred-went-through-p) | ||
| 235 | (should (eq 1 n-deferred-1)) | ||
| 236 | (should (eq 2 n-deferred-2)) | ||
| 237 | (should (eq 0 (hash-table-count (jsonrpc--deferred-actions conn))))))) | ||
| 238 | |||
| 239 | |||
| 240 | |||
| 241 | (provide 'jsonrpc-tests) | ||
| 242 | ;;; jsonrpc-tests.el ends here | ||