Revert commits making ZMQ optional

These commits were pre-maturely pushed to master.

This reverts commits:

    - 3322ce7b31
    - ee8b5180e5
    - 8883a6631a
    - ae5dad9796
    - 0e202a02fa
    - 5725215268
    - 3b3e358933
    - a5f8d991b0
    - 1a739feec7
    - 4115ff5f73
This commit is contained in:
Nathaniel Nicandro 2020-04-02 10:48:44 -05:00
parent 3322ce7b31
commit a4e0616ed4
9 changed files with 552 additions and 756 deletions

View file

@ -45,7 +45,7 @@ dev: cask
$(CASK) --dev update
.PHONY: test
test:
test: zmq
$(CASK) exec ert-runner --script $(TAGS) $(PATTERN)
.PHONY: clean
@ -62,5 +62,5 @@ widgets:
make -C js
.PHONY: compile
compile:
compile: zmq
$(CASK) build

View file

@ -1194,40 +1194,7 @@ interact with an [[https://github.com/jupyter-widgets/ipyleaflet][ipyleaflet]] m
** TODO =jupyter-ioloop=
** TODO =jupyter-channel-ioloop=
** TODO =jupyter-zmq-channel-ioloop=
** =jupyter-comm-layer=
The =jupyter-comm-layer= class abstracts the sending/receiving of messages
between a kernel and a =jupyter-kernel-client= so that a =jupyter-kernel-client=
does not need to be concerned with the underlying way that messages are
communicated. It also allows one to build up layers of event handling in which
subsets of events may be handled on one layer and the remaining events handled
by the remaining layers.
A =jupyter-comm-layer= deals in events which are lists with an identifying
element in the head position. Jupyter messages are then just one kind of
event. When events are received, the =jupyter-event-handler= method is called on
all registered event handlers, passing the event to the handlers. Events can
be sent to the other end of a =jupyter-comm-layer= using the =jupyter-send= method.
Objects interested in receiving events need to define a =jupyter-event-handler=
method for the particular events they would like to receive. Then, to start
receiving they should call =jupyter-comm-add-handler= and to
stop =jupyter-comm-remove-handler=. Here is an example =jupyter-event-handler=
#+begin_src elisp
(cl-defmethod jupyter-event-handler ((client jupyter-kernel-client)
(event (head message)))
(cl-destructuring-bind (_ channel _idents . msg) event
(jupyter-handle-message client channel msg)))
#+end_src
The lifetime of a =jupyter-comm-layer= is controlled by the
methods =jupyter-comm-start= and =jupyter-comm-start=. To check if
a =jupyter-comm-layer= is able to send/receive events, use =jupyter-comm-alive-p=.
Initialization of a =jupyter-comm-layer=, e.g. to ensure the right Jupyter
session ID/key are used, is done by the optional =jupyter-comm-initialize=
method.
** TODO =jupyter-comm-layer=
** Callbacks and hooks
:PROPERTIES:
:ID: 0E7CA280-8D14-4994-A3C7-C3B7204AC9D2
@ -1340,22 +1307,18 @@ set the =jupyter-request-inhibited-handlers= slot of a =jupyter-request=
object. This slot can take the same values as =jupyter-inhibit-handlers=.
** Waiting for messages
When using an asynchronous method of communication between a kernel and a
client, e.g. when =jupyter-ioloop-comm= is used, there needs to be a way for a
program to guarantee that some message has already been received. That is, a
program must be able to wait for particular messages before proceeding.
All message passing between the kernel and Emacs happens asynchronously. So if
a code path in Emacs Lisp is dependent on some message already having been
received, e.g. an idle message, there needs to be primitives that will block so
that there is a guarantee that a particular message has been received before
proceeding.
=jupyter-wait-until= will wait until the function it is passed returns non-nil
for one of the messages received by a request and will return either the value
of the function or =nil= if a timeout is reached. The default timeout
is =jupyter-default-timeout=.
The following functions all wait for different conditions to be met on the
received messages of a request and return the message that caused the function
to stop waiting or =nil= if no message was received within a timeout period.
The default timeout is =jupyter-default-timeout= seconds.
=jupyter-wait-until-received= and =jupyter-wait-until-idle= are convenience
functions built on top of =jupyter-wait-until=.
*** Examples
Wait until an idle message has been received for a request:
For example, to wait until an idle message has been received for a request:
#+BEGIN_SRC elisp
(let ((timeout 4))
@ -1365,13 +1328,37 @@ Wait until an idle message has been received for a request:
timeout))
#+END_SRC
Wait until a message of a specific type is received for a request:
To wait until a message of a specific type is received for a request:
#+BEGIN_SRC elisp
(jupyter-wait-until-received :execute-reply
(jupyter-send-execute-request client :code "[i*10 for i in range(100000)]"))
#+END_SRC
The most general form of the blocking functions is =jupyter-wait-until= which
takes a message type and a predicate function of a single argument. Whenever a
message is received that matches the message type, the message is passed to the
function to determine if =jupyter-wait-until= should return from waiting.
#+BEGIN_SRC elisp
(defun stream-prints-50-p (msg)
(let ((text (jupyter-message-get msg :text)))
(cl-loop for line in (split-string text "\n")
thereis (equal line "50"))))
(let ((timeout 2))
(jupyter-wait-until
(jupyter-send-execute-request client :code "[print(i) for i in range(100)]")
:stream #'stream-prints-50-p
timeout))
#+END_SRC
The above code runs =stream-prints-50-p= for every =stream= message received
from a kernel (here assumed to be a python kernel) for an execute request that
prints the numbers 0 to 99 and waits until the kernel has printed the number 50
before returning from the =jupyter-wait-until= call. If the number 50 is not
printed before the two second timeout, =jupyter-wait-until= returns =nil=.
Otherwise it returns the stream message whose content contains the number 50.
** Message property lists
:PROPERTIES:
:ID: D09FDD89-43A9-41DA-A6E8-6D6C73336981

View file

@ -1,154 +0,0 @@
;;; jupyter-server-ioloop-comm.el --- Async support for Jupyter kernel servers -*- lexical-binding: t -*-
;; Copyright (C) 2020 Nathaniel Nicandro
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
;; Created: 11 Mar 2020
;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation; either version 3, or (at
;; your option) any later version.
;; This program is distributed in the hope that it will be useful, but
;; WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;; General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING. If not, write to the
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.
;;; Commentary:
;; Async support for Jupyter kernel servers. Requires ZMQ.
;;; Code:
(require 'jupyter-server)
(require 'jupyter-ioloop-comm)
(require 'jupyter-server-ioloop)
(defgroup jupyter-server-ioloop-comm nil
"Async support for Jupyter kernel servers (requires ZMQ)."
:group 'jupyter)
(defclass jupyter-server-ioloop-comm (jupyter-server jupyter-ioloop-comm)
())
(defclass jupyter-server-ioloop-kernel-comm (jupyter-server-abstract-kcomm)
())
;;; `jupyter-ioloop-comm' event handlers
(cl-defmethod jupyter-event-handler ((comm jupyter-server-ioloop-comm)
(event (head disconnect-channels)))
(let ((kernel-id (cadr event)))
(with-slots (ioloop) comm
(cl-callf2 remove kernel-id
(process-get (oref ioloop process) :kernel-ids)))))
(cl-defmethod jupyter-event-handler ((comm jupyter-server-ioloop-comm)
(event (head connect-channels)))
(let ((kernel-id (cadr event)))
(with-slots (ioloop) comm
(cl-callf append (process-get (oref ioloop process) :kernel-ids)
(list kernel-id)))))
(cl-defmethod jupyter-event-handler ((comm jupyter-server-ioloop-comm) event)
"Send EVENT to all clients connected to COMM.
Each client must have a KERNEL slot which, in turn, must have an
ID slot. The second element of EVENT is expected to be a kernel
ID. Send EVENT, with the kernel ID excluded, to a client whose
kernel has a matching ID."
(let ((kernel-id (cadr event)))
(setq event (cons (car event) (cddr event)))
(jupyter-comm-handler-loop comm client
(when (equal kernel-id (oref (oref client kernel) id))
;; TODO: Since the event handlers of CLIENT will eventually call the
;; `jupyter-handle-message' of a `jupyter-kernel-client' we really
;; don't need to do any filtering based off of a `jupyter-session-id',
;; but maybe should? The `jupyter-handle-message' method will only
;; handle messages that have a parent ID of a previous request so there
;; already is filtering at the kernel client level.
(jupyter-event-handler client event)))))
;;; `jupyter-ioloop-comm' methods
(cl-defmethod jupyter-comm-start ((comm jupyter-server-ioloop-comm))
(unless (and (slot-boundp comm 'ioloop)
(jupyter-ioloop-alive-p (oref comm ioloop)))
;; TODO: Is a write to the cookie file and then a read of the cookie file
;; whenever connecting a websocket in a subprocess good enough? If, e.g.
;; the notebook is restarted and it clears the login information, there are
;; sometimes error due to `jupyter-api-request' trying to ask for login
;; information which look like "wrong type argument listp, [http://...]".
;; They don't seem to happens with the changes mentioned, but is it enough?
(url-cookie-write-file)
(oset comm ioloop (jupyter-server-ioloop
:url (oref comm url)
:ws-url (oref comm ws-url)
:ws-headers (jupyter-api-auth-headers comm)))
(cl-call-next-method)))
(cl-defmethod jupyter-comm-add-handler ((comm jupyter-server-ioloop-comm)
(kcomm jupyter-server-ioloop-kernel-comm))
(cl-call-next-method)
(with-slots (id) (oref kcomm kernel)
(unless (jupyter-server-kernel-connected-p comm id)
(jupyter-server--connect-channels comm id))))
(cl-defmethod jupyter-comm-remove-handler ((comm jupyter-server-ioloop-comm)
(kcomm jupyter-server-ioloop-kernel-comm))
(with-slots (id) (oref kcomm kernel)
(when (jupyter-server-kernel-connected-p comm id)
(jupyter-send comm 'disconnect-channels id)
(unless (jupyter-ioloop-wait-until (oref comm ioloop)
'disconnect-channels #'identity)
(error "Timeout when disconnecting websocket for kernel id %s" id))))
(cl-call-next-method))
(cl-defmethod jupyter-server-kernel-connected-p ((comm jupyter-server-ioloop-comm) id)
"Return non-nil if COMM has a WebSocket connection to a kernel with ID."
(when (jupyter-comm-alive-p comm)
(with-slots (process) (oref comm ioloop)
(member id (process-get process :kernel-ids)))))
;; `jupyter-server-ioloop-kcomm'
(cl-defmethod jupyter-comm-start ((comm jupyter-server-ioloop-kernel-comm) &rest _ignore)
"Register COMM to receive server events.
If SERVER receives events that have the same kernel ID as the
kernel associated with COMM, then COMM's `jupyter-event-handler'
will receive those events."
(with-slots (server) (oref comm kernel)
(jupyter-comm-add-handler server comm)))
(cl-defmethod jupyter-comm-stop ((comm jupyter-server-ioloop-kernel-comm) &rest _ignore)
"Disconnect COMM from receiving server events."
(jupyter-comm-remove-handler (oref (oref comm kernel) server) comm))
(cl-defmethod jupyter-comm-alive-p ((comm jupyter-server-ioloop-kernel-comm))
"Return non-nil if COMM can receive server events for its associated kernel."
(with-slots (kernel) comm
(and (jupyter-server-kernel-connected-p
(oref kernel server)
(oref kernel id))
(catch 'member
(jupyter-comm-handler-loop (oref kernel server) client
(when (eq client comm)
(throw 'member t)))))))
(cl-defmethod jupyter-send ((comm jupyter-server-ioloop-kernel-comm) event-type &rest event)
"Use COMM to send an EVENT to the server with type, EVENT-TYPE.
SERVER will direct EVENT to the right kernel based on the kernel
ID of the kernel associated with COMM."
(with-slots (kernel) comm
(unless (jupyter-comm-alive-p comm)
(jupyter-comm-start comm))
(apply #'jupyter-send (oref kernel server) event-type (oref kernel id) event)))
(provide 'jupyter-server-ioloop-comm)
;;; jupyter-server-ioloop-comm.el ends here

View file

@ -77,22 +77,17 @@
(require 'jupyter-repl)
(require 'jupyter-rest-api)
(require 'jupyter-kernel-manager)
(require 'jupyter-ioloop-comm)
(require 'jupyter-server-ioloop)
(declare-function jupyter-tramp-file-name-p "jupyter-tramp" (filename))
(declare-function jupyter-tramp-server-from-file-name "jupyter-tramp" (filename))
(declare-function jupyter-tramp-file-name-from-url "jupyter-tramp" (url))
(declare-function jupyter-server-ioloop-kernel-comm "jupyter-server-ioloop-comm")
(declare-function jupyter-server-ioloop-comm "jupyter-server-ioloop-comm")
(defgroup jupyter-server nil
"Support for the Jupyter kernel gateway"
:group 'jupyter)
(defcustom jupyter-server-use-zmq (and (locate-library "zmq") t)
"Whether or not ZMQ should be used as a backend."
:type 'boolean
:group 'jupyter)
(defvar-local jupyter-current-server nil
"The `jupyter-server' associated with the current buffer.
Used in, e.g. a `jupyter-server-kernel-list-mode' buffer.")
@ -107,6 +102,7 @@ Used in, e.g. a `jupyter-server-kernel-list-mode' buffer.")
;; `jupyter-server-client' since it isn't a representation of a server, but a
;; communication channel with one.
(defclass jupyter-server (jupyter-rest-client
jupyter-ioloop-comm
eieio-instance-tracker)
((tracking-symbol :initform 'jupyter--servers)
(kernelspecs
@ -163,14 +159,10 @@ Access should be done through `jupyter-available-kernelspecs'.")))
;; The notebook server already takes care of forcing shutdown of a kernel.
(ignore))
(defclass jupyter-server-abstract-kcomm (jupyter-comm-layer)
((kernel :type jupyter-server-kernel :initarg :kernel))
:abstract t)
(defclass jupyter-server-kernel-comm (jupyter-comm-layer)
((kernel :type jupyter-server-kernel :initarg :kernel)))
(defclass jupyter-server-kernel-comm (jupyter-server-abstract-kcomm)
((ws :type websocket)))
(cl-defmethod jupyter-comm-id ((comm jupyter-server-abstract-kcomm))
(cl-defmethod jupyter-comm-id ((comm jupyter-server-kernel-comm))
(let* ((kernel (oref comm kernel))
(id (oref kernel id)))
(or (jupyter-server-kernel-name (oref kernel server) id)
@ -249,7 +241,7 @@ CLIENT must be communicating with a `jupyter-server-kernel', the
ID of the kernel will be associated with NAME, see
`jupyter-server-kernel-names'."
(cl-check-type client jupyter-kernel-client)
(cl-check-type (oref client kcomm) jupyter-server-abstract-kcomm)
(cl-check-type (oref client kcomm) jupyter-server-kernel-comm)
(let* ((kernel (thread-first client
(oref kcomm)
(oref kernel)))
@ -258,25 +250,53 @@ ID of the kernel will be associated with NAME, see
;;; Plumbing
;;;; `jupyter-server' events
(cl-defmethod jupyter-event-handler ((comm jupyter-server)
(event (head disconnect-channels)))
(let ((kernel-id (cadr event)))
(with-slots (ioloop) comm
(cl-callf2 remove kernel-id
(process-get (oref ioloop process) :kernel-ids)))))
(cl-defmethod jupyter-event-handler ((comm jupyter-server)
(event (head connect-channels)))
(let ((kernel-id (cadr event)))
(with-slots (ioloop) comm
(cl-callf append (process-get (oref ioloop process) :kernel-ids)
(list kernel-id)))))
(cl-defmethod jupyter-event-handler ((comm jupyter-server) event)
"Send EVENT to all clients connected to COMM.
Each client must have a KERNEL slot which, in turn, must have an
ID slot. The second element of EVENT is expected to be a kernel
ID. Send EVENT, with the kernel ID excluded, to a client whose
kernel has a matching ID."
(let ((kernel-id (cadr event)))
(setq event (cons (car event) (cddr event)))
(jupyter-comm-handler-loop comm client
(when (equal kernel-id (oref (oref client kernel) id))
;; TODO: Since the event handlers of CLIENT will eventually call the
;; `jupyter-handle-message' of a `jupyter-kernel-client' we really
;; don't need to do any filtering based off of a `jupyter-session-id',
;; but maybe should? The `jupyter-handle-message' method will only
;; handle messages that have a parent ID of a previous request so there
;; already is filtering at the kernel client level.
(jupyter-event-handler client event)))))
;;;; `jupyter-server' methods
(cl-defgeneric jupyter-server-kernel-connected-p ((client jupyter-server) id)
"Return non-nil if CLIENT can communicate with a kernel that has ID.")
;; TODO: Move the following two functions to jupyter-server-ioloop-comm.el
(defun jupyter-server--connect-channels (server id)
(when (object-of-class-p server 'jupyter-comm-layer)
(jupyter-send server 'connect-channels id)
(jupyter-with-timeout
(nil jupyter-default-timeout
(error "Timeout when connecting websocket to kernel id %s" id))
(jupyter-server-kernel-connected-p server id))))
(jupyter-server-kernel-connected-p server id)))
(defun jupyter-server--refresh-comm (server)
"Stop and then start SERVER communication.
Reconnect the previously connected kernels when starting."
(when (and (object-of-class-p server 'jupyter-comm-layer)
(jupyter-comm-alive-p server))
(when (jupyter-comm-alive-p server)
(let ((connected (cl-remove-if-not
(apply-partially #'jupyter-server-kernel-connected-p server)
(mapcar (lambda (kernel) (plist-get kernel :id))
@ -297,6 +317,44 @@ with default `jupyter-api-authentication-method'"))
(prog1 (cl-call-next-method)
(jupyter-server--refresh-comm server)))))
(cl-defmethod jupyter-comm-start ((comm jupyter-server))
(unless (and (slot-boundp comm 'ioloop)
(jupyter-ioloop-alive-p (oref comm ioloop)))
;; TODO: Is a write to the cookie file and then a read of the cookie file
;; whenever connecting a websocket in a subprocess good enough? If, e.g.
;; the notebook is restarted and it clears the login information, there are
;; sometimes error due to `jupyter-api-request' trying to ask for login
;; information which look like "wrong type argument listp, [http://...]".
;; They don't seem to happens with the changes mentioned, but is it enough?
(url-cookie-write-file)
(oset comm ioloop (jupyter-server-ioloop
:url (oref comm url)
:ws-url (oref comm ws-url)
:ws-headers (jupyter-api-auth-headers comm)))
(cl-call-next-method)))
(cl-defmethod jupyter-comm-add-handler ((comm jupyter-server)
(kcomm jupyter-server-kernel-comm))
(cl-call-next-method)
(with-slots (id) (oref kcomm kernel)
(unless (jupyter-server-kernel-connected-p comm id)
(jupyter-server--connect-channels comm id))))
(cl-defmethod jupyter-comm-remove-handler ((comm jupyter-server)
(kcomm jupyter-server-kernel-comm))
(with-slots (id) (oref kcomm kernel)
(when (jupyter-server-kernel-connected-p comm id)
(jupyter-send comm 'disconnect-channels id)
(unless (jupyter-ioloop-wait-until (oref comm ioloop)
'disconnect-channels #'identity)
(error "Timeout when disconnecting websocket for kernel id %s" id))))
(cl-call-next-method))
(cl-defmethod jupyter-server-kernel-connected-p ((comm jupyter-server) id)
"Return non-nil if COMM has a WebSocket connection to a kernel with ID."
(and (jupyter-comm-alive-p comm)
(member id (process-get (oref (oref comm ioloop) process) :kernel-ids))))
(defun jupyter-server--verify-kernelspec (server spec)
(cl-destructuring-bind (name _ . kspec) spec
(let ((server-spec (assoc name (jupyter-server-kernelspecs server))))
@ -333,75 +391,53 @@ The kernelspecs are returned in the same form as returned by
(cons nil (plist-get spec :spec)))))))
(plist-get (oref server kernelspecs) :kernelspecs))
;;;; `jupyter-server-kernel-comm'
;;;; `jupyter-server-kernel-comm' methods
;; TODO: Remove the need for these methods, they are remnants from an older
;; implementation. They will need to be removed from `jupyter-kernel-client'.
(cl-defmethod jupyter-channel-alive-p ((comm jupyter-server-abstract-kcomm) _channel)
(jupyter-comm-alive-p comm))
(cl-defmethod jupyter-comm-start ((comm jupyter-server-kernel-comm) &rest _ignore)
"Register COMM to receive server events.
If SERVER receives events that have the same kernel ID as the
kernel associated with COMM, then COMM's `jupyter-event-handler'
will receive those events."
(with-slots (server) (oref comm kernel)
(jupyter-comm-start server)
(jupyter-comm-add-handler server comm)))
(cl-defmethod jupyter-channels-running-p ((comm jupyter-server-abstract-kcomm))
(jupyter-comm-alive-p comm))
(cl-defmethod jupyter-comm-stop ((comm jupyter-server-kernel-comm) &rest _ignore)
"Disconnect COMM from receiving server events."
(jupyter-comm-remove-handler (oref (oref comm kernel) server) comm))
(defun jupyter-server--ws-on-message (ws frame)
(cl-case (websocket-frame-opcode frame)
((text binary)
(let* ((msg (jupyter-read-plist-from-string
(websocket-frame-payload frame)))
;; TODO: Get rid of some of these explicit/implicit `intern' calls
(channel (intern (concat ":" (plist-get msg :channel))))
(msg-type (jupyter-message-type-as-keyword
(jupyter-message-type msg)))
(parent-header (plist-get msg :parent_header)))
(plist-put msg :msg_type msg-type)
(plist-put parent-header :msg_type msg-type)
(jupyter-event-handler
(plist-get (websocket-client-data ws) :comm)
;; NOTE: The nil is the identity field expected by a
;; `jupyter-channel-ioloop', it is mimicked here.
(cl-list* 'message channel nil msg))))
(t
(error "Unhandled websocket frame opcode (%s)"
(websocket-frame-opcode frame)))))
(cl-defmethod jupyter-comm-start ((comm jupyter-server-kernel-comm))
(unless (jupyter-comm-alive-p comm)
(with-slots (server id) (oref comm kernel)
(let ((ws (jupyter-api-get-kernel-ws
server id
:on-message #'jupyter-server--ws-on-message)))
(oset comm ws ws)
(plist-put (websocket-client-data ws) :comm comm)))))
(cl-defmethod jupyter-comm-stop ((comm jupyter-server-kernel-comm))
(when (jupyter-comm-alive-p comm)
(websocket-close (oref comm ws))
(plist-put (websocket-client-data (oref comm ws)) :comm nil)
(slot-makeunbound comm 'ws)))
(cl-defmethod jupyter-comm-alive-p ((comm jupyter-server-kernel-comm))
(and (slot-boundp comm 'ws)
(eq (plist-get (websocket-client-data (oref comm ws)) :comm) comm)))
(cl-defmethod jupyter-send ((comm jupyter-server-kernel-comm) _event-type &rest event)
(cl-defmethod jupyter-send ((comm jupyter-server-kernel-comm) event-type &rest event)
"Use COMM to send an EVENT to the server with type, EVENT-TYPE.
SERVER will direct EVENT to the right kernel based on the kernel
ID of the kernel associated with COMM."
(with-slots (kernel) comm
(unless (jupyter-comm-alive-p comm)
(jupyter-comm-start comm))
(cl-destructuring-bind (channel msg-type msg &optional msg-id) event
(with-slots (ws) comm
(websocket-send-text
ws (jupyter-encode-raw-message
(plist-get (websocket-client-data ws) :session) msg-type
:channel (substring (symbol-name channel) 1)
:msg-id msg-id
:content msg)))))
(apply #'jupyter-send (oref kernel server) event-type (oref kernel id) event)))
(cl-defmethod jupyter-comm-alive-p ((comm jupyter-server-kernel-comm))
"Return non-nil if COMM can receive server events for its associated kernel."
(with-slots (kernel) comm
(and (jupyter-server-kernel-connected-p
(oref kernel server)
(oref kernel id))
(catch 'member
(jupyter-comm-handler-loop (oref kernel server) client
(when (eq client comm)
(throw 'member t)))))))
;; TODO: Remove the need for these methods, they are remnants from an older
;; implementation. They will need to be removed from `jupyter-kernel-client'.
(cl-defmethod jupyter-channel-alive-p ((comm jupyter-server-kernel-comm) _channel)
(jupyter-comm-alive-p comm))
(cl-defmethod jupyter-channels-running-p ((comm jupyter-server-kernel-comm))
(jupyter-comm-alive-p comm))
;;;; `jupyter-server-kernel-manager'
(defclass jupyter-server-kernel-manager (jupyter-kernel-manager)
((comm :type jupyter-server-abstract-kcomm)))
((comm :type jupyter-server-kernel-comm)))
(cl-defmethod jupyter-comm-start ((manager jupyter-server-kernel-manager))
"Start a websocket connection to MANAGER's kernel.
@ -409,10 +445,7 @@ MANAGER's COMM slot will be set to the `jupyter-comm-layer'
receiving events on the websocket when this method returns."
(with-slots (kernel comm) manager
(unless (slot-boundp manager 'comm)
(oset manager comm
(if (object-of-class-p (oref kernel server) 'jupyter-comm-layer)
(jupyter-server-ioloop-kernel-comm :kernel kernel)
(jupyter-server-kernel-comm :kernel kernel))))
(oset manager comm (jupyter-server-kernel-comm :kernel kernel)))
(unless (jupyter-comm-alive-p comm)
(jupyter-comm-start comm))))
@ -512,18 +545,6 @@ least the following keys:
(define-error 'jupyter-server-non-existent
"The server doesn't exist")
(defun jupyter-server-make-instance (&rest args)
"Return a different subclass of `jupyter-server' depending on `jupyter-server-use-zmq'.
A `jupyter-server-ioloop-comm' object is returned if
`jupyter-server-use-zmq' is non-nil, a `jupyter-server' is
returned otherwise. ARGS is passed to the `make-instance'
invocation for the subclass."
(if jupyter-server-use-zmq
(progn
(require 'jupyter-server-ioloop-comm)
(apply #'jupyter-server-ioloop-comm args))
(apply #'jupyter-server args)))
(defun jupyter-current-server (&optional ask)
"Return an existing `jupyter-server' or ASK for a new one.
If ASK is non-nil, always ask for a URL and return the
@ -562,7 +583,7 @@ a URL."
(setf (url-type u) "ws")
(url-recreate-url u)))))
(or (jupyter-find-server url ws-url)
(let ((server (jupyter-server-make-instance :url url :ws-url ws-url)))
(let ((server (jupyter-server :url url :ws-url ws-url)))
(if (jupyter-api-server-exists-p server) server
(delete-instance server)
(signal 'jupyter-server-non-existent (list url)))))))))
@ -574,7 +595,7 @@ a URL."
((and jupyter-current-client
(object-of-class-p
(oref jupyter-current-client kcomm)
'jupyter-server-abstract-kcomm)
'jupyter-server-kernel-comm)
(thread-first jupyter-current-client
(oref kcomm)
(oref kernel)

View file

@ -239,7 +239,7 @@ variables in PARAMS."
(require 'jupyter-server)
(let* ((url (jupyter-tramp-url-from-file-name session))
(server (or (jupyter-tramp-server-from-file-name session)
(jupyter-server-make-instance :url url)))
(jupyter-server :url url)))
(localname (file-local-name session))
(name-or-id (if (null localname) (error "No remote server session name")
;; If a kernel has an associated name, get its kernel ID

View file

@ -257,29 +257,9 @@
;;; Server
;; And `jupyter-server-kernel-comm'
(ert-deftest jupyter-server ()
:tags '(server)
(let ((jupyter-server-use-zmq nil))
(jupyter-test-with-notebook server
(jupyter-test-with-server-kernel server "python" kernel
(ert-info ("`jupyter-comm-layer' methods")
(let ((kcomm (jupyter-server-kernel-comm :kernel kernel)))
(should-not (jupyter-comm-alive-p kcomm))
(jupyter-comm-start kcomm)
(should (jupyter-comm-alive-p kcomm))
(jupyter-comm-stop kcomm)
(should-not (jupyter-comm-alive-p kcomm))))
(ert-info ("Sending/receiving")
(let ((client (jupyter-kernel-client)))
(oset client kcomm (jupyter-server-kernel-comm :kernel kernel))
(jupyter-start-channels client)
(should (json-plist-p (jupyter-kernel-info client)))
(jupyter-stop-channels client)))))))
;; And `jupyter-server-ioloop-kernel-comm'
(ert-deftest jupyter-server-ioloop-comm ()
:tags '(server)
(skip-unless jupyter-server-use-zmq)
(jupyter-test-with-notebook server
(ert-info ("`jupyter-comm-layer' methods")
(when (jupyter-comm-alive-p server)
@ -292,7 +272,7 @@
(let ((id (oref kernel id)))
(should (jupyter-api-get-kernel server id))
(ert-info ("Connecting kernel comm to server")
(let ((kcomm (jupyter-server-ioloop-kernel-comm
(let ((kcomm (jupyter-server-kernel-comm
:kernel kernel)))
(should-not (jupyter-server-kernel-connected-p server id))
(jupyter-comm-add-handler server kcomm)
@ -302,7 +282,7 @@
(should-not (jupyter-comm-alive-p kcomm))
(should (jupyter-comm-alive-p server))))
(ert-info ("Connecting kernel comm starts server comm if necessary")
(let ((kcomm (jupyter-server-ioloop-kernel-comm
(let ((kcomm (jupyter-server-kernel-comm
:kernel kernel)))
(jupyter-comm-stop server)
(should-not (jupyter-comm-alive-p server))

View file

@ -26,6 +26,8 @@
;;; Code:
(require 'zmq)
(require 'jupyter-zmq-channel-comm)
(require 'jupyter-env)
(require 'jupyter-client)
(require 'jupyter-repl)
@ -491,6 +493,58 @@
msg)
(cons "idle" "foo")))))
;;; Channels
(ert-deftest jupyter-zmq-channel ()
:tags '(channels zmq)
(let* ((port (car (jupyter-available-local-ports 1)))
(channel (jupyter-zmq-channel
:type :shell
:endpoint (format "tcp://127.0.0.1:%s" port))))
(ert-info ("Starting the channel")
(should-not (jupyter-channel-alive-p channel))
(jupyter-start-channel channel :identity "foo")
(should (jupyter-channel-alive-p channel))
(should (equal (zmq-socket-get (oref channel socket)
zmq-ROUTING-ID)
"foo")))
(ert-info ("Stopping the channel")
(let ((sock (oref channel socket)))
(jupyter-stop-channel channel)
(should-not (jupyter-channel-alive-p channel))
;; Ensure the socket was disconnected
(should-error (zmq-send sock "foo" zmq-NOBLOCK) :type 'zmq-EAGAIN)))))
(ert-deftest jupyter-hb-channel ()
:tags '(channels)
(should (eq (oref (jupyter-hb-channel) type) :hb))
(let* ((port (car (jupyter-available-local-ports 1)))
(channel (jupyter-hb-channel
:endpoint (format "tcp://127.0.0.1:%s" port)
:session (jupyter-session)))
(died-cb-called nil)
(jupyter-hb-max-failures 1))
(oset channel time-to-dead 0.1)
(should-not (jupyter-channel-alive-p channel))
(should-not (jupyter-hb-beating-p channel))
(should (oref channel paused))
(oset channel beating t)
(jupyter-start-channel channel)
(jupyter-hb-on-kernel-dead channel (lambda () (setq died-cb-called t)))
(should (jupyter-channel-alive-p channel))
;; `jupyter-hb-unpause' needs to explicitly called
(should (oref channel paused))
(jupyter-hb-unpause channel)
(sleep-for 0.2)
;; It seems the timers are run after returning from the first `sleep-for'
;; call.
(sleep-for 0.1)
(should (oref channel paused))
(should-not (oref channel beating))
(should died-cb-called)
(should (jupyter-channel-alive-p channel))
(should-not (jupyter-hb-beating-p channel))))
;;; GC
(ert-deftest jupyter-weak-ref ()
@ -555,6 +609,45 @@
(lambda (_) nil)))
(should-error (jupyter-locate-python))))
(ert-deftest jupyter-kernel-lifetime ()
:tags '(kernel)
(let* ((conn-info (jupyter-local-tcp-conn-info))
(kernel (jupyter-spec-kernel
:spec (jupyter-guess-kernelspec "python")
:session (jupyter-session
:key (plist-get conn-info :key)
:conn-info conn-info))))
(should-not (jupyter-kernel-alive-p kernel))
(jupyter-start-kernel kernel)
(should (jupyter-kernel-alive-p kernel))
(jupyter-kill-kernel kernel)
(should-not (jupyter-kernel-alive-p kernel))
(setq conn-info (jupyter-local-tcp-conn-info))
(ert-info ("`jupyter-kernel-manager'")
;; TODO: Should the manager create a session if one isn't present?
(oset kernel session (jupyter-session
:key (plist-get conn-info :key)
:conn-info conn-info))
(let* ((manager (jupyter-kernel-process-manager :kernel kernel))
(control-channel (oref manager control-channel))
process)
(should-not (jupyter-kernel-alive-p manager))
(should-not control-channel)
(jupyter-start-kernel manager)
(setq process (oref kernel process))
(setq control-channel (oref manager control-channel))
(should (jupyter-zmq-channel-p control-channel))
(should (jupyter-kernel-alive-p manager))
(should (jupyter-kernel-alive-p kernel))
(jupyter-shutdown-kernel manager)
(ert-info ("Kernel shutdown is clean")
(should-not (process-live-p process))
(should (zerop (process-exit-status process)))
(should-not (jupyter-kernel-alive-p manager))
(should-not (jupyter-kernel-alive-p kernel)))
(setq control-channel (oref manager control-channel))
(should-not (jupyter-zmq-channel-p control-channel))))))
(ert-deftest jupyter-command-kernel ()
:tags '(kernel)
(let ((kernel (jupyter-command-kernel
@ -610,6 +703,49 @@
;;; Client
;; TODO: Different values of the session argument
;; TODO: Update for new `jupyter-channel-ioloop-comm'
(ert-deftest jupyter-comm-initialize ()
:tags '(client init)
(skip-unless nil)
;; The default comm is a jupyter-channel-ioloop-comm
(let ((conn-info (jupyter-test-conn-info-plist))
(client (jupyter-kernel-client)))
(oset client kcomm (jupyter-zmq-channel-comm))
(jupyter-comm-initialize client conn-info)
;; kcomm by default is a `jupyter-channel-ioloop-comm'
(with-slots (session kcomm) client
(ert-info ("Client session")
(should (string= (jupyter-session-key session)
(plist-get conn-info :key)))
(should (equal (jupyter-session-conn-info session)
conn-info)))
(ert-info ("Heartbeat channel initialized")
(should (eq session (oref (oref kcomm hb) session)))
(should (string= (oref (oref kcomm hb) endpoint)
(format "tcp://127.0.0.1:%d"
(plist-get conn-info :hb_port)))))
(ert-info ("Shell, iopub, stdin initialized")
(cl-loop
for channel in '(:shell :iopub :stdin)
for port_sym = (intern (concat (symbol-name channel) "_port"))
do
(should (plist-member (plist-get channels channel) :alive-p))
(should (plist-member (plist-get channels channel) :endpoint))
(should
(string= (plist-get (plist-get channels channel) :endpoint)
(format "tcp://127.0.0.1:%d"
(plist-get conn-info port_sym))))))
(ert-info ("Initialization stops any running channels")
(should-not (jupyter-channels-running-p client))
(jupyter-start-channels client)
(should (jupyter-channels-running-p client))
(jupyter-comm-initialize client conn-info)
(should-not (jupyter-channels-running-p client)))
(ert-info ("Invalid signature scheme")
(plist-put conn-info :signature_scheme "hmac-foo")
(should-error (jupyter-comm-initialize client conn-info))))))
(ert-deftest jupyter-write-connection-file ()
:tags '(client)
(skip-unless (not (memq system-type '(ms-dos windows-nt cygwin))))
@ -635,6 +771,28 @@
(delete-file file)))
(should-not (memq fun kill-emacs-hook))))
(ert-deftest jupyter-client-channels ()
:tags '(client channels)
(ert-info ("Starting/stopping channels")
(let ((conn-info (jupyter-test-conn-info-plist))
(client (jupyter-kernel-client)))
(oset client kcomm (jupyter-zmq-channel-comm))
(jupyter-comm-initialize client conn-info)
(cl-loop
for channel in '(:hb :shell :iopub :stdin)
for alive-p = (jupyter-channel-alive-p client channel)
do (should-not alive-p))
(jupyter-start-channels client)
(cl-loop
for channel in '(:hb :shell :iopub :stdin)
for alive-p = (jupyter-channel-alive-p client channel)
do (should alive-p))
(jupyter-stop-channels client)
(cl-loop
for channel in '(:hb :shell :iopub :stdin)
for alive-p = (jupyter-channel-alive-p client channel)
do (should-not alive-p)))))
(ert-deftest jupyter-inhibited-handlers ()
:tags '(client handlers)
(jupyter-test-with-python-client client
@ -750,6 +908,183 @@
(should (memq r1 mapped))
(should-not (memq r2 mapped)))))
;;; IOloop
(ert-deftest jupyter-ioloop-lifetime ()
:tags '(ioloop)
(let ((ioloop (jupyter-ioloop))
(jupyter-default-timeout 2))
(should-not (process-live-p (oref ioloop process)))
(jupyter-ioloop-start ioloop :tag1)
(should (equal (jupyter-ioloop-last-event ioloop) '(start)))
(with-slots (process) ioloop
(should (process-live-p process))
(jupyter-ioloop-stop ioloop)
(should (equal (jupyter-ioloop-last-event ioloop) '(quit)))
(sleep-for 0.1)
(should-not (process-live-p process)))))
(defvar jupyter-ioloop-test-handler-called nil
"Flag variable used for testing the `juyter-ioloop'.")
(cl-defmethod jupyter-ioloop-handler ((_ioloop jupyter-ioloop)
(_tag (eql :test))
(event (head test)))
(should (equal (cadr event) "message"))
(setq jupyter-ioloop-test-handler-called t))
(ert-deftest jupyter-ioloop-wait-until ()
:tags '(ioloop)
(let ((ioloop (jupyter-ioloop)))
(should-not (jupyter-ioloop-last-event ioloop))
(jupyter-ioloop-start ioloop :test)
(should (equal (jupyter-ioloop-last-event ioloop) '(start)))
(jupyter-ioloop-stop ioloop)))
(ert-deftest jupyter-ioloop-callbacks ()
:tags '(ioloop)
(ert-info ("Callback added before starting the ioloop")
(let ((ioloop (jupyter-ioloop)))
(setq jupyter-ioloop-test-handler-called nil)
(jupyter-ioloop-add-callback ioloop
`(lambda () (zmq-prin1 (list 'test "message"))))
(jupyter-ioloop-start ioloop :test)
(jupyter-ioloop-stop ioloop)
(should jupyter-ioloop-test-handler-called)))
(ert-info ("Callback added after starting the ioloop")
(let ((ioloop (jupyter-ioloop)))
(setq jupyter-ioloop-test-handler-called nil)
(jupyter-ioloop-start ioloop :test)
(should (process-live-p (oref ioloop process)))
(jupyter-ioloop-add-callback ioloop
`(lambda () (zmq-prin1 (list 'test "message"))))
(jupyter-ioloop-wait-until ioloop 'test #'identity)
(jupyter-ioloop-stop ioloop)
(should jupyter-ioloop-test-handler-called))))
(ert-deftest jupyter-ioloop-setup ()
:tags '(ioloop)
(let ((ioloop (jupyter-ioloop)))
(setq jupyter-ioloop-test-handler-called nil)
(jupyter-ioloop-add-setup ioloop
(zmq-prin1 (list 'test "message")))
(jupyter-ioloop-start ioloop :test)
(jupyter-ioloop-stop ioloop)
(should jupyter-ioloop-test-handler-called)))
(ert-deftest jupyter-ioloop-teardown ()
:tags '(ioloop)
(let ((ioloop (jupyter-ioloop)))
(setq jupyter-ioloop-test-handler-called nil)
(jupyter-ioloop-add-teardown ioloop
(zmq-prin1 (list 'test "message")))
(jupyter-ioloop-start ioloop :test)
(jupyter-ioloop-stop ioloop)
(should jupyter-ioloop-test-handler-called)))
(ert-deftest jupyter-ioloop-add-event ()
:tags '(ioloop)
(let ((ioloop (jupyter-ioloop)))
(setq jupyter-ioloop-test-handler-called nil)
(jupyter-ioloop-add-event ioloop test (data)
"Echo DATA back to the parent process."
(list 'test data))
(jupyter-ioloop-start ioloop :test)
(jupyter-send ioloop 'test "message")
(jupyter-ioloop-stop ioloop)
(should jupyter-ioloop-test-handler-called)))
(ert-deftest jupyter-channel-ioloop-send-event ()
:tags '(ioloop)
(jupyter-test-channel-ioloop
(ioloop (jupyter-zmq-channel-ioloop))
(cl-letf (((symbol-function #'jupyter-send)
(lambda (_channel _msg-type _msg msg-id) msg-id)))
(setq jupyter-channel-ioloop-session (jupyter-session :key "foo"))
(push (jupyter-zmq-channel :type :shell) jupyter-channel-ioloop-channels)
(let* ((msg-id (jupyter-new-uuid))
(event `(list 'send :shell :execute-request '(msg) ,msg-id)))
(jupyter-test-ioloop-eval-event ioloop event)
(ert-info ("Return value to parent process")
(let ((result (read (buffer-string))))
(should (equal result `(sent :shell ,msg-id)))))))))
(ert-deftest jupyter-channel-ioloop-start-channel-event ()
:tags '(ioloop)
(jupyter-test-channel-ioloop
(ioloop (jupyter-zmq-channel-ioloop))
(setq jupyter-channel-ioloop-session (jupyter-session :key "foo"))
(let ((channel-endpoint "tcp://127.0.0.1:5555"))
(ert-info ("start-channel event creates channel")
(should (null jupyter-channel-ioloop-channels))
(let ((event `(list 'start-channel :shell ,channel-endpoint)))
(jupyter-test-ioloop-eval-event ioloop event))
(should-not (null jupyter-channel-ioloop-channels))
(let ((channel (object-assoc :shell :type jupyter-channel-ioloop-channels)))
(should (jupyter-zmq-channel-p channel))))
(let ((channel (object-assoc :shell :type jupyter-channel-ioloop-channels)))
(with-slots (type socket endpoint) channel
(ert-info ("Verify the requested channel was started")
(should (eq type :shell))
(should (zmq-socket-p socket))
(should (equal endpoint channel-endpoint))
(should (equal (zmq-socket-get socket zmq-LAST-ENDPOINT) channel-endpoint))
(ert-info ("Identity of socket matches session")
(should (equal (zmq-socket-get socket zmq-IDENTITY)
(jupyter-session-id jupyter-channel-ioloop-session)))))
(ert-info ("Ensure the channel was added to the poller")
;; FIXME: Does it make sense to have this side effect as part of starting
;; a channel? It makes it so that we don't reference any `zmq' functions
;; in `jupyter-channel-ioloop'.
(should-error
(zmq-poller-add jupyter-ioloop-poller socket (list zmq-POLLIN))
:type 'zmq-EINVAL)))
(ert-info ("Return value to parent process")
(let ((result (read (buffer-string))))
(should (equal result `(start-channel :shell)))))))))
(ert-deftest jupyter-channel-ioloop-stop-channel-event ()
:tags '(ioloop)
(jupyter-test-channel-ioloop
(ioloop (jupyter-zmq-channel-ioloop))
(setq jupyter-channel-ioloop-session (jupyter-session :key "foo"))
(let ((event `(list 'start-channel :shell "tcp://127.0.0.1:5556")))
(jupyter-test-ioloop-eval-event ioloop event)
(erase-buffer))
(let* ((channel (object-assoc :shell :type jupyter-channel-ioloop-channels))
(socket (oref channel socket)))
(ert-info ("Verify the requested channel stops")
(should (jupyter-channel-alive-p channel))
(should (progn (zmq-poller-modify
jupyter-ioloop-poller
(oref channel socket) (list zmq-POLLIN zmq-POLLOUT))
t))
(jupyter-test-ioloop-eval-event ioloop `(list 'stop-channel :shell))
(should-not (jupyter-channel-alive-p channel)))
(ert-info ("Ensure the channel was removed from the poller")
(should-error
(zmq-poller-modify jupyter-ioloop-poller socket (list zmq-POLLIN))
:type 'zmq-EINVAL))
(ert-info ("Return value to parent process")
(let ((result (read (buffer-string))))
(should (equal result `(stop-channel :shell))))))))
(ert-deftest jupyter-zmq-channel-ioloop-send-fast ()
:tags '(ioloop queue)
;; :expected-result :failed
(jupyter-test-with-python-client client
(let ((jupyter-current-client client))
(jupyter-send-execute-request client :code "1 + 1")
(jupyter-send-execute-request client :code "1 + 1")
(jupyter-send-execute-request client :code "1 + 1")
(let ((req (jupyter-send-execute-request client :code "1 + 1")))
(should
(equal
(jupyter-message-data
(jupyter-wait-until-received :execute-result req jupyter-long-timeout)
:text/plain)
"2"))))))
;;; Completion
(ert-deftest jupyter-completion-number-p ()

View file

@ -1,414 +0,0 @@
;;; jupyter-zmq-test.el --- Jupyter tests that require ZMQ -*- lexical-binding: t -*-
;; Copyright (C) 2020 Nathaniel Nicandro
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
;; Created: 13 Mar 2020
;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation; either version 3, or (at
;; your option) any later version.
;; This program is distributed in the hope that it will be useful, but
;; WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;; General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING. If not, write to the
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.
;;; Commentary:
;;
;;; Code:
(require 'zmq)
(require 'jupyter-ioloop)
(require 'jupyter-zmq-channel-ioloop)
(require 'jupyter-zmq-channel-comm)
(defvar jupyter-test-zmq-sockets (make-hash-table :weakness 'key))
(advice-add 'zmq-socket
:around (lambda (&rest args)
(let ((sock (apply args)))
(prog1 sock
(puthash sock t jupyter-test-zmq-sockets)))))
(add-hook
'kill-emacs-hook
(lambda ()
;; Do this cleanup to avoid core dumps on Travis due to epoll reconnect
;; attempts.
(cl-loop
for sock being the hash-keys of jupyter-test-zmq-sockets do
(ignore-errors
(zmq-set-option sock zmq-LINGER 0)
(zmq-close sock)))
(ignore-errors (zmq-context-terminate (zmq-current-context)))))
;;; Channels
(ert-deftest jupyter-zmq-channel ()
:tags '(channels zmq)
(let* ((port (car (jupyter-available-local-ports 1)))
(channel (jupyter-zmq-channel
:type :shell
:endpoint (format "tcp://127.0.0.1:%s" port))))
(ert-info ("Starting the channel")
(should-not (jupyter-channel-alive-p channel))
(jupyter-start-channel channel :identity "foo")
(should (jupyter-channel-alive-p channel))
(should (equal (zmq-socket-get (oref channel socket)
zmq-ROUTING-ID)
"foo")))
(ert-info ("Stopping the channel")
(let ((sock (oref channel socket)))
(jupyter-stop-channel channel)
(should-not (jupyter-channel-alive-p channel))
;; Ensure the socket was disconnected
(should-error (zmq-send sock "foo" zmq-NOBLOCK) :type 'zmq-EAGAIN)))))
(ert-deftest jupyter-hb-channel ()
:tags '(channels zmq)
(should (eq (oref (jupyter-hb-channel) type) :hb))
(let* ((port (car (jupyter-available-local-ports 1)))
(channel (jupyter-hb-channel
:endpoint (format "tcp://127.0.0.1:%s" port)
:session (jupyter-session)))
(died-cb-called nil)
(jupyter-hb-max-failures 1))
(oset channel time-to-dead 0.1)
(should-not (jupyter-channel-alive-p channel))
(should-not (jupyter-hb-beating-p channel))
(should (oref channel paused))
(oset channel beating t)
(jupyter-start-channel channel)
(jupyter-hb-on-kernel-dead channel (lambda () (setq died-cb-called t)))
(should (jupyter-channel-alive-p channel))
;; `jupyter-hb-unpause' needs to explicitly called
(should (oref channel paused))
(jupyter-hb-unpause channel)
(sleep-for 0.2)
;; It seems the timers are run after returning from the first `sleep-for'
;; call.
(sleep-for 0.1)
(should (oref channel paused))
(should-not (oref channel beating))
(should died-cb-called)
(should (jupyter-channel-alive-p channel))
(should-not (jupyter-hb-beating-p channel))))
;;; Kernel
(ert-deftest jupyter-kernel-lifetime ()
:tags '(kernel zmq)
(let* ((conn-info (jupyter-local-tcp-conn-info))
(kernel (jupyter-spec-kernel
:spec (jupyter-guess-kernelspec "python")
:session (jupyter-session
:key (plist-get conn-info :key)
:conn-info conn-info))))
(should-not (jupyter-kernel-alive-p kernel))
(jupyter-start-kernel kernel)
(should (jupyter-kernel-alive-p kernel))
(jupyter-kill-kernel kernel)
(should-not (jupyter-kernel-alive-p kernel))
(setq conn-info (jupyter-local-tcp-conn-info))
(ert-info ("`jupyter-kernel-manager'")
;; TODO: Should the manager create a session if one isn't present?
(oset kernel session (jupyter-session
:key (plist-get conn-info :key)
:conn-info conn-info))
(let* ((manager (jupyter-kernel-process-manager :kernel kernel))
(control-channel (oref manager control-channel))
process)
(should-not (jupyter-kernel-alive-p manager))
(should-not control-channel)
(jupyter-start-kernel manager)
(setq process (oref kernel process))
(setq control-channel (oref manager control-channel))
(should (jupyter-zmq-channel-p control-channel))
(should (jupyter-kernel-alive-p manager))
(should (jupyter-kernel-alive-p kernel))
(jupyter-shutdown-kernel manager)
(ert-info ("Kernel shutdown is clean")
(should-not (process-live-p process))
(should (zerop (process-exit-status process)))
(should-not (jupyter-kernel-alive-p manager))
(should-not (jupyter-kernel-alive-p kernel)))
(setq control-channel (oref manager control-channel))
(should-not (jupyter-zmq-channel-p control-channel))))))
;;; Client
;; FIXME: These two tests should be written so that they don't depend on ZMQ
;; and then moved back into `jupyter-test.el`.
;; TODO: Different values of the session argument
;; TODO: Update for new `jupyter-channel-ioloop-comm'
(ert-deftest jupyter-comm-initialize ()
:tags '(client init zmq)
(skip-unless nil)
;; The default comm is a jupyter-channel-ioloop-comm
(let ((conn-info (jupyter-test-conn-info-plist))
(client (jupyter-kernel-client)))
(oset client kcomm (jupyter-zmq-channel-comm))
(jupyter-comm-initialize client conn-info)
;; kcomm by default is a `jupyter-channel-ioloop-comm'
(with-slots (session kcomm) client
(ert-info ("Client session")
(should (string= (jupyter-session-key session)
(plist-get conn-info :key)))
(should (equal (jupyter-session-conn-info session)
conn-info)))
(ert-info ("Heartbeat channel initialized")
(should (eq session (oref (oref kcomm hb) session)))
(should (string= (oref (oref kcomm hb) endpoint)
(format "tcp://127.0.0.1:%d"
(plist-get conn-info :hb_port)))))
(ert-info ("Shell, iopub, stdin initialized")
(cl-loop
for channel in '(:shell :iopub :stdin)
for port_sym = (intern (concat (symbol-name channel) "_port"))
do
(should (plist-member (plist-get channels channel) :alive-p))
(should (plist-member (plist-get channels channel) :endpoint))
(should
(string= (plist-get (plist-get channels channel) :endpoint)
(format "tcp://127.0.0.1:%d"
(plist-get conn-info port_sym))))))
(ert-info ("Initialization stops any running channels")
(should-not (jupyter-channels-running-p client))
(jupyter-start-channels client)
(should (jupyter-channels-running-p client))
(jupyter-comm-initialize client conn-info)
(should-not (jupyter-channels-running-p client)))
(ert-info ("Invalid signature scheme")
(plist-put conn-info :signature_scheme "hmac-foo")
(should-error (jupyter-comm-initialize client conn-info))))))
(ert-deftest jupyter-client-channels ()
:tags '(client channels zmq)
(ert-info ("Starting/stopping channels")
(let ((conn-info (jupyter-test-conn-info-plist))
(client (jupyter-kernel-client)))
(oset client kcomm (jupyter-zmq-channel-comm))
(jupyter-comm-initialize client conn-info)
(cl-loop
for channel in '(:hb :shell :iopub :stdin)
for alive-p = (jupyter-channel-alive-p client channel)
do (should-not alive-p))
(jupyter-start-channels client)
(cl-loop
for channel in '(:hb :shell :iopub :stdin)
for alive-p = (jupyter-channel-alive-p client channel)
do (should alive-p))
(jupyter-stop-channels client)
(cl-loop
for channel in '(:hb :shell :iopub :stdin)
for alive-p = (jupyter-channel-alive-p client channel)
do (should-not alive-p)))))
;;; IOloop
(defun jupyter-test-ioloop-eval-event (ioloop event)
(eval
`(progn
,@(oref ioloop setup)
,(jupyter-ioloop--event-dispatcher ioloop event))))
(defmacro jupyter-test-channel-ioloop (ioloop &rest body)
(declare (indent 1))
(let ((var (car ioloop))
(val (cadr ioloop)))
(with-temp-buffer
`(let* ((,var ,val)
(standard-output (current-buffer))
(jupyter-channel-ioloop-channels nil)
(jupyter-channel-ioloop-session nil)
;; Needed so that `jupyter-ioloop-environment-p' passes
(jupyter-ioloop-stdin t)
(jupyter-ioloop-poller (zmq-poller)))
(unwind-protect
(progn ,@body)
(zmq-poller-destroy jupyter-ioloop-poller)
(jupyter-ioloop-stop ,var))))))
(ert-deftest jupyter-ioloop-lifetime ()
:tags '(ioloop zmq)
(let ((ioloop (jupyter-ioloop))
(jupyter-default-timeout 2))
(should-not (process-live-p (oref ioloop process)))
(jupyter-ioloop-start ioloop :tag1)
(should (equal (jupyter-ioloop-last-event ioloop) '(start)))
(with-slots (process) ioloop
(should (process-live-p process))
(jupyter-ioloop-stop ioloop)
(should (equal (jupyter-ioloop-last-event ioloop) '(quit)))
(sleep-for 0.1)
(should-not (process-live-p process)))))
(defvar jupyter-ioloop-test-handler-called nil
"Flag variable used for testing the `juyter-ioloop'.")
(cl-defmethod jupyter-ioloop-handler ((_ioloop jupyter-ioloop)
(_tag (eql :test))
(event (head test)))
(should (equal (cadr event) "message"))
(setq jupyter-ioloop-test-handler-called t))
(ert-deftest jupyter-ioloop-wait-until ()
:tags '(ioloop zmq)
(let ((ioloop (jupyter-ioloop)))
(should-not (jupyter-ioloop-last-event ioloop))
(jupyter-ioloop-start ioloop :test)
(should (equal (jupyter-ioloop-last-event ioloop) '(start)))
(jupyter-ioloop-stop ioloop)))
(ert-deftest jupyter-ioloop-callbacks ()
:tags '(ioloop zmq)
(ert-info ("Callback added before starting the ioloop")
(let ((ioloop (jupyter-ioloop)))
(setq jupyter-ioloop-test-handler-called nil)
(jupyter-ioloop-add-callback ioloop
`(lambda () (zmq-prin1 (list 'test "message"))))
(jupyter-ioloop-start ioloop :test)
(jupyter-ioloop-stop ioloop)
(should jupyter-ioloop-test-handler-called)))
(ert-info ("Callback added after starting the ioloop")
(let ((ioloop (jupyter-ioloop)))
(setq jupyter-ioloop-test-handler-called nil)
(jupyter-ioloop-start ioloop :test)
(should (process-live-p (oref ioloop process)))
(jupyter-ioloop-add-callback ioloop
`(lambda () (zmq-prin1 (list 'test "message"))))
(jupyter-ioloop-wait-until ioloop 'test #'identity)
(jupyter-ioloop-stop ioloop)
(should jupyter-ioloop-test-handler-called))))
(ert-deftest jupyter-ioloop-setup ()
:tags '(ioloop zmq)
(let ((ioloop (jupyter-ioloop)))
(setq jupyter-ioloop-test-handler-called nil)
(jupyter-ioloop-add-setup ioloop
(zmq-prin1 (list 'test "message")))
(jupyter-ioloop-start ioloop :test)
(jupyter-ioloop-stop ioloop)
(should jupyter-ioloop-test-handler-called)))
(ert-deftest jupyter-ioloop-teardown ()
:tags '(ioloop zmq)
(let ((ioloop (jupyter-ioloop)))
(setq jupyter-ioloop-test-handler-called nil)
(jupyter-ioloop-add-teardown ioloop
(zmq-prin1 (list 'test "message")))
(jupyter-ioloop-start ioloop :test)
(jupyter-ioloop-stop ioloop)
(should jupyter-ioloop-test-handler-called)))
(ert-deftest jupyter-ioloop-add-event ()
:tags '(ioloop zmq)
(let ((ioloop (jupyter-ioloop)))
(setq jupyter-ioloop-test-handler-called nil)
(jupyter-ioloop-add-event ioloop test (data)
"Echo DATA back to the parent process."
(list 'test data))
(jupyter-ioloop-start ioloop :test)
(jupyter-send ioloop 'test "message")
(jupyter-ioloop-stop ioloop)
(should jupyter-ioloop-test-handler-called)))
(ert-deftest jupyter-channel-ioloop-send-event ()
:tags '(ioloop zmq)
(jupyter-test-channel-ioloop
(ioloop (jupyter-zmq-channel-ioloop))
(cl-letf (((symbol-function #'jupyter-send)
(lambda (_channel _msg-type _msg msg-id) msg-id)))
(setq jupyter-channel-ioloop-session (jupyter-session :key "foo"))
(push (jupyter-zmq-channel :type :shell) jupyter-channel-ioloop-channels)
(let* ((msg-id (jupyter-new-uuid))
(event `(list 'send :shell :execute-request '(msg) ,msg-id)))
(jupyter-test-ioloop-eval-event ioloop event)
(ert-info ("Return value to parent process")
(let ((result (read (buffer-string))))
(should (equal result `(sent :shell ,msg-id)))))))))
(ert-deftest jupyter-channel-ioloop-start-channel-event ()
:tags '(ioloop zmq)
(jupyter-test-channel-ioloop
(ioloop (jupyter-zmq-channel-ioloop))
(setq jupyter-channel-ioloop-session (jupyter-session :key "foo"))
(let ((channel-endpoint "tcp://127.0.0.1:5555"))
(ert-info ("start-channel event creates channel")
(should (null jupyter-channel-ioloop-channels))
(let ((event `(list 'start-channel :shell ,channel-endpoint)))
(jupyter-test-ioloop-eval-event ioloop event))
(should-not (null jupyter-channel-ioloop-channels))
(let ((channel (object-assoc :shell :type jupyter-channel-ioloop-channels)))
(should (jupyter-zmq-channel-p channel))))
(let ((channel (object-assoc :shell :type jupyter-channel-ioloop-channels)))
(with-slots (type socket endpoint) channel
(ert-info ("Verify the requested channel was started")
(should (eq type :shell))
(should (zmq-socket-p socket))
(should (equal endpoint channel-endpoint))
(should (equal (zmq-socket-get socket zmq-LAST-ENDPOINT) channel-endpoint))
(ert-info ("Identity of socket matches session")
(should (equal (zmq-socket-get socket zmq-IDENTITY)
(jupyter-session-id jupyter-channel-ioloop-session)))))
(ert-info ("Ensure the channel was added to the poller")
;; FIXME: Does it make sense to have this side effect as part of starting
;; a channel? It makes it so that we don't reference any `zmq' functions
;; in `jupyter-channel-ioloop'.
(should-error
(zmq-poller-add jupyter-ioloop-poller socket (list zmq-POLLIN))
:type 'zmq-EINVAL)))
(ert-info ("Return value to parent process")
(let ((result (read (buffer-string))))
(should (equal result `(start-channel :shell)))))))))
(ert-deftest jupyter-channel-ioloop-stop-channel-event ()
:tags '(ioloop zmq)
(jupyter-test-channel-ioloop
(ioloop (jupyter-zmq-channel-ioloop))
(setq jupyter-channel-ioloop-session (jupyter-session :key "foo"))
(let ((event `(list 'start-channel :shell "tcp://127.0.0.1:5556")))
(jupyter-test-ioloop-eval-event ioloop event)
(erase-buffer))
(let* ((channel (object-assoc :shell :type jupyter-channel-ioloop-channels))
(socket (oref channel socket)))
(ert-info ("Verify the requested channel stops")
(should (jupyter-channel-alive-p channel))
(should (progn (zmq-poller-modify
jupyter-ioloop-poller
(oref channel socket) (list zmq-POLLIN zmq-POLLOUT))
t))
(jupyter-test-ioloop-eval-event ioloop `(list 'stop-channel :shell))
(should-not (jupyter-channel-alive-p channel)))
(ert-info ("Ensure the channel was removed from the poller")
(should-error
(zmq-poller-modify jupyter-ioloop-poller socket (list zmq-POLLIN))
:type 'zmq-EINVAL))
(ert-info ("Return value to parent process")
(let ((result (read (buffer-string))))
(should (equal result `(stop-channel :shell))))))))
(ert-deftest jupyter-zmq-channel-ioloop-send-fast ()
:tags '(ioloop queue zmq)
;; :expected-result :failed
(jupyter-test-with-python-client client
(let ((jupyter-current-client client))
(jupyter-send-execute-request client :code "1 + 1")
(jupyter-send-execute-request client :code "1 + 1")
(jupyter-send-execute-request client :code "1 + 1")
(let ((req (jupyter-send-execute-request client :code "1 + 1")))
(should
(equal
(jupyter-message-data
(jupyter-wait-until-received :execute-result req jupyter-long-timeout)
:text/plain)
"2"))))))

View file

@ -26,8 +26,11 @@
;;; Code:
(require 'zmq)
(require 'jupyter-client)
(require 'jupyter-repl)
(require 'jupyter-zmq-channel-ioloop)
(require 'jupyter-channel-ioloop-comm)
(require 'jupyter-org-client)
(require 'jupyter-kernel-process-manager)
(require 'org-element)
@ -86,7 +89,14 @@ handling a message is always
(cl-defmethod initialize-instance ((client jupyter-echo-client) &optional _slots)
(cl-call-next-method)
(oset client messages (make-ring 10))
(oset client kcomm (jupyter-mock-comm-layer)))
(oset client kcomm (jupyter-channel-ioloop-comm
:ioloop-class 'jupyter-zmq-channel-ioloop))
(with-slots (kcomm) client
(oset kcomm hb (jupyter-hb-channel))
(oset kcomm channels
(list :stdin (make-jupyter-proxy-channel)
:shell (make-jupyter-proxy-channel)
:iopub (make-jupyter-proxy-channel)))))
(cl-defmethod jupyter-send ((client jupyter-echo-client)
channel
@ -129,12 +139,6 @@ handling a message is always
(cl-defmethod jupyter-comm-alive-p ((comm jupyter-mock-comm-layer))
(oref comm alive))
(cl-defmethod jupyter-channel-alive-p ((comm jupyter-mock-comm-layer) _channel)
(jupyter-comm-alive-p comm))
(cl-defmethod jupyter-channels-running-p ((comm jupyter-mock-comm-layer))
(jupyter-comm-alive-p comm))
(cl-defmethod jupyter-comm-start ((comm jupyter-mock-comm-layer))
(unless (oref comm alive)
(oset comm alive 0))
@ -280,6 +284,29 @@ running BODY."
`(jupyter-test-with-kernel-repl "python" ,client
,@body))
(defun jupyter-test-ioloop-eval-event (ioloop event)
(eval
`(progn
,@(oref ioloop setup)
,(jupyter-ioloop--event-dispatcher ioloop event))))
(defmacro jupyter-test-channel-ioloop (ioloop &rest body)
(declare (indent 1))
(let ((var (car ioloop))
(val (cadr ioloop)))
(with-temp-buffer
`(let* ((,var ,val)
(standard-output (current-buffer))
(jupyter-channel-ioloop-channels nil)
(jupyter-channel-ioloop-session nil)
;; Needed so that `jupyter-ioloop-environment-p' passes
(jupyter-ioloop-stdin t)
(jupyter-ioloop-poller (zmq-poller)))
(unwind-protect
(progn ,@body)
(zmq-poller-destroy jupyter-ioloop-poller)
(jupyter-ioloop-stop ,var))))))
(defmacro jupyter-test-rest-api-request (bodyform &rest check-forms)
"Replace the body of `url-retrieve*' with CHECK-FORMS, evaluate BODYFORM.
For `url-retrieve', the callback will be called with a nil status."
@ -323,7 +350,7 @@ For `url-retrieve', the callback will be called with a nil status."
`(let* ((host (format "localhost:%s" (jupyter-test-ensure-notebook-server)))
(url (format "http://%s" host))
(,server (or (jupyter-find-server url)
(jupyter-server-make-instance :url url))))
(jupyter-server :url url))))
,@body))
(defmacro jupyter-test-with-server-kernel (server name kernel &rest body)
@ -598,6 +625,14 @@ see the documentation on the --NotebookApp.password argument."
(process-buffer (car jupyter-test-notebook))
(buffer-string)))))))
(defvar jupyter-test-zmq-sockets (make-hash-table :weakness 'key))
(advice-add 'zmq-socket
:around (lambda (&rest args)
(let ((sock (apply args)))
(prog1 sock
(puthash sock t jupyter-test-zmq-sockets)))))
;; Do lots of cleanup to avoid core dumps on Travis due to epoll reconnect
;; attempts.
(add-hook
@ -611,6 +646,12 @@ see the documentation on the --NotebookApp.password argument."
for client in (jupyter-clients)
do (ignore-errors (jupyter-stop-channels client))
(when (oref client manager)
(ignore-errors (jupyter-shutdown-kernel (oref client manager)))))))
(ignore-errors (jupyter-shutdown-kernel (oref client manager)))))
(cl-loop
for sock being the hash-keys of jupyter-test-zmq-sockets do
(ignore-errors
(zmq-set-option sock zmq-LINGER 0)
(zmq-close sock)))
(ignore-errors (zmq-context-terminate (zmq-current-context)))))
;;; test-helper.el ends here