mirror of
https://github.com/vale981/emacs-jupyter
synced 2025-03-05 07:41:37 -05:00
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:
parent
3322ce7b31
commit
a4e0616ed4
9 changed files with 552 additions and 756 deletions
4
Makefile
4
Makefile
|
@ -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
|
||||
|
|
85
README.org
85
README.org
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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-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)))
|
||||
|
||||
(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."
|
||||
(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)))))
|
||||
(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)))
|
||||
|
||||
(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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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 ()
|
||||
|
|
|
@ -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"))))))
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue