emacs-jupyter/test/test-helper.el

581 lines
22 KiB
EmacsLisp
Raw Normal View History

2018-11-14 13:15:29 -06:00
;;; test-helper.el --- Helpers for jupyter-test.el -*- lexical-binding: t -*-
;; Copyright (C) 2018 Nathaniel Nicandro
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
;; Created: 15 Nov 2018
2019-07-24 15:23:04 -05:00
;; Version: 0.8.1
2018-11-14 13:15:29 -06:00
;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
2019-05-31 09:44:39 -05:00
;; published by the Free Software Foundation; either version 3, or (at
2018-11-14 13:15:29 -06:00
;; 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-client)
(require 'jupyter-repl)
Make `jupyter-channel-ioloop` independent of `zmq` This change localizes all `zmq` related functionality to `jupyter-ioloop` and `jupyter-zmq-*` files. * jupyter-channel-ioloop-comm.el: Add better commentary. (jupyter-base): Require. (jupyter-channel-ioloop-comm): Add `ioloop-class` slot (initialize-instance [jupyter-channel-ioloop-comm]): Use it. * jupyter-channel-ioloop.el (jupyter-base, jupyter-zmq-channel): Un-require. (jupyter-ioloop-session, jupyter-ioloop-channels): Rename to `jupyter-channel-ioloop-session` `jupyter-channel-ioloop-channels` and update all callers. (jupyter-channel-ioloop): Make into an abstract class. (initialize-instance [jupyter-channel-ioloop]): Re-add `jupyter-channel-ioloop-add-send-event`. Don't add to `jupyter-ioloop-post-hook`. (jupyter-channel-ioloop-recv-messages): Remove. (jupyter-channel-ioloop--set-session, jupyter-ioloop-start) (jupyter-channel-ioloop-add-send-event): Doc changes. (jupyter-channel-ioloop-add-start-channel-event) (jupyter-channel-ioloop-add-stop-channel-event): Don't add/remove from the `jupyter-ioloop-poller`. Now expected to be handled in the `jupyter-channel` subclass. Update documentation. In addition, for the start-channel event, do not attempt to add a channel if one doesn't already exist. * jupyter-ioloop.el (jupyter-ioloop-add-teardown): Remove mention of `jupyter-channel-ioloop` behavior. (jupyter-ioloop-add-arg-type): Update example variable. (jupyter-ioloop-environment-p): New function. * jupyter-kernel-manager.el (jupyter-channel): Require. (jupyter-make-client): Require and use `jupyter-zmq-channel-ioloop`. (jupyter-start-channels): Use `make-instance`. (jupyter-interrupt-kernel): Remove `condition-case`. Not needed since preventing socket blocking is now handled by `jupyter-recv`. * jupyter-repl.el (jupyter-connect-repl): Require and use `jupyter-zmq-channel-ioloop`. * jupyter-zmq-channel-ioloop.el: New file. * jupyter-zmq-channel.el (jupyter-ioloop-poller-remove) (jupyter-ioloop-poller-add): New declares. (jupyter-start-channel): Add to `jupyter-ioloop-poller` when in `jupyter-ioloop-environment-p`. (jupyter-stop-channel): Only disconnect the socket from its endpoint instead of closing it, leave that up to garbage collection. Remove from `jupyter-ioloop-poller` when in `jupyter-ioloop-environment-p`. (jupyter-recv): Handle non-blocking. * test/jupyter-test.el (jupyter-zmq-channel): Use non-blocking `zmq-send` since socket is no longer closed when calling `jupyter-stop-channel`. (jupyter-ioloop-test-eval-ioloop): Rename to `jupyter-test-ioloop-eval-event`, update all callers, and move to `test/test-helper.el`. (jupyter-channel-ioloop-send-event, jupyter-channel-ioloop-stop-channel-event) (jupyter-channel-ioloop-start-channel-event): Fix tests for variable name changes. Use `jupyter-test-channel-ioloop`. Update `jupyter-ioloop-poller`, addition/removal from poller is now done in the `jupyter-channel` subclass by checking `jupyter-ioloop-environment-p`. * test/test-helper.el (jupyter-zmq-channel-ioloop): Require. (initialize-instance [jupyter-echo-client]): Use it. (jupyter-test-channel-ioloop): New macro. (jupyter-test-ioloop-eval-event): New function.
2019-06-28 20:44:13 -05:00
(require 'jupyter-zmq-channel-ioloop)
Do not depend strongly on zmq Having the `jupyter-comm-layer` abstraction means we do not need to do so. * jupyter-base.el (zmq): Un-require. (jupyter-socket-types): Move to `jupyter-channels.el`. (jupyter-session): Don't mention zmq in doc string. (jupyter-available-local-ports, jupyter-make-ssh-tunnel): New functions. (jupyter-tunnel-connection): Use them. * jupyter-channel-ioloop-comm.el: New file. * jupyter-channels.el (jupyter-messages): Un-require. (jupyter-comm-layer, zmq): New requires. (jupyter-socket-types): Moved from `jupyter-base.el`. (jupyter-send, jupyter-recv): Implementations for `jupyter-session` moved from `jupyter-messages.el`. (jupyter-sync-channel-comm): `jupyter-comm-layer` implementation for `jupyter-sync-channel` objects moved from `jupyter-comm-layer.el`. * jupyter-comm-layer.el (jupyter-channel-ioloop): Un-require. (jupyter-sync-channel-comm): Move implementation to `jupyter-channels.el`. (jupyter-ioloop-comm): Move implementation to new file `jupyter-ioloop-comm.el`. (jupyter-channel-ioloop-comm): Move implementation to new file `jupyter-channel-ioloop-comm.el`. * jupyter-ioloop-comm.el: New file. * jupyter-ioloop.el (zmq): Require. * jupyter-kernel-manager.el (jupyter-make-client): Ensure `jupyter-channel-ioloop-comm` is required. * jupyter-messages.el (jupyter-send) (jupyter-recv): Moved to `jupyter-channels.el` * jupyter-repl.el (jupyter-connect-repl): Ensure `jupyter-channel-ioloop-comm` is required. * test/jupyter-test.el (jupyter-available-local-ports): New test. * test/test-helper.el (jupyter-channel-ioloop-comm): New require.
2019-06-28 20:03:00 -05:00
(require 'jupyter-channel-ioloop-comm)
2018-11-14 13:15:29 -06:00
(require 'jupyter-org-client)
(require 'jupyter-kernel-manager)
(require 'cl-lib)
(require 'ert)
;; Increase timeouts when testing for consistency. I think what is going on is
;; that communication with subprocesses gets slowed down when many processes
;; are being open and closed? The kernel processes are cached so they are
;; re-used for the most part except for tests that explicitly start and stop a
;; process. Increasing these timeouts seemed to do the trick.
(when (or (getenv "APPVEYOR") (getenv "TRAVIS"))
(setq jupyter-long-timeout 120
jupyter-default-timeout 60))
2018-11-16 22:14:31 -06:00
(defvar jupyter-test-with-new-client nil
"Whether the global client for a kernel should be used for tests.
Let bind to a non-nil value around a call to
`jupyter-test-with-kernel-client' or `jupyter-test-with-kernel-repl' to
2018-11-16 22:14:31 -06:00
start a new kernel REPL instead of re-using one.")
(defvar jupyter-test-temporary-directory-name "jupyter")
(defvar jupyter-test-temporary-directory
(make-temp-file jupyter-test-temporary-directory-name 'directory)
"The directory where temporary processes/files will start or be written to.")
(make-directory (expand-file-name "tmp" jupyter-test-temporary-directory))
(message "system-configuration %s" system-configuration)
(add-hook
'kill-emacs-hook
(lambda ()
(ignore-errors (delete-directory jupyter-test-temporary-directory))))
2018-11-14 13:15:29 -06:00
;;; `jupyter-echo-client'
(defclass jupyter-echo-client (jupyter-kernel-client)
((messages))
:documentation "A client that echo's any messages sent back to
the channel the message was sent on. No communication is actually
done with a kernel. Every sent message on a channel is just
directly sent back to the handler method. The message flow when
handling a message is always
- status: busy
- reply message
- status: idle")
(cl-defmethod initialize-instance ((client jupyter-echo-client) &optional _slots)
2018-11-14 13:15:29 -06:00
(cl-call-next-method)
(oset client messages (make-ring 10))
Make `jupyter-channel-ioloop` independent of `zmq` This change localizes all `zmq` related functionality to `jupyter-ioloop` and `jupyter-zmq-*` files. * jupyter-channel-ioloop-comm.el: Add better commentary. (jupyter-base): Require. (jupyter-channel-ioloop-comm): Add `ioloop-class` slot (initialize-instance [jupyter-channel-ioloop-comm]): Use it. * jupyter-channel-ioloop.el (jupyter-base, jupyter-zmq-channel): Un-require. (jupyter-ioloop-session, jupyter-ioloop-channels): Rename to `jupyter-channel-ioloop-session` `jupyter-channel-ioloop-channels` and update all callers. (jupyter-channel-ioloop): Make into an abstract class. (initialize-instance [jupyter-channel-ioloop]): Re-add `jupyter-channel-ioloop-add-send-event`. Don't add to `jupyter-ioloop-post-hook`. (jupyter-channel-ioloop-recv-messages): Remove. (jupyter-channel-ioloop--set-session, jupyter-ioloop-start) (jupyter-channel-ioloop-add-send-event): Doc changes. (jupyter-channel-ioloop-add-start-channel-event) (jupyter-channel-ioloop-add-stop-channel-event): Don't add/remove from the `jupyter-ioloop-poller`. Now expected to be handled in the `jupyter-channel` subclass. Update documentation. In addition, for the start-channel event, do not attempt to add a channel if one doesn't already exist. * jupyter-ioloop.el (jupyter-ioloop-add-teardown): Remove mention of `jupyter-channel-ioloop` behavior. (jupyter-ioloop-add-arg-type): Update example variable. (jupyter-ioloop-environment-p): New function. * jupyter-kernel-manager.el (jupyter-channel): Require. (jupyter-make-client): Require and use `jupyter-zmq-channel-ioloop`. (jupyter-start-channels): Use `make-instance`. (jupyter-interrupt-kernel): Remove `condition-case`. Not needed since preventing socket blocking is now handled by `jupyter-recv`. * jupyter-repl.el (jupyter-connect-repl): Require and use `jupyter-zmq-channel-ioloop`. * jupyter-zmq-channel-ioloop.el: New file. * jupyter-zmq-channel.el (jupyter-ioloop-poller-remove) (jupyter-ioloop-poller-add): New declares. (jupyter-start-channel): Add to `jupyter-ioloop-poller` when in `jupyter-ioloop-environment-p`. (jupyter-stop-channel): Only disconnect the socket from its endpoint instead of closing it, leave that up to garbage collection. Remove from `jupyter-ioloop-poller` when in `jupyter-ioloop-environment-p`. (jupyter-recv): Handle non-blocking. * test/jupyter-test.el (jupyter-zmq-channel): Use non-blocking `zmq-send` since socket is no longer closed when calling `jupyter-stop-channel`. (jupyter-ioloop-test-eval-ioloop): Rename to `jupyter-test-ioloop-eval-event`, update all callers, and move to `test/test-helper.el`. (jupyter-channel-ioloop-send-event, jupyter-channel-ioloop-stop-channel-event) (jupyter-channel-ioloop-start-channel-event): Fix tests for variable name changes. Use `jupyter-test-channel-ioloop`. Update `jupyter-ioloop-poller`, addition/removal from poller is now done in the `jupyter-channel` subclass by checking `jupyter-ioloop-environment-p`. * test/test-helper.el (jupyter-zmq-channel-ioloop): Require. (initialize-instance [jupyter-echo-client]): Use it. (jupyter-test-channel-ioloop): New macro. (jupyter-test-ioloop-eval-event): New function.
2019-06-28 20:44:13 -05:00
(oset client kcomm (jupyter-channel-ioloop-comm
:ioloop-class 'jupyter-zmq-channel-ioloop))
Generalize communication with a kernel The previous mechanism to communicate with a kernel was too low level from the perspective of a client. The client interfaced directly with the subprocess abstraction, `jupyter-ioloop`, and had to handle all "events" that occurred in the `jupyter-ioloop`, e.g. when a channel was started or stopped. But in reality such events should not be the concern of a client. A client should only care about events that are directly related to kernel messages and not events related to the implementation details of *how* communication occurs. This commit abstracts out the way in which a client communicates with its kernel by introducing a new `jupyter-comm-layer` class. The `jupyter-comm-layer` class takes care of managing the communication channel between a kernel and its clients as well as sending events to all registered clients. This way, clients operate solely at the level of events on the communication layer. All a client does is register itself to receive events on the communication layer and send events on the layer. * jupyter-base.el (jupyter-session-endpoints): New function. * jupyter-client.el (jupyter-kernel-client): Remove ioloop and channels slots. Add kcomm slot. (initialize-instance): Unconditionally stop channels. (jupyter-initialize-connection): Change into a method call. Call `jupyter-initialize-connection` on the `kcomm` slot. (jupyter-with-client-buffer): Remove stale comment. (jupyter-send): Call `jupyter-send` on the `kcomm` slot. (jupyter-ioloop-handler): Remove all method definitions, replace `sent` and `message` methods with their `jupyter-event-handler` equivalents. (jupyter-hb-pause, jupyter-hb-unpause, jupyter-hb-beating): (jupyter-channel-alive-p, jupyter-start-channel, jupyter-stop-channel): (jupyter-start-channels, jupyter-stop-channels): Replace with calls to their equivalents using the `kcomm` slot. * jupyter-comm-layer.el: New file. * jupyter-kernel-manager (jupyter-make-client): Set a client's `kcomm` slot to `jupyter-channel-ioloop-comm`. * jupyter-messages.el (jupyter-decode-message): Use `list` directly. There seemed to be issues when using the new `jupyter-sync-channel-comm` due to using quoted lists. * test/jupyter-test.el: Add `jupyter-comm-layer` test. Update other tests. * test/test-helper.el: Add `jupyter-comm-layer` mock objects. Update `jupyter-echo-client`.
2019-04-08 11:42:00 -05:00
(with-slots (kcomm) client
(oset kcomm hb (jupyter-hb-channel))
(oset kcomm stdin (make-jupyter-proxy-channel))
(oset kcomm shell (make-jupyter-proxy-channel))
(oset kcomm iopub (make-jupyter-proxy-channel))))
2018-11-14 13:15:29 -06:00
(cl-defmethod jupyter-send ((client jupyter-echo-client)
channel
type
message
&optional _flags)
(let ((req (jupyter-request :id (jupyter-new-uuid))))
2018-11-14 13:15:29 -06:00
(if (string-match "request" (symbol-name type))
(setq type (intern (replace-match "reply" nil nil (symbol-name type))))
(error "Not a request message type (%s)" type))
;; Message flow
;; - status: busy
;; - reply message
;; - status: idle
;;
;; Needed internally by a `jupyter-kernel-client', this is mainly handled
;; by the eventloop.
(puthash (jupyter-request-id req) req (oref client requests))
;; Simulate a delay
(run-at-time
0.001 nil
(lambda ()
(jupyter-handle-message
client :iopub (jupyter-test-message req :status (list :execution_state "busy")))
(jupyter-handle-message client channel (jupyter-test-message req type message))
(jupyter-handle-message
client :iopub (jupyter-test-message req :status (list :execution_state "idle")))))
req))
(cl-defmethod jupyter-handle-message ((client jupyter-echo-client) _channel msg)
(ring-insert+extend (oref client messages) msg 'grow)
(cl-call-next-method))
Generalize communication with a kernel The previous mechanism to communicate with a kernel was too low level from the perspective of a client. The client interfaced directly with the subprocess abstraction, `jupyter-ioloop`, and had to handle all "events" that occurred in the `jupyter-ioloop`, e.g. when a channel was started or stopped. But in reality such events should not be the concern of a client. A client should only care about events that are directly related to kernel messages and not events related to the implementation details of *how* communication occurs. This commit abstracts out the way in which a client communicates with its kernel by introducing a new `jupyter-comm-layer` class. The `jupyter-comm-layer` class takes care of managing the communication channel between a kernel and its clients as well as sending events to all registered clients. This way, clients operate solely at the level of events on the communication layer. All a client does is register itself to receive events on the communication layer and send events on the layer. * jupyter-base.el (jupyter-session-endpoints): New function. * jupyter-client.el (jupyter-kernel-client): Remove ioloop and channels slots. Add kcomm slot. (initialize-instance): Unconditionally stop channels. (jupyter-initialize-connection): Change into a method call. Call `jupyter-initialize-connection` on the `kcomm` slot. (jupyter-with-client-buffer): Remove stale comment. (jupyter-send): Call `jupyter-send` on the `kcomm` slot. (jupyter-ioloop-handler): Remove all method definitions, replace `sent` and `message` methods with their `jupyter-event-handler` equivalents. (jupyter-hb-pause, jupyter-hb-unpause, jupyter-hb-beating): (jupyter-channel-alive-p, jupyter-start-channel, jupyter-stop-channel): (jupyter-start-channels, jupyter-stop-channels): Replace with calls to their equivalents using the `kcomm` slot. * jupyter-comm-layer.el: New file. * jupyter-kernel-manager (jupyter-make-client): Set a client's `kcomm` slot to `jupyter-channel-ioloop-comm`. * jupyter-messages.el (jupyter-decode-message): Use `list` directly. There seemed to be issues when using the new `jupyter-sync-channel-comm` due to using quoted lists. * test/jupyter-test.el: Add `jupyter-comm-layer` test. Update other tests. * test/test-helper.el: Add `jupyter-comm-layer` mock objects. Update `jupyter-echo-client`.
2019-04-08 11:42:00 -05:00
;;; `jupyter-mock-comm-layer'
(defclass jupyter-mock-comm-layer (jupyter-comm-layer
jupyter-comm-autostop)
Generalize communication with a kernel The previous mechanism to communicate with a kernel was too low level from the perspective of a client. The client interfaced directly with the subprocess abstraction, `jupyter-ioloop`, and had to handle all "events" that occurred in the `jupyter-ioloop`, e.g. when a channel was started or stopped. But in reality such events should not be the concern of a client. A client should only care about events that are directly related to kernel messages and not events related to the implementation details of *how* communication occurs. This commit abstracts out the way in which a client communicates with its kernel by introducing a new `jupyter-comm-layer` class. The `jupyter-comm-layer` class takes care of managing the communication channel between a kernel and its clients as well as sending events to all registered clients. This way, clients operate solely at the level of events on the communication layer. All a client does is register itself to receive events on the communication layer and send events on the layer. * jupyter-base.el (jupyter-session-endpoints): New function. * jupyter-client.el (jupyter-kernel-client): Remove ioloop and channels slots. Add kcomm slot. (initialize-instance): Unconditionally stop channels. (jupyter-initialize-connection): Change into a method call. Call `jupyter-initialize-connection` on the `kcomm` slot. (jupyter-with-client-buffer): Remove stale comment. (jupyter-send): Call `jupyter-send` on the `kcomm` slot. (jupyter-ioloop-handler): Remove all method definitions, replace `sent` and `message` methods with their `jupyter-event-handler` equivalents. (jupyter-hb-pause, jupyter-hb-unpause, jupyter-hb-beating): (jupyter-channel-alive-p, jupyter-start-channel, jupyter-stop-channel): (jupyter-start-channels, jupyter-stop-channels): Replace with calls to their equivalents using the `kcomm` slot. * jupyter-comm-layer.el: New file. * jupyter-kernel-manager (jupyter-make-client): Set a client's `kcomm` slot to `jupyter-channel-ioloop-comm`. * jupyter-messages.el (jupyter-decode-message): Use `list` directly. There seemed to be issues when using the new `jupyter-sync-channel-comm` due to using quoted lists. * test/jupyter-test.el: Add `jupyter-comm-layer` test. Update other tests. * test/test-helper.el: Add `jupyter-comm-layer` mock objects. Update `jupyter-echo-client`.
2019-04-08 11:42:00 -05:00
((alive :initform nil)))
(cl-defmethod jupyter-comm-alive-p ((comm jupyter-mock-comm-layer))
(oref comm alive))
(cl-defmethod jupyter-comm-start ((comm jupyter-mock-comm-layer))
(unless (oref comm alive)
(oset comm alive 0))
(cl-incf (oref comm alive)))
(cl-defmethod jupyter-comm-stop ((comm jupyter-mock-comm-layer))
(cl-decf (oref comm alive))
(when (zerop (oref comm alive))
(oset comm alive nil)))
(cl-defstruct jupyter-mock-comm-obj event)
(cl-defmethod jupyter-event-handler ((obj jupyter-mock-comm-obj) event)
(setf (jupyter-mock-comm-obj-event obj) event))
2018-11-14 13:15:29 -06:00
;;; Macros
(cl-defmacro jupyter-ert-info ((message-form &key ((:prefix prefix-form) "Info: "))
&body body)
"Identical to `ert-info', but clear the REPL buffer before running BODY.
In a REPL buffer, the contents are erased and an input prompt is
inserted.
If the `current-buffer' is not a REPL, this is identical to
`ert-info'."
(declare (debug ((form &rest [sexp form]) body))
(indent 1))
2018-11-16 22:14:31 -06:00
`(ert-info (,message-form :prefix (quote ,prefix-form))
2018-11-14 13:15:29 -06:00
;; Clear the REPL buffer before each new test section, but do this only if
;; the current client is a REPL client
(when (and jupyter-current-client
(object-of-class-p jupyter-current-client
'jupyter-repl-client)
(eq (current-buffer)
(oref jupyter-current-client buffer)))
(let ((inhibit-read-only t))
(erase-buffer)
(jupyter-test-repl-ret-sync)))
,@body))
(defmacro jupyter-test-at-temporary-directory (&rest body)
(declare (debug (&rest form)))
`(let ((default-directory jupyter-test-temporary-directory)
(temporary-file-directory jupyter-test-temporary-directory)
(tramp-cache-data (make-hash-table :test #'equal)))
,@body))
2018-11-14 13:15:29 -06:00
(defmacro jupyter-with-echo-client (client &rest body)
(declare (indent 1) (debug (symbolp &rest form)))
`(let ((,client (jupyter-echo-client)))
,@body))
2018-11-16 22:14:31 -06:00
(defvar jupyter-test-global-clients nil)
(defvar jupyter-test-global-repls nil)
(defmacro jupyter-test-with-client-cache (client-fun saved-sym kernel client &rest body)
(declare (indent 4) (debug (functionp symbolp stringp symbolp &rest form)))
(let ((spec (make-symbol "spec"))
(saved (make-symbol "saved")))
`(progn
;; If a kernel has died, e.g. being shutdown, remove it.
(cl-loop
for saved in (copy-sequence ,saved-sym)
for client = (cdr saved)
when (and client (slot-boundp client 'manager)
(not (jupyter-kernel-alive-p (oref client manager))))
do (jupyter-stop-channels client)
(cl-callf2 delq saved ,saved-sym))
(let* ((,spec (progn (jupyter-error-if-no-kernelspec ,kernel)
(car (jupyter-find-kernelspecs ,kernel))))
(,saved (cdr (assoc (car ,spec) ,saved-sym)))
(,client (if (and ,saved (not jupyter-test-with-new-client))
,saved
;; Want a fresh kernel, so shutdown the cached one
(when ,saved
(if (slot-boundp ,saved 'manager)
(jupyter-shutdown-kernel (oref ,saved manager))
(jupyter-send-shutdown-request ,saved))
(jupyter-stop-channels ,saved))
(let ((client (,client-fun (car ,spec))))
(prog1 client
(let ((el (cons (car ,spec) client)))
(push el ,saved-sym)))))))
;; See the note about increasing timeouts during CI testing at the top
;; of jupyter-test.el
(accept-process-output nil 1)
,@body))))
(defmacro jupyter-test-with-kernel-client (kernel client &rest body)
2018-11-14 13:15:29 -06:00
"Start a new KERNEL client, bind it to CLIENT, evaluate BODY.
2018-11-16 22:14:31 -06:00
This only starts a single global client unless the variable
`jupyter-test-with-new-client' is non-nil."
2018-11-14 13:15:29 -06:00
(declare (indent 2) (debug (stringp symbolp &rest form)))
`(jupyter-test-with-client-cache
(lambda (name) (cadr (jupyter-start-new-kernel name)))
jupyter-test-global-clients ,kernel ,client
2019-05-07 18:22:28 -05:00
(unwind-protect
(progn ,@body)
(when jupyter-test-with-new-client
(jupyter-shutdown-kernel (oref client manager))))))
2018-11-14 13:15:29 -06:00
(defmacro jupyter-test-with-python-client (client &rest body)
2018-11-14 13:15:29 -06:00
"Start a new Python kernel, bind it to CLIENT, evaluate BODY."
(declare (indent 1) (debug (symbolp &rest form)))
`(jupyter-test-with-kernel-client "python" ,client
2018-11-14 13:15:29 -06:00
,@body))
(defmacro jupyter-test-with-kernel-repl (kernel client &rest body)
2018-11-14 13:15:29 -06:00
"Start a new KERNEL REPL, bind the client to CLIENT, evaluate BODY.
If `jupyter-test-with-new-client' is nil, any previously started
REPLs available will be re-used without starting a new one and no
cleanup of the REPL is done after evaluating BODY.
When `jupyter-test-with-new-client' is non-nil, a fresh REPL is
started and the REPL deleted after evaluating BODY."
2018-11-14 13:15:29 -06:00
(declare (indent 2) (debug (stringp symbolp &rest form)))
`(jupyter-test-with-client-cache
jupyter-run-repl jupyter-test-global-repls ,kernel ,client
(unwind-protect
(jupyter-with-repl-buffer ,client
(progn ,@body))
(cl-letf (((symbol-function 'yes-or-no-p)
(lambda (_prompt) t))
((symbol-function 'y-or-n-p)
(lambda (_prompt) t))
(jupyter-default-timeout 5))
(when jupyter-test-with-new-client
(kill-buffer (oref ,client buffer)))))))
2018-11-14 13:15:29 -06:00
(defmacro jupyter-test-with-python-repl (client &rest body)
2018-11-14 13:15:29 -06:00
"Start a new Python REPL and run BODY.
CLIENT is bound to the Python REPL. Delete the REPL buffer after
running BODY."
(declare (indent 1) (debug (symbolp &rest form)))
`(jupyter-test-with-kernel-repl "python" ,client
2018-11-14 13:15:29 -06:00
,@body))
Make `jupyter-channel-ioloop` independent of `zmq` This change localizes all `zmq` related functionality to `jupyter-ioloop` and `jupyter-zmq-*` files. * jupyter-channel-ioloop-comm.el: Add better commentary. (jupyter-base): Require. (jupyter-channel-ioloop-comm): Add `ioloop-class` slot (initialize-instance [jupyter-channel-ioloop-comm]): Use it. * jupyter-channel-ioloop.el (jupyter-base, jupyter-zmq-channel): Un-require. (jupyter-ioloop-session, jupyter-ioloop-channels): Rename to `jupyter-channel-ioloop-session` `jupyter-channel-ioloop-channels` and update all callers. (jupyter-channel-ioloop): Make into an abstract class. (initialize-instance [jupyter-channel-ioloop]): Re-add `jupyter-channel-ioloop-add-send-event`. Don't add to `jupyter-ioloop-post-hook`. (jupyter-channel-ioloop-recv-messages): Remove. (jupyter-channel-ioloop--set-session, jupyter-ioloop-start) (jupyter-channel-ioloop-add-send-event): Doc changes. (jupyter-channel-ioloop-add-start-channel-event) (jupyter-channel-ioloop-add-stop-channel-event): Don't add/remove from the `jupyter-ioloop-poller`. Now expected to be handled in the `jupyter-channel` subclass. Update documentation. In addition, for the start-channel event, do not attempt to add a channel if one doesn't already exist. * jupyter-ioloop.el (jupyter-ioloop-add-teardown): Remove mention of `jupyter-channel-ioloop` behavior. (jupyter-ioloop-add-arg-type): Update example variable. (jupyter-ioloop-environment-p): New function. * jupyter-kernel-manager.el (jupyter-channel): Require. (jupyter-make-client): Require and use `jupyter-zmq-channel-ioloop`. (jupyter-start-channels): Use `make-instance`. (jupyter-interrupt-kernel): Remove `condition-case`. Not needed since preventing socket blocking is now handled by `jupyter-recv`. * jupyter-repl.el (jupyter-connect-repl): Require and use `jupyter-zmq-channel-ioloop`. * jupyter-zmq-channel-ioloop.el: New file. * jupyter-zmq-channel.el (jupyter-ioloop-poller-remove) (jupyter-ioloop-poller-add): New declares. (jupyter-start-channel): Add to `jupyter-ioloop-poller` when in `jupyter-ioloop-environment-p`. (jupyter-stop-channel): Only disconnect the socket from its endpoint instead of closing it, leave that up to garbage collection. Remove from `jupyter-ioloop-poller` when in `jupyter-ioloop-environment-p`. (jupyter-recv): Handle non-blocking. * test/jupyter-test.el (jupyter-zmq-channel): Use non-blocking `zmq-send` since socket is no longer closed when calling `jupyter-stop-channel`. (jupyter-ioloop-test-eval-ioloop): Rename to `jupyter-test-ioloop-eval-event`, update all callers, and move to `test/test-helper.el`. (jupyter-channel-ioloop-send-event, jupyter-channel-ioloop-stop-channel-event) (jupyter-channel-ioloop-start-channel-event): Fix tests for variable name changes. Use `jupyter-test-channel-ioloop`. Update `jupyter-ioloop-poller`, addition/removal from poller is now done in the `jupyter-channel` subclass by checking `jupyter-ioloop-environment-p`. * test/test-helper.el (jupyter-zmq-channel-ioloop): Require. (initialize-instance [jupyter-echo-client]): Use it. (jupyter-test-channel-ioloop): New macro. (jupyter-test-ioloop-eval-event): New function.
2019-06-28 20:44:13 -05:00
(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))))))
(defun jupyter-test-ioloop-eval-event (ioloop event)
(eval
`(progn
,@(oref ioloop setup)
,(jupyter-ioloop--event-dispatcher ioloop event))))
(defmacro jupyter-test-rest-api (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."
(declare (indent 1))
`(progn
(defvar url-request-data)
(defvar url-request-method)
(defvar url-request-extra-headers)
(defvar url-http-end-of-headers)
(defvar url-http-response-status)
(defvar url-http-content-type)
(let (url-request-data
url-request-method
url-request-extra-headers
url-http-end-of-headers
url-http-content-type
(url-http-response-status 200)
(fun (lambda (url &rest _)
(setq url-http-end-of-headers (point-min))
,@check-forms
(current-buffer))))
(with-temp-buffer
(cl-letf (((symbol-function #'url-retrieve-synchronously) fun)
((symbol-function #'url-retrieve)
(lambda (url cb &optional cbargs &rest _)
(prog1
(funcall fun url)
(apply cb nil cbargs)))))
,bodyform)))))
2018-11-14 13:15:29 -06:00
;;; Functions
2018-11-24 22:08:26 -06:00
(defun jupyter-test-ipython-kernel-version (spec)
"Return the IPython kernel version string corresponding to SPEC.
Assumes that SPEC is a kernelspec for a Python kernel and
extracts the IPython kernel's semver."
(let* ((cmd (aref (plist-get spec :argv) 0))
(process-environment
(append
(cl-loop for (key val) on (plist-get spec :env) by #'cddr
collect (concat (substring (symbol-name key) 1) "=" val))
process-environment))
2019-05-02 18:50:00 -05:00
(version
(with-temp-buffer
(call-process cmd nil t nil
"-c" "import ipykernel; \
print(\"{}.{}.{}\".format(*ipykernel.version_info[:3]))")
(buffer-string))))
2018-11-24 22:08:26 -06:00
(string-trim version)))
2018-11-14 13:15:29 -06:00
(defun jupyter-error-if-no-kernelspec (kernel)
(prog1 kernel
(unless (car (jupyter-find-kernelspecs
(regexp-quote kernel)))
(error "Kernel not found (%s)" kernel))))
(defun jupyter-test-message (req type content)
"Return a bare bones message plist for REQ.
TYPE is the message type of the returned message. CONTENT is the
message contents."
(list :msg_id (jupyter-new-uuid)
:msg_type type
:parent_header (list :msg_id (jupyter-request-id req))
;; Add a dummy execution count since it is handled specially in
;; `jupyter-handle-message' to update the state of the client.
:content (append content (list :execution_count 0))))
2018-11-14 13:15:29 -06:00
(defun jupyter-test-wait-until-idle-repl (client)
"Wait until the execution state of a REPL CLIENT is idle."
(while (not (equal (jupyter-execution-state client) "idle"))
(sleep-for 0.01)))
(defun jupyter-test-repl-ret-sync ()
"A synchronous version of `jupyter-repl-ret'."
(jupyter-repl-ret)
2018-11-16 22:14:31 -06:00
;; Account for the multiple idle -> busy cycles that occurs from
;; `jupyter-repl-ret'
(sleep-for 0.2)
2018-11-14 13:15:29 -06:00
(jupyter-test-wait-until-idle-repl
jupyter-current-client))
2018-11-16 22:14:31 -06:00
(defun jupyter-test-conn-info-plist ()
"Return a connection info plist suitable for testing."
(let* ((ports (cl-loop
with sock = (zmq-socket (zmq-current-context) zmq-PUB)
for c in '(:shell :hb :iopub :stdin :control)
collect c and
collect (zmq-bind-to-random-port sock "tcp://127.0.0.1")
finally (zmq-close sock))))
`(: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
of POSITIONS means that all text from `point-min' to `point-max'
should have PROP with VAL."
(cl-loop
for i from (point-min) to (point-max)
if (or (null positions) (memq i positions))
do (should (equal (get-text-property i prop) val))
else do (should-not (get-text-property i prop))))
;;; `org-mode'
(defvar org-babel-load-languages)
(defvar org-confirm-babel-evaluate)
(defvar jupyter-org-test-session nil
"Name of the session for testing Jupyter source blocks.")
(defvar jupyter-org-test-buffer nil
"`org-mode' buffer for testing Jupyter source blocks.")
(defun jupyter-org-test-setup ()
(unless jupyter-org-test-session
(require 'org)
(setq org-babel-load-languages
'((python . t)
(jupyter . t)))
(setq org-confirm-babel-evaluate nil)
(setq jupyter-org-test-session (make-temp-name "ob-jupyter-test"))
(setq jupyter-org-test-buffer (get-buffer-create "ob-jupyter-test"))
(org-babel-do-load-languages
'org-babel-load-languages
org-babel-load-languages)
(with-current-buffer jupyter-org-test-buffer
(org-mode)
(insert
"#+BEGIN_SRC jupyter-python " ":session " jupyter-org-test-session "\n"
"#+END_SRC")
(setq jupyter-current-client
(with-current-buffer (org-babel-initiate-session)
jupyter-current-client))))
(with-current-buffer jupyter-org-test-buffer
(erase-buffer)))
(defmacro jupyter-org-test (&rest body)
(declare (debug (body)))
`(progn
(jupyter-org-test-setup)
(with-current-buffer jupyter-org-test-buffer
,@body)))
(defmacro jupyter-org-test-src-block (block expected-result &rest args)
"Test source code BLOCK.
EXPECTED-RESULT is a string that the source block's results
should match. ARGS is a plist of header arguments to be set for
the source code block. For example if ARGS is (:results \"raw\")
then the source code block will begin like
#+BEGIN_SRC jupyter-python :results raw ...
Note if ARGS contains a key, regexp, then if regexp is non-nil,
EXPECTED-RESULT is a regular expression to match against the
results instead of an equality match."
(let (regexp)
(setq args
(cl-loop for (arg val) on args by #'cddr
if (eq arg :regexp) do (setq regexp val)
else collect (cons arg val)))
`(jupyter-org-test
(jupyter-org-test-src-block-1 ,block ,expected-result ,regexp ',args))))
(defun jupyter-org-test-make-block (code args)
(let ((arg-str (mapconcat
(lambda (x)
(cl-destructuring-bind (name . val) x
(concat (symbol-name name) " " (format "%s" val))))
args " ")))
(concat
"#+BEGIN_SRC jupyter-python " arg-str " :session "
jupyter-org-test-session "\n"
code "\n"
"#+END_SRC")))
(defun jupyter-org-test-src-block-1 (code test-result &optional regexp args)
(let ((src-block (jupyter-org-test-make-block code args)))
(insert src-block)
(let* ((info (org-babel-get-src-block-info)))
(save-window-excursion
(org-babel-execute-src-block nil info))
(org-with-point-at (org-babel-where-is-src-block-result nil info)
(when (equal (alist-get :async args) "yes")
(jupyter-wait-until-idle
(jupyter-last-sent-request jupyter-current-client)))
(let ((element (org-element-context)))
;; Handle empty results with just a RESULTS keyword
;;
;; #+RESULTS:
(if (eq (org-element-type element) 'keyword) ""
(let ((result (buffer-substring-no-properties
(jupyter-org-element-begin-after-affiliated element)
(org-element-property :end element))))
(if regexp (should (string-match-p test-result result))
(message "\
Testing src-block:
%s
Expected result:
\"%s\"
Result:
\"%s\"
"
2019-02-22 08:30:00 -06:00
src-block test-result result)
(should (eq (compare-strings
result nil nil test-result nil nil
'ignore-case)
t))))))))))
;;; Notebook server
(defvar jupyter-test-notebook nil
"A cons cell (PROC . PORT).
PROC is the notebook process and PORT is the port it is connected
to.")
(defun jupyter-test-ensure-notebook-server ()
"Ensure there is a notebook process available.
Return the port it was started on. The starting directory of the
process will be in the `jupyter-test-temporary-directory'."
(if (process-live-p (car jupyter-test-notebook))
(cdr jupyter-test-notebook)
(unless noninteractive
(error "This should only be called in batch mode"))
(message "Starting up notebook process for tests")
(let* ((sock (zmq-socket (zmq-current-context) zmq-PUB))
(port (zmq-bind-to-random-port sock "tcp://127.0.0.1")))
(prog1 port
(zmq-unbind sock (zmq-get-option sock zmq-LAST-ENDPOINT))
(zmq-close sock)
(let ((default-directory jupyter-test-temporary-directory)
(buffer (generate-new-buffer "*jupyter-notebook*"))
(args (append
(list "notebook" "--no-browser" "--debug"
(format "--NotebookApp.port=%s" port))
(cond
((eq authentication t)
(list))
((stringp authentication)
(list
"--NotebookApp.token=''"
(format "--NotebookApp.password='%s'"
authentication)))
(t
(list
"--NotebookApp.token=''"
"--NotebookApp.password=''"))))))
(setq jupyter-test-notebook
(cons (start-process
"jupyter-notebook" (generate-new-buffer "*jupyter-notebook*")
"jupyter" "notebook"
"--no-browser"
"--NotebookApp.token=''"
"--NotebookApp.password=''"
(format "--NotebookApp.port=%s" port))
port))
(sleep-for 5))))))
(add-hook 'kill-emacs-hook
(lambda ()
(ignore-errors
(message "%s" (with-current-buffer
(process-buffer (car jupyter-test-notebook))
(buffer-string))))))
2018-11-14 13:15:29 -06:00
;;; test-helper.el ends here