mirror of
https://github.com/vale981/emacs-jupyter
synced 2025-03-06 07:51:39 -05:00
697 lines
31 KiB
EmacsLisp
697 lines
31 KiB
EmacsLisp
;;; jupyter-tests.el --- Jupyter tests -*- lexical-binding: t -*-
|
|
|
|
;; Copyright (C) 2018 Nathaniel Nicandro
|
|
|
|
;; Author: Nathaniel Nicandro <nathanielnicandro@gmail.com>
|
|
;; Created: 08 Jan 2018
|
|
;; Version: 0.0.1
|
|
;; X-URL: https://github.com/nathan/jupyter-tests
|
|
|
|
;; 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 2, 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 'jupyter-client)
|
|
(require 'jupyter-repl)
|
|
(require 'jupyter-kernel-manager)
|
|
(require 'cl-lib)
|
|
(require 'ert)
|
|
|
|
;; TODO: Required tests
|
|
;; - `jupyter-channels'
|
|
;; - `jupyter-messages'
|
|
;; - `jupyter-request' semantics
|
|
;; - Ensure `jupyter-request-idle-received-p' gets set
|
|
;; - Ensure `jupyter-request-run-handlers-p' actually prevents a handler from running
|
|
;; - IOLoop subprocess
|
|
;; - Make sure all commands that can be sent to the subprocess work
|
|
;; - Jupyter REPL
|
|
|
|
(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
|
|
placed back into the channel's recv-queue. This is mainly for
|
|
testing the callback functionality of a
|
|
`jupyter-kernel-client'.")
|
|
|
|
(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. The actual return value is a cons cell as
|
|
expected by `jupyter-queue-message'."
|
|
(cons "" (list :msg_id (jupyter-new-uuid)
|
|
:msg_type type
|
|
:parent_header (list :msg_id (jupyter-request-id req))
|
|
:content content)))
|
|
|
|
(cl-defmethod initialize-instance ((client jupyter-echo-client) &rest _slots)
|
|
(cl-call-next-method)
|
|
(oset client messages (make-ring 10))
|
|
(cl-loop
|
|
for channel in (list 'shell-channel
|
|
'iopub-channel
|
|
'hb-channel
|
|
'stdin-channel)
|
|
unless (slot-value client channel)
|
|
do (setf (slot-value client channel)
|
|
(make-instance
|
|
(intern (concat
|
|
"jupyter-"
|
|
(symbol-name channel)))
|
|
:endpoint "foo://bar"))))
|
|
|
|
(cl-defmethod jupyter-send ((client jupyter-echo-client)
|
|
channel
|
|
type
|
|
message
|
|
&optional _flags)
|
|
(let ((req (make-jupyter-request :-id (jupyter-new-uuid))))
|
|
(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
|
|
(jupyter-queue-message
|
|
(oref client iopub-channel)
|
|
(jupyter-test-message req :status (list :execution_state "busy")))
|
|
(jupyter-queue-message
|
|
channel (jupyter-test-message req type message))
|
|
(jupyter-queue-message
|
|
(oref client iopub-channel)
|
|
(jupyter-test-message req :status (list :execution_state "idle")))
|
|
(run-at-time
|
|
0.01 nil
|
|
(lambda (client channel)
|
|
(jupyter-handle-message client (oref client iopub-channel))
|
|
(jupyter-handle-message client channel)
|
|
(jupyter-handle-message client (oref client iopub-channel)))
|
|
client channel)
|
|
;; Needed internally by a `jupyter-kernel-client', this is mainly handled
|
|
;; by the eventloop.
|
|
(puthash (jupyter-request-id req) req (oref client requests))
|
|
req))
|
|
|
|
(cl-defmethod jupyter-handle-message ((client jupyter-echo-client) channel)
|
|
(ring-insert+extend (oref client messages)
|
|
(cdr (ring-ref (oref channel recv-queue) -1))
|
|
'grow)
|
|
(cl-call-next-method))
|
|
|
|
(cl-defmacro jupyter-test-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))
|
|
`(ert-info ((quote ,message-form) :prefix (quote ,prefix-form))
|
|
;; Clear the REPL buffer before each new test section, but do this only if
|
|
;; the current client is a REPL client
|
|
(when (and jupyter-repl-current-client
|
|
(obj-of-class-p jupyter-repl-current-client
|
|
'jupyter-repl-client)
|
|
(eq (current-buffer)
|
|
(oref jupyter-repl-current-client buffer)))
|
|
(let ((inhibit-read-only t))
|
|
(erase-buffer)
|
|
(jupyter-test-repl-ret-sync)))
|
|
,@body))
|
|
|
|
(defmacro jupyter-with-echo-client (client &rest body)
|
|
(declare (indent 1) (debug (symbolp &rest form)))
|
|
`(let ((,client (jupyter-echo-client)))
|
|
,@body))
|
|
|
|
(defmacro jupyter-with-python-client (client &rest body)
|
|
"Start a new Python kernel and run BODY.
|
|
CLIENT is bound to the Python client of the kernel. Cleanup the
|
|
client and delete the kernel process after running BODY."
|
|
(declare (indent 1) (debug (symbolp &rest form)))
|
|
(let ((manager (make-symbol "--manager")))
|
|
`(cl-destructuring-bind (,manager ,client _info)
|
|
(jupyter-start-new-kernel "python")
|
|
(unwind-protect
|
|
(progn ,@body)
|
|
(when (jupyter-kernel-alive-p ,manager)
|
|
(destructor ,manager))
|
|
(destructor ,client)))))
|
|
|
|
(defmacro jupyter-with-python-repl (client &rest body)
|
|
"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)))
|
|
`(let ((,client (run-jupyter-repl "python")))
|
|
(unwind-protect
|
|
(with-jupyter-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)))
|
|
(kill-buffer (oref client buffer))))))
|
|
|
|
(defun jupyter-test-wait-until-idle-repl (client)
|
|
"Wait until the execution state of a REPL CLIENT is idle."
|
|
(while (not (equal (oref client execution-state)
|
|
"idle"))
|
|
(sleep-for 0.01)))
|
|
|
|
(defun jupyter-test-repl-ret-sync ()
|
|
"A synchronous version of `jupyter-repl-ret'."
|
|
;; TODO: There are issues here since the kernel goes through multiple idle ->
|
|
;; busy cycles
|
|
(jupyter-repl-ret)
|
|
(jupyter-test-wait-until-idle-repl
|
|
jupyter-repl-current-client))
|
|
|
|
(ert-deftest jupyter-echo-client ()
|
|
(jupyter-with-echo-client client
|
|
(ert-info ("Mock echo client echo's messages back to channel.")
|
|
(let* ((msg (jupyter-message-execute-request :code "foo"))
|
|
(req (jupyter-send client (oref client shell-channel)
|
|
:execute-request msg)))
|
|
(sleep-for 1.0)
|
|
(setq msgs (nreverse (ring-elements (oref client messages))))
|
|
(should (= (length msgs) 3))
|
|
(should (equal (jupyter-message-type (first msgs)) :status))
|
|
(should (equal (jupyter-message-parent-id (first msgs))
|
|
(jupyter-request-id req)))
|
|
(should (equal (jupyter-message-get (first msgs) :execution_state) "busy"))
|
|
(should (equal (jupyter-message-type (second msgs)) :execute-reply))
|
|
(should (equal (jupyter-message-parent-id (second msgs))
|
|
(jupyter-request-id req)))
|
|
(should (equal (jupyter-message-content (second msgs)) msg))
|
|
(should (equal (jupyter-message-type (third msgs)) :status))
|
|
(should (equal (jupyter-message-parent-id (third msgs))
|
|
(jupyter-request-id req)))
|
|
(should (equal (jupyter-message-get (third msgs) :execution_state) "idle"))))))
|
|
|
|
(ert-deftest jupyter-callbacks ()
|
|
(jupyter-with-echo-client client
|
|
(let ((req (jupyter-send-execute-request client :code "foo")))
|
|
(ert-info ("Blocking callbacks")
|
|
(jupyter-wait-until-idle req)
|
|
(should (jupyter-request-idle-received-p req)))
|
|
(ert-info ("Error after idle message has been received")
|
|
(should-error (jupyter-add-callback req :status #'identity))))
|
|
(ert-info ("Multiple callbacks, same message type")
|
|
(let* ((callback-count 0)
|
|
(cb (lambda (msg)
|
|
(should (eq (jupyter-message-type msg) :status))
|
|
(setq callback-count (1+ callback-count))))
|
|
(req (jupyter-send-execute-request client :code "foo")))
|
|
(jupyter-add-callback req :status cb)
|
|
(jupyter-wait-until-idle req)
|
|
(should (= callback-count 2))))
|
|
(ert-info ("Adding callbacks, message type list")
|
|
(let* ((callback-count 0)
|
|
(cb (lambda (msg)
|
|
(setq callback-count (1+ callback-count))
|
|
(should (memq (jupyter-message-type msg)
|
|
'(:status :execute-reply)))))
|
|
(req (jupyter-send-execute-request client :code "foo")))
|
|
(jupyter-add-callback req '(:status :execute-reply) cb)
|
|
(jupyter-wait-until-idle req)
|
|
(should (= callback-count 3))))))
|
|
|
|
(ert-deftest jupyter-messages ()
|
|
(ert-info ("Splitting identities from messages")
|
|
(let ((msg (list "123" "323" jupyter-message-delimiter
|
|
"msg1" "msg2" "\0\0")))
|
|
(should (equal (jupyter--split-identities msg)
|
|
(cons (list "123" "323")
|
|
(list "msg1" "msg2" "\0\0"))))
|
|
(setq msg (list "123" "No" "delim" "in" "message"))
|
|
(should-error (jupyter--split-identities msg))))
|
|
(ert-info ("Creating message headers")
|
|
(let* ((session (jupyter-session :key (jupyter-new-uuid)))
|
|
(header (jupyter--message-header session :input-reply)))
|
|
(should (plist-get header :msg_id))
|
|
(should (plist-get header :date))
|
|
(should (eq (plist-get header :msg_type) :input-reply))
|
|
(should (string= (plist-get header :version) jupyter-protocol-version))
|
|
(should (string= (plist-get header :username) user-login-name))
|
|
(should (string= (plist-get header :session) (jupyter-session-id session)))))
|
|
(ert-info ("Encoding/decoding time strings")
|
|
(should (equal (jupyter--encode-time '(23385 27704 100000))
|
|
"2018-07-26T01:37:44.100000"))
|
|
(should (equal (jupyter--decode-time "2018-07-26T01:37:44.100")
|
|
'(23385 27704 100000 0)))
|
|
(should (equal (jupyter--decode-time "2018-07-26T01:37:44.10011122")
|
|
'(23385 27704 100111 0)))
|
|
(should (equal (jupyter--decode-time "2018-07-26T01:37:44")
|
|
'(23385 27704 0 0)))
|
|
(should (equal (jupyter--decode-time "2018-07-26")
|
|
'(23385 21840 0 0)))))
|
|
|
|
(ert-deftest jupyter-channels ()
|
|
(ert-info ("Channel types should match their class")
|
|
(should (eq (oref (jupyter-shell-channel) type) :shell))
|
|
(should (eq (oref (jupyter-stdin-channel) type) :stdin))
|
|
(should (eq (oref (jupyter-iopub-channel) type) :iopub))
|
|
(should (eq (oref (jupyter-hb-channel) type) :hb)))
|
|
(let ((channel (jupyter-sync-channel :endpoint "tcp://127.0.0.1:5555")))
|
|
(ert-info ("Starting a channel")
|
|
(should-not (jupyter-channel-alive-p channel))
|
|
(jupyter-start-channel channel :identity "foo")
|
|
(should (oref channel socket))
|
|
(should (equal (zmq-socket-get (oref channel socket) zmq-ROUTING-ID)
|
|
"foo"))
|
|
(should (jupyter-channel-alive-p channel)))
|
|
(ert-info ("Stopping a channel")
|
|
(ring-insert (oref channel recv-queue) "bar")
|
|
(let ((sock (oref channel socket)))
|
|
(jupyter-stop-channel channel)
|
|
(should-not (oref channel socket))
|
|
;; All pending messages are droped when a channel is stopped
|
|
(should (= (ring-length (oref channel recv-queue)) 0))
|
|
(should-not (jupyter-channel-alive-p channel))))
|
|
(ert-info ("Heartbeat channel")
|
|
(cl-flet ((start-hb () (zmq-start-process
|
|
(lambda (ctx)
|
|
(let ((sock (zmq-socket ctx zmq-REP)))
|
|
(zmq-bind sock "tcp://127.0.0.1:5556")
|
|
(while t
|
|
(zmq-send sock (zmq-recv sock))))))))
|
|
(let ((proc (start-hb))
|
|
(channel (jupyter-hb-channel :endpoint "tcp://127.0.0.1:5556")))
|
|
(unwind-protect
|
|
(progn
|
|
(ert-info ("Starting the channel")
|
|
(should-not (jupyter-channel-alive-p channel))
|
|
(jupyter-start-channel channel)
|
|
(should (jupyter-channel-alive-p channel))
|
|
(should (oref channel paused))
|
|
(jupyter-hb-unpause channel)
|
|
(sleep-for 2)
|
|
(should-not (oref channel paused))
|
|
(should (jupyter-hb-beating-p channel)))
|
|
(ert-info ("Pausing the channel")
|
|
(jupyter-hb-pause channel)
|
|
(oset channel beating 'beating)
|
|
(should (oref channel paused))
|
|
(sleep-for 1)
|
|
;; Channel shouldn't update when paused
|
|
(should (eq (oref channel beating) 'beating)))
|
|
(ert-info ("Checking the heartbeat")
|
|
(should (jupyter-hb-beating-p channel))
|
|
(delete-process proc)
|
|
(kill-buffer (process-buffer proc))
|
|
(sleep-for (* 2 (oref channel time-to-dead)))
|
|
(should-not (jupyter-hb-beating-p channel))
|
|
(setq proc (start-hb))
|
|
(sleep-for 1)
|
|
(should (jupyter-hb-beating-p channel)))
|
|
(ert-info ("Stopping the channel")
|
|
(let ((p (oref channel timer)))
|
|
(jupyter-stop-channel channel)
|
|
(should-not (memq p timer-list))
|
|
(should-not (oref channel timer)))))
|
|
(when (process-live-p proc)
|
|
(delete-process proc)
|
|
(kill-buffer (process-buffer proc)))
|
|
(when (jupyter-channel-alive-p channel)
|
|
(jupyter-stop-channel channel))))))))
|
|
|
|
(ert-deftest jupyter-client ()
|
|
(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)))
|
|
(conn-info `(: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)))
|
|
(client (jupyter-kernel-client)))
|
|
(ert-info ("`jupyter-initialize-connection'")
|
|
(jupyter-initialize-connection client conn-info)
|
|
(let ((session (oref client session)))
|
|
(should (string= (jupyter-session-key session)
|
|
(plist-get conn-info :key)))
|
|
(should (eq session (oref (oref client shell-channel) session)))
|
|
(should (eq session (oref (oref client stdin-channel) session)))
|
|
(should (eq session (oref (oref client iopub-channel) session)))
|
|
(should (eq session (oref (oref client hb-channel) session))))
|
|
(should (string= (oref (oref client shell-channel) endpoint)
|
|
(format "tcp://127.0.0.1:%d" (plist-get ports :shell))))
|
|
(should (string= (oref (oref client stdin-channel) endpoint)
|
|
(format "tcp://127.0.0.1:%d" (plist-get ports :stdin))))
|
|
(should (string= (oref (oref client iopub-channel) endpoint)
|
|
(format "tcp://127.0.0.1:%d" (plist-get ports :iopub))))
|
|
(should (string= (oref (oref client hb-channel) endpoint)
|
|
(format "tcp://127.0.0.1:%d" (plist-get ports :hb))))
|
|
(ert-info ("Re-initializing stops running channels")
|
|
(jupyter-start-channels client)
|
|
(should (jupyter-channels-running-p client))
|
|
(jupyter-initialize-connection client conn-info)
|
|
(should-not (jupyter-channels-running-p client))
|
|
(jupyter-start-channels client)))
|
|
(ert-info ("`jupyter-inhibit-handlers'")
|
|
(let* ((jupyter-inhibit-handlers '(:stream))
|
|
(req (jupyter-send-kernel-info-request client)))
|
|
(should (equal (jupyter-request-inhibited-handlers req)
|
|
'(:stream)))
|
|
(setq jupyter-inhibit-handlers '(:foo))
|
|
(should-error (jupyter-send-kernel-info-request client))))
|
|
(ert-info ("Destructor")
|
|
(should (buffer-live-p (oref client -buffer)))
|
|
(should (jupyter-channels-running-p client))
|
|
(destructor client)
|
|
(should-not (jupyter-channels-running-p client))
|
|
(should-not (buffer-live-p (oref client -buffer))))))
|
|
|
|
(ert-deftest jupyter-message-types ()
|
|
(jupyter-with-python-client client
|
|
(ert-info ("Kernel info")
|
|
(let ((res (jupyter-wait-until-received :kernel-info-reply
|
|
(jupyter-send-kernel-info-request client))))
|
|
(should res)
|
|
(should (json-plist-p res))
|
|
(should (eq (jupyter-message-type res) :kernel-info-reply))))
|
|
(ert-info ("Comm info")
|
|
(let ((res (jupyter-wait-until-received :comm-info-reply
|
|
(jupyter-send-comm-info-request client))))
|
|
(should-not (null res))
|
|
(should (json-plist-p res))
|
|
(should (eq (jupyter-message-type res) :comm-info-reply))))
|
|
(ert-info ("Execute")
|
|
(let ((res (jupyter-wait-until-received :execute-reply
|
|
(jupyter-send-execute-request client :code "y = 1 + 2"))))
|
|
(should-not (null res))
|
|
(should (json-plist-p res))
|
|
(should (eq (jupyter-message-type res) :execute-reply))))
|
|
(ert-info ("Input")
|
|
(cl-letf (((symbol-function 'read-from-minibuffer)
|
|
(lambda (_prompt &rest _args) "foo")))
|
|
(let ((res (jupyter-wait-until-received :execute-result
|
|
(jupyter-send-execute-request client :code "input('')"))))
|
|
(should-not (null res))
|
|
(should (json-plist-p res))
|
|
(should (eq (jupyter-message-type res) :execute-result))
|
|
(should (equal (jupyter-message-data data :text/plain) "'foo'")))))
|
|
(ert-info ("Inspect")
|
|
(let ((res (jupyter-wait-until-received :inspect-reply
|
|
(jupyter-send-inspect-request client
|
|
:code "list((1, 2, 3))"
|
|
:pos 2
|
|
:detail 0))))
|
|
(should-not (null res))
|
|
(should (json-plist-p res))
|
|
(should (eq (jupyter-message-type res) :inspect-reply))))
|
|
(ert-info ("Complete")
|
|
(let ((res (jupyter-wait-until-received :complete-reply
|
|
(jupyter-send-complete-request client
|
|
:code "foo = lis"
|
|
:pos 8))))
|
|
(should-not (null res))
|
|
(should (json-plist-p res))
|
|
(should (eq (jupyter-message-type res) :complete-reply))))
|
|
(ert-info ("History")
|
|
(let ((res (jupyter-wait-until-received :history-reply
|
|
(jupyter-send-history-request client
|
|
:hist-access-type "tail" :n 2))))
|
|
(should-not (null res))
|
|
(should (json-plist-p res))
|
|
(should (eq (jupyter-message-type res) :history-reply))))
|
|
(ert-info ("Is Complete")
|
|
(let ((res (jupyter-wait-until-received :is-complete-reply
|
|
(jupyter-send-is-complete-request client
|
|
:code "for i in range(5):"))))
|
|
(should-not (null res))
|
|
(should (json-plist-p res))
|
|
(should (eq (jupyter-message-type res) :is-complete-reply))))
|
|
(ert-info ("Shutdown")
|
|
(let ((res (jupyter-wait-until-received :shutdown-reply
|
|
(jupyter-send-shutdown-request client))))
|
|
(should-not (null res))
|
|
(should (json-plist-p res))
|
|
(should (eq (jupyter-message-type res) :shutdown-reply))))))
|
|
|
|
(ert-deftest jupyter-repl ()
|
|
(jupyter-with-python-repl client
|
|
(should (jupyter-repl-client-has-manager-p))
|
|
(should (jupyter-repl-connected-p))
|
|
(ert-info ("Replacing cell code")
|
|
(should (equal (jupyter-repl-cell-code) ""))
|
|
(jupyter-repl-replace-cell-code "1 + 1")
|
|
(should (equal (jupyter-repl-cell-code) "1 + 1"))
|
|
(jupyter-repl-replace-cell-code "foo\n bar")
|
|
(should (equal (jupyter-repl-cell-code) "foo\n bar"))
|
|
(jupyter-repl-replace-cell-code ""))
|
|
(ert-info ("Cell code position info")
|
|
(jupyter-repl-replace-cell-code "1 + 2")
|
|
(should (= (point) (point-max)))
|
|
(goto-char (1- (point)))
|
|
(should (= (char-after) ?2))
|
|
(should (= (jupyter-repl-cell-code-position) 5))
|
|
(goto-char (line-beginning-position))
|
|
(should (= (char-after) ?1))
|
|
(should (= (jupyter-repl-cell-code-position) 1)))
|
|
(ert-info ("`jupyter-repl-ret'")
|
|
(ert-info ("`point' before last cell in buffer")
|
|
(let ((tick (buffer-modified-tick)))
|
|
(goto-char (point-min))
|
|
(jupyter-test-repl-ret-sync)
|
|
(should (= (point) (point-max)))
|
|
(should (equal tick (buffer-modified-tick)))))
|
|
(ert-info ("No cells in buffer")
|
|
(let ((inhibit-read-only t))
|
|
(erase-buffer))
|
|
(jupyter-test-repl-ret-sync)
|
|
(should (get-text-property (- (point) 2) 'jupyter-cell))))))
|
|
|
|
(ert-deftest jupyter-repl-cell-motions ()
|
|
(jupyter-with-python-repl client
|
|
(jupyter-test-info ("`jupyter-repl-goto-cell'")
|
|
(let (cell-pos req)
|
|
(setq cell-pos (jupyter-repl-cell-beginning-position))
|
|
(jupyter-test-repl-ret-sync)
|
|
(save-excursion
|
|
(goto-char cell-pos)
|
|
(setq req (jupyter-repl-cell-request)))
|
|
(jupyter-test-repl-ret-sync)
|
|
(should (/= (point) cell-pos))
|
|
(jupyter-repl-goto-cell req)
|
|
(should (= (point) cell-pos))))
|
|
(jupyter-test-info ("`jupyter-repl-previous-cell'")
|
|
(let (cell-pos1)
|
|
(setq cell-pos1 (jupyter-repl-cell-beginning-position))
|
|
(goto-char cell-pos1)
|
|
(ert-info ("First motion to beginning of current cell")
|
|
(jupyter-repl-replace-cell-code "1 + 1")
|
|
(should (/= (point) cell-pos1))
|
|
(should (= (jupyter-repl-previous-cell) 0))
|
|
(should (= (point) cell-pos1))
|
|
(jupyter-repl-replace-cell-code ""))
|
|
(ert-info ("Motion with count")
|
|
(jupyter-test-repl-ret-sync)
|
|
(jupyter-test-repl-ret-sync)
|
|
(goto-char (jupyter-repl-cell-beginning-position))
|
|
(should (= (jupyter-repl-previous-cell 2) 0))
|
|
(should (= (point) cell-pos1)))
|
|
(ert-info ("First cell of buffer")
|
|
(goto-char cell-pos1)
|
|
(should (= (jupyter-repl-previous-cell) 1))
|
|
(should (= (point) (point-min))))))
|
|
(jupyter-test-info ("`jupyter-repl-backward-cell'")
|
|
(let (cell-pos1)
|
|
(setq cell-pos1 (jupyter-repl-cell-code-beginning-position))
|
|
(jupyter-test-repl-ret-sync)
|
|
(should-not (= (point) cell-pos1))
|
|
(jupyter-repl-backward-cell)
|
|
(should (= (point) cell-pos1))))
|
|
(jupyter-test-info ("`jupyter-repl-next-cell'")
|
|
(let (cell-pos1 cell-pos2)
|
|
(setq cell-pos1 (jupyter-repl-cell-beginning-position))
|
|
(ert-info ("Motion with count")
|
|
(jupyter-test-repl-ret-sync)
|
|
(jupyter-test-repl-ret-sync)
|
|
(setq cell-pos2 (jupyter-repl-cell-beginning-position))
|
|
(goto-char cell-pos1)
|
|
(should (= (jupyter-repl-next-cell 2) 0))
|
|
(should (= (point) cell-pos2)))
|
|
(ert-info ("Last cell of buffer")
|
|
(goto-char cell-pos2)
|
|
(should (= (jupyter-repl-next-cell) 1))
|
|
(should (= (point) (point-max))))))
|
|
(jupyter-test-info ("`jupyter-repl-forward-cell'")
|
|
(let (cell-pos1 cell-pos2)
|
|
(setq cell-pos1 (jupyter-repl-cell-code-beginning-position))
|
|
(jupyter-test-repl-ret-sync)
|
|
(setq cell-pos2 (jupyter-repl-cell-code-beginning-position))
|
|
(goto-char cell-pos1)
|
|
(jupyter-repl-forward-cell)
|
|
(should (= (point) cell-pos2))))))
|
|
|
|
(ert-deftest jupyter-repl-cell-positions ()
|
|
(jupyter-with-python-repl client
|
|
(ert-info ("Beginning of a cell")
|
|
(should (= (point) (jupyter-repl-cell-code-beginning-position)))
|
|
(should (get-text-property (- (point) 2) 'jupyter-cell))
|
|
(should (jupyter-repl-cell-beginning-p (- (point) 2)))
|
|
(should (= (jupyter-repl-cell-beginning-position) (- (point) 2))))
|
|
(ert-info ("End of unfinalized cell")
|
|
(should-not (jupyter-repl-cell-finalized-p))
|
|
(should-not (get-text-property (point-max) 'jupyter-cell))
|
|
(should (= (jupyter-repl-cell-end-p (point-max))))
|
|
(should (= (jupyter-repl-cell-end-position) (point-max)))
|
|
(should (= (jupyter-repl-cell-code-end-position) (point-max))))
|
|
(ert-info ("End of finalized cell")
|
|
(jupyter-test-repl-ret-sync)
|
|
(should (= (point) (jupyter-repl-cell-code-beginning-position)))
|
|
(goto-char (1- (jupyter-repl-cell-beginning-position)))
|
|
(should (jupyter-repl-cell-end-p))
|
|
(should (= (jupyter-repl-cell-end-position) (point)))
|
|
(should (= (jupyter-repl-cell-code-end-position) (1- (point))))
|
|
(should (jupyter-repl-cell-finalized-p)))
|
|
(ert-info ("Cell boundary errors")
|
|
(goto-char (point-max))
|
|
(jupyter-repl-replace-cell-code "1 + 1")
|
|
(jupyter-wait-until-idle (jupyter-send-execute-request client))
|
|
(forward-line -2)
|
|
(should (eq (car (get-text-property (1- (point)) 'jupyter-cell))
|
|
'out))
|
|
(should-error (jupyter-repl-cell-beginning-position))
|
|
(should-error (jupyter-repl-cell-end-position)))))
|
|
|
|
(ert-deftest jupyter-repl-prompts ()
|
|
(jupyter-with-python-repl client
|
|
(ert-info ("Prompt properties")
|
|
(let (prompt-overlay)
|
|
(goto-char (jupyter-repl-cell-beginning-position))
|
|
(setq prompt-overlay (car (overlays-at (point))))
|
|
(should-not (null prompt-overlay))
|
|
(should (equal (get-text-property (point) 'jupyter-cell) '(beginning 1)))
|
|
(should (= (jupyter-repl-cell-count) 1))))
|
|
(ert-info ("Input prompts")
|
|
(goto-char (jupyter-repl-cell-code-beginning-position))
|
|
;; To prevent prompts from inheriting text properties of cell code there is
|
|
;; an invisible character at the end of every prompt. This is because
|
|
;; prompts are implemented as overlays and therefore will inherit the text
|
|
;; properties of adjacent text, we want to prevent that.
|
|
(should (invisible-p (1- (point))))
|
|
(should (jupyter-repl-cell-beginning-p (- (point) 2)))
|
|
(should (eq (char-after (- (point) 2)) ?\n))
|
|
(let* ((props (text-properties-at (- (point) 2)))
|
|
(cell-property (memq 'jupyter-cell props)))
|
|
(should (not (null cell-property)))
|
|
(should (listp (cdr cell-property)))
|
|
(should (equal (cadr cell-property) '(beginning 1)))))
|
|
(ert-info ("Continuation prompts")
|
|
|
|
)
|
|
(ert-info ("Output prompts")
|
|
|
|
)))
|
|
|
|
(defun jupyter-test-src-block (session code test-result)
|
|
(let ((pos (point)))
|
|
(insert
|
|
"#+BEGIN_SRC jupyter-python " ":session " session "\n"
|
|
code "\n"
|
|
"#+END_SRC")
|
|
(let* ((info (org-babel-get-src-block-info)))
|
|
(org-babel-execute-src-block nil info)
|
|
(org-with-point-at (org-babel-where-is-src-block-result nil info)
|
|
(forward-line 1)
|
|
(let ((result
|
|
(string-trim-right
|
|
(buffer-substring-no-properties
|
|
(point) (goto-char (org-babel-result-end))))))
|
|
(should (equal result test-result))
|
|
(delete-region pos (point)))))))
|
|
|
|
(defvar org-babel-jupyter-resource-directory nil)
|
|
|
|
(ert-deftest org-babel-jupyter ()
|
|
(ert-info ("Dynamic result types")
|
|
(let ((session (make-temp-name "ob-jupyter-test")) repl-buffer)
|
|
(unwind-protect
|
|
(with-temp-buffer
|
|
(org-mode)
|
|
(insert
|
|
"#+BEGIN_SRC jupyter-python " ":session " session "\n"
|
|
"#+END_SRC")
|
|
(setq repl-buffer (org-babel-initiate-session))
|
|
(erase-buffer)
|
|
(ert-info ("Scalar results")
|
|
(jupyter-test-src-block session "1 + 1" ": 2"))
|
|
(ert-info ("HTML results")
|
|
(let ((code "\
|
|
from IPython.core.display import HTML\n\
|
|
HTML('<a href=\"http://foo.com\">link</a>')"))
|
|
(jupyter-test-src-block session code "\
|
|
#+BEGIN_EXPORT html
|
|
<a href=\"http://foo.com\">link</a>
|
|
#+END_EXPORT")))
|
|
(ert-info ("Image results")
|
|
(let* ((default-directory (file-name-directory
|
|
(locate-library "jupyter")))
|
|
(org-babel-jupyter-resource-directory "./")
|
|
(file (expand-file-name "jupyter.png"))
|
|
(data (let ((buffer-file-coding-system 'binary))
|
|
(with-temp-buffer
|
|
(set-buffer-multibyte nil)
|
|
(insert-file-contents-literally file)
|
|
(base64-encode-region (point-min) (point-max) t)
|
|
(goto-char (point-max))
|
|
(insert "\n")
|
|
(buffer-substring-no-properties (point-min) (point-max)))))
|
|
(image-file-name (org-babel-jupyter-file-name data "png"))
|
|
(code (format "\
|
|
from IPython.display import Image
|
|
Image(filename='%s')" file)))
|
|
(unwind-protect
|
|
(jupyter-test-src-block session code (format "[[file:%s]]" image-file-name))
|
|
(when (file-exists-p image-file-name)
|
|
(delete-file image-file-name))))))
|
|
(cl-letf (((symbol-function 'yes-or-no-p)
|
|
(lambda (_prompt) t))
|
|
((symbol-function 'y-or-n-p)
|
|
(lambda (_prompt) t)))
|
|
(when repl-buffer
|
|
(kill-buffer repl-buffer)))))))
|
|
|
|
;;; jupyter-tests.el ends here
|
|
|
|
;; Local Variables:
|
|
;; byte-compile-warnings: (not free-vars)
|
|
;; End:
|