Revert "Remove tests related to jupyter-kernel-process, ioloop, comm, zmq, and channels"

This reverts commit 1381d7975d2c47ebcec1359dc24e443aa5af0fa2.
This commit is contained in:
Nathaniel Nicandro 2021-02-06 10:21:28 -06:00
parent 56ada9392b
commit db0f3678ac
2 changed files with 520 additions and 1 deletions

View file

@ -468,6 +468,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-alive-p channel))
(jupyter-start channel :identity "foo")
(should (jupyter-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)
(should-not (jupyter-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-alive-p channel))
(should-not (jupyter-hb-beating-p channel))
(should (oref channel paused))
(oset channel beating t)
(jupyter-start channel)
(jupyter-hb-on-kernel-dead channel (lambda () (setq died-cb-called t)))
(should (jupyter-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-alive-p channel))
(should-not (jupyter-hb-beating-p channel))))
;;; GC
(ert-deftest jupyter-weak-ref ()
@ -517,6 +569,151 @@
(lambda (_) nil)))
(should-error (jupyter-locate-python))))
;; FIXME: Revisit after transition
(ert-deftest jupyter-kernel-process ()
:tags '(kernel)
;; TODO: `jupyter-do-interrupt'
(ert-info ("`jupyter-do-launch', `jupyter-do-shutdown'")
(cl-macrolet
((confirm-shutdown-state
()
`(progn
(should-not (jupyter-alive-p kernel))
(should (jupyter-kernel-spec kernel))
(should-not (jupyter-kernel-session kernel))
(should-not (process-live-p (jupyter-process kernel)))))
(confirm-launch-state
()
`(progn
(should (jupyter-alive-p kernel))
(should (jupyter-kernel-spec kernel))
(should (jupyter-kernel-session kernel))
(should (process-live-p (jupyter-process kernel))))))
(let ((kernel (jupyter-kernel-process
:spec (jupyter-guess-kernelspec "python"))))
(confirm-shutdown-state)
(jupyter-launch kernel)
(confirm-launch-state)
(jupyter-do-shutdown kernel)
(confirm-shutdown-state))))
;; (let (called)
;; (let* ((plist '(:argv ["sleep" "60"] :env nil :interrupt_mode "signal"))
;; (kernel (jupyter-kernel
;; :spec (make-jupyter-kernelspec
;; :name "sleep"
;; :plist plist))))
;; (let ((jupyter-long-timeout 0.01))
;; (jupyter-launch kernel))
;; (cl-letf (((symbol-function #'interrupt-process)
;; (lambda (&rest args)
;; (setq called t))))
;; (jupyter-do-interrupt kernel))
;; (should called)
;; (setq called nil)
;; (jupyter-do-shutdown kernel)))
)
;; (let ((kernel (jupyter--kernel-process
;; :spec (jupyter-guess-kernelspec "python"))))
;; (ert-info ("Session set after kernel starts")
;; (should-not (jupyter-kernel-alive-p kernel))
;; (jupyter-start-kernel kernel)
;; (should (jupyter-kernel-alive-p kernel))
;; (should (oref kernel session))
;; (jupyter-kill-kernel kernel)
;; (should-not (jupyter-kernel-alive-p kernel)))
;; (ert-info ("Can we communicate?")
;; (let ((manager (jupyter-kernel-manager :kernel kernel)))
;; (jupyter-start-kernel manager)
;; (unwind-protect
;; (let ((jupyter-current-client
;; (jupyter-make-client manager 'jupyter-kernel-client)))
;; (jupyter-start-channels jupyter-current-client)
;; (unwind-protect
;; (progn
;; (jupyter-wait-until-startup jupyter-current-client)
;; (should (equal (jupyter-eval "1 + 1") "2")))
;; (jupyter-stop-channels jupyter-current-client)))
;; (jupyter-shutdown-kernel manager)))))
(ert-deftest jupyter-delete-connection-files ()
:tags '(kernel process)
(let ((jupyter--kernel-processes
(cl-loop repeat 2
collect (list nil (make-temp-file "jupyter-test")))))
(jupyter-delete-connection-files)
(should-not
(cl-loop for (_ conn-file) in jupyter--kernel-processes
thereis (file-exists-p conn-file)))))
(ert-deftest jupyter-kernel-process/connection-file-management ()
:tags '(kernel process)
(let (jupyter--kernel-processes)
(pcase-let ((`(,kernelA ,kernelB)
(cl-loop
repeat 2
collect (jupyter-kernel :spec "python"))))
(jupyter-launch kernelA)
(should (= (length jupyter--kernel-processes) 1))
(unwind-protect
(pcase-let* ((`(,processA ,conn-fileA) (car jupyter--kernel-processes))
(process-bufferA (process-buffer processA)))
(should (eq processA (jupyter-kernel-process-process kernelA)))
(jupyter-do-shutdown kernelA)
(should-not (process-live-p processA))
(should (file-exists-p conn-fileA))
(should (buffer-live-p process-bufferA))
(jupyter-launch kernelB)
(should-not (buffer-live-p process-bufferA))
(should-not (file-exists-p conn-fileA))
(should (= (length jupyter--kernel-processes) 1))
(pcase-let ((`(,processB ,conn-fileB)
(car jupyter--kernel-processes)))
(should-not (eq processA processB))
(should-not (string= conn-fileA conn-fileB))))
(jupyter-do-shutdown kernelA)
(jupyter-do-shutdown kernelB)))))
(ert-deftest jupyter-kernel-process/on-unexpected-exit ()
:tags '(kernel process)
(skip-unless nil)
(let ((kernel (jupyter-kernel :spec "python"))
called)
(jupyter-launch
kernel (lambda (kernel)
(setq called t)))
(let ((process (jupyter--kernel-process kernel)))
(kill-process process)
(sleep-for 0.01)
(should called))))
(ert-deftest jupyter-session-with-random-ports ()
:tags '(kernel)
(let ((session (jupyter-session-with-random-ports)))
(should (jupyter-session-p session))
(let ((process-exists
(cl-loop
for p in (process-list)
thereis (string= (process-name p)
"jupyter-session-with-random-ports"))))
(should-not process-exists))
(cl-destructuring-bind (&key hb_port stdin_port
control_port shell_port iopub_port
&allow-other-keys)
(jupyter-session-conn-info session)
;; Verify the ports are open for us
(cl-loop
for port in (list hb_port stdin_port
control_port shell_port iopub_port)
for proc = (make-network-process
:name "jupyter-test"
:server t
:host "127.0.0.1"
:service port)
do (delete-process proc)))))
(ert-deftest jupyter-expand-environment-variables ()
:tags '(kernel)
(let ((process-environment
@ -582,6 +779,89 @@
;;; Client
;; TODO: Different values of the session argument
;;
;; FIXME: Re-work after refactoring the kernelspec -> connectable kernel code paths.
(ert-deftest jupyter-comm-initialize ()
:tags '(client init)
(skip-unless nil)
(jupyter-test-with-python-client client
(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)
(let* ((conn-info '(:kernel_name "python"
:transport "tcp" :ip "127.0.0.1"
:signature_scheme "hmac-sha256"
:key "00a2cadb-3da7-45d2-b394-dbd01b5f80eb"
:hb_port 45473 :stdin_port 40175
:control_port 36301
:shell_port 39263 :iopub_port 36731))
(conn-file (jupyter-write-connection-file
(jupyter-session
:conn-info conn-info))))
(should (file-exists-p conn-file))
(should (string= (file-name-directory conn-file) (jupyter-runtime-directory)))
(should (equal (jupyter-read-plist conn-file) conn-info))))
;; FIXME: Revisit after transition
(ert-deftest jupyter-client-channels ()
:tags '(client channels)
(skip-unless nil)
(ert-info ("Starting/stopping channels")
;; FIXME: Without a new client, I'm getting
;;
;; (zmq-EFSM "Operation cannot be accomplished in current state")
;;
;; on the `jupyter-connect-repl' test pretty consistently.
(let ((jupyter-test-with-new-client t))
(jupyter-test-with-python-client client
(jupyter-stop-channels client)
(cl-loop
for channel in '(:hb :shell :iopub :stdin)
for alive-p = (jupyter-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-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-alive-p client channel)
do (should-not alive-p))))))
(ert-deftest jupyter-inhibited-handlers ()
:tags '(client handlers)
(jupyter-test-with-python-client client
@ -658,6 +938,182 @@
(should (jupyter-request-idle-p req))
(should (null jupyter-test-idle-sync-hook)))))
;;; 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 #'ignore)
(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'.")
(defun jupyter-test-ioloop-start (ioloop)
(jupyter-ioloop-start
ioloop (lambda (event)
(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-test-ioloop-start ioloop)
(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-test-ioloop-start ioloop)
(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-test-ioloop-start ioloop)
(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-test-ioloop-start ioloop)
(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-test-ioloop-start ioloop)
(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-test-ioloop-start ioloop)
(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-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-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)
(jupyter-test-with-python-client client
(let ((jupyter-current-client client))
(jupyter-send client :execute-request :code "1 + 1")
(jupyter-send client :execute-request :code "1 + 1")
(jupyter-send client :execute-request :code "1 + 1")
(let ((req (jupyter-send client :execute-request :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

@ -26,6 +26,9 @@
;;; Code:
(require 'zmq)
(require 'jupyter-zmq-channel-ioloop)
(require 'jupyter-kernel-process)
(require 'jupyter-repl)
(require 'jupyter-server)
(require 'jupyter-org-client)
@ -258,6 +261,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."
@ -386,6 +412,29 @@ message contents."
(jupyter-test-wait-until-idle-repl
jupyter-current-client))
(defun jupyter-test-conn-info-plist ()
"Return a connection info plist suitable for testing."
(let* ((ports
(cl-loop
with ports = (jupyter-available-local-ports 5)
for c in '(:shell :hb :iopub :stdin :control)
collect c and collect (pop ports))))
`(:shell_port
,(plist-get ports :shell)
:key "8671b7e4-5656e6c9d24edfce81916780"
:hb_port
,(plist-get ports :hb)
:kernel_name "python"
:control_port
,(plist-get ports :control)
:signature_scheme "hmac-sha256"
:ip "127.0.0.1"
:stdin_port
,(plist-get ports :stdin)
:transport "tcp"
:iopub_port
,(plist-get ports :iopub))))
(defun jupyter-test-text-has-property (prop val &optional positions)
"Ensure PROP has VAL for text at POSITIONS.
It is an error if any text not at POSITIONS has PROP. A nil value
@ -592,6 +641,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
@ -601,6 +658,12 @@ see the documentation on the --NotebookApp.password argument."
(cl-loop
for client in (jupyter-all-objects 'jupyter--clients)
do (ignore-errors (jupyter-shutdown-kernel client)))
(ignore-errors (delete-process (car jupyter-test-notebook)))))
(ignore-errors (delete-process (car jupyter-test-notebook)))
(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