diff --git a/.ert-runner b/.ert-runner new file mode 100644 index 0000000..4a8a196 --- /dev/null +++ b/.ert-runner @@ -0,0 +1,3 @@ +-l test/test-load.el +-L ./lisp +-L ./test \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 50ccb9c..0db09fe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,4 +20,4 @@ env: - EVM_EMACS=emacs-git-snapshot-travis IPYCMD=jupyter JUPYTER=1.0.0 script: - emacs --version - - python tools/testein.py --emacs emacs --ipython $(which $IPYCMD) + - cask exec ert-runner diff --git a/lisp/ein-kernel.el b/lisp/ein-kernel.el index 87b7129..899fe57 100644 --- a/lisp/ein-kernel.el +++ b/lisp/ein-kernel.el @@ -119,6 +119,7 @@ ,@(if kernelspec `(("kernel" . (("name" . ,(ein:$kernelspec-name kernelspec)))))))))) + :sync ein:force-sync :parser #'ein:json-read :success (apply-partially #'ein:kernel--kernel-started kernel) :error (apply-partially #'ein:kernel--start-failed kernel notebook)))))) diff --git a/test/ein-testing-cell.el b/test/ein-testing-cell.el new file mode 100644 index 0000000..9194c8c --- /dev/null +++ b/test/ein-testing-cell.el @@ -0,0 +1,76 @@ +;;; ein-testing-cell.el --- Testing utilities for cell module + +;; Copyright (C) 2012 Takafumi Arakaki + +;; Author: Takafumi Arakaki + +;; This file is NOT part of GNU Emacs. + +;; ein-testing-cell.el 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 of +;; the License, or (at your option) any later version. + +;; ein-testing-cell.el 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 ein-testing-cell.el. +;; If not, see . + +;;; Commentary: + +;; + +;;; Code: + +(require 'json) + +(defvar ein:testing-example-svg "\ + + + + + +") + +(defun ein:testing-codecell-pyout-data (text &optional prompt-number) + "Create a plist representing JSON data for code-cell output. +TEXT is a string and PROMPT-NUMBER is an integer." + (list :output_type "pyout" + :prompt_number (or prompt-number 0) + :text text)) + +(defun ein:testing-codecell-data (&optional input prompt-number outputs) + "Create a plist representing JSON data for code-type cell. +To make OUTPUTS data, use `ein:testing-codecell-pyout-data'." + (list :cell_type "code" + :source (or input "") + :language "python" + :outputs outputs + :metadata (list :collapsed json-false :autoscroll json-false) + :execution_count prompt-number)) + +(defun ein:testing-textcell-data (&optional source cell-type) + (list :cell_type cell-type + :source (or source ""))) + +(defun ein:testing-markdowncell-data (&optional source) + (ein:testing-textcell-data source "markdown")) + +(defun ein:testing-rawcell-data (&optional source) + (ein:testing-textcell-data source "raw")) + +(defun ein:testing-htmlcell-data (&optional source) + (ein:testing-textcell-data source "html")) + +(defun ein:testing-headingcell-data (&optional source level) + (append (ein:testing-textcell-data source "heading") + (list :level (or level 1)))) + +(provide 'ein-testing-cell) + +;;; ein-testing-cell.el ends here diff --git a/test/ein-testing-kernel.el b/test/ein-testing-kernel.el new file mode 100644 index 0000000..73b2c60 --- /dev/null +++ b/test/ein-testing-kernel.el @@ -0,0 +1,85 @@ +;;; ein-testing-kernel.el --- Testing utilities for kernel module + +;; Copyright (C) 2012 Takafumi Arakaki + +;; Author: Takafumi Arakaki + +;; This file is NOT part of GNU Emacs. + +;; ein-testing-kernel.el 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 of the License, or +;; (at your option) any later version. + +;; ein-testing-kernel.el 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 ein-testing-kernel.el. If not, see . + +;;; Commentary: + +;; + +;;; Code: + +(eval-when-compile (require 'cl)) +(require 'ert) + +(require 'ein-kernel) + + + +;;; Test `ein:kernel-construct-help-string' + +(defvar ein:testing-kernel-construct-help-string-pcallsig-list + '(nil :call_def :init_definition :definition)) + +(defvar ein:testing-kernel-construct-help-string-pdocstring-list + '(nil :call_docstring :init_docstring :docstring)) + +(defun ein:testing-kernel-construct-help-string-test-func (content result) + (should (equal (ein:kernel-construct-help-string content) result))) + +(defun ein:testing-kernel-construct-help-string-loop + (&optional test pcallsig-list pdocstring-list) + "Run tests for `ein:kernel-construct-help-string-loop'. + +TEST + A function takes two arguments, namely CONTENT and RESULT. + CONTENT is the argument to `ein:kernel-construct-help-string' and + RESULT must match to its returned value. Use `should' to test + equality. +PCALLSIG-LIST + `nil' or (subset of) `ein:testing-kernel-construct-help-string-pcallsig-list'. +PDOCSTRING-LIST + `nil' or (subset of) `ein:testing-kernel-construct-help-string-pdocstring-list'. + +All combinations of PCALLSIG-LIST and PDOCSTRING-LIST are used to +construct CONTENT and RESULT." + (unless test + (setq test #'ein:testing-kernel-construct-help-string-test-func)) + (unless pcallsig-list + (setq pcallsig-list + ein:testing-kernel-construct-help-string-pcallsig-list)) + (unless pdocstring-list + (setq pdocstring-list + ein:testing-kernel-construct-help-string-pdocstring-list)) + (loop with callsig = "function(a=1, b=2, c=d)" + with docstring = "This function does what." + for pcallsig in pcallsig-list + do (loop for pdoc in pdocstring-list + for content = (append + (when pcallsig (list pcallsig callsig)) + (when pdoc (list pdoc docstring))) + for result = (ein:aif (append + (when pcallsig (list callsig)) + (when pdoc (list docstring))) + (ein:join-str "\n" it)) + do (funcall test content result)))) + +(provide 'ein-testing-kernel) + +;;; ein-testing-kernel.el ends here diff --git a/test/ein-testing-notebook.el b/test/ein-testing-notebook.el new file mode 100644 index 0000000..e58a711 --- /dev/null +++ b/test/ein-testing-notebook.el @@ -0,0 +1,122 @@ +;;; ein-testing-notebook.el --- Testing utilities for notebook module + +;; Copyright (C) 2012 Takafumi Arakaki + +;; Author: Takafumi Arakaki + +;; This file is NOT part of GNU Emacs. + +;; ein-testing-notebook.el 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 of +;; the License, or (at your option) any later version. + +;; ein-testing-notebook.el 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 ein-testing-notebook.el. +;; If not, see . + +;;; Commentary: + +;; + +;;; Code: + +(eval-when-compile (require 'cl)) + +(require 'ein-notebook) +(require 'ein-testing-cell) + +(defun ein:testing-notebook-from-json (json-string &optional name path) + (let* ((data (ein:json-read-from-string json-string)) + (name (plist-get data :name)) + (path (plist-get data :path)) + (kernelspec (make-ein:$kernelspec :name "python3")) + (content (make-ein:$content :url-or-port "DUMMY-URL" + :ipython-version 3 + :path path))) + (unless name (setq name "NOTEBOOK-DUMMY")) + (unless path (setq path "NOTEBOOK-DUMMY")) + ;; cl-flet does not work correctly here! + (cl-flet ((pop-to-buffer (buf) + buf) + (ein:query-ipython-version (&optional url-or-port force) + 3) + (ein:notebook-start-kernel (notebook) + notebook)) + (let ((notebook (ein:notebook-new "DUMMY-URL" path kernelspec))) + (setf (ein:$notebook-kernel notebook) + (ein:kernel-new 8888 "/kernels" (ein:$notebook-events notebook) (ein:query-ipython-version))) + (setf (ein:$kernel-events (ein:$notebook-kernel notebook)) + (ein:events-new)) + (ein:notebook-request-open-callback notebook (ein:new-content content nil :data data)) + (ein:notebook-buffer notebook))))) + +(defun ein:testing-notebook-make-data (cells &optional name path) + (setq cells + (ein:testing-notebook--preprocess-cells-data-for-json-encode cells)) + (unless name (setq name "Dummy Name.ipynb")) + (unless path (setq path "Dummy Name.ipynb")) + `((path . ,path) + (name . ,name) + (type . "notebook") + (format . "json") + (mimetype . nil) + (writeable . t) + (content (metadata . ()) + (nbformat . 4) + (nbformat_minor . 0) + (cells . ,(apply #'vector cells))))) + +(defun ein:testing-notebook--preprocess-cells-data-for-json-encode (cells) + "Preprocess CELLS data to make it work nice with `json-encode'." + (mapcar (lambda (c) + (cond + ((equal (plist-get c :cell_type) "code") + ;; turn `:outputs' into an array. + (plist-put c :outputs (apply #'vector (plist-get c :outputs)))) + (t c))) + cells)) + +(defun ein:testing-notebook-make-new (&optional name cells-data) + "Make new notebook. One empty cell will be inserted +automatically if CELLS-DATA is nil." + (ein:testing-notebook-from-json + (json-encode (ein:testing-notebook-make-data cells-data name)))) + +(defun ein:testing-notebook-make-empty (&optional name) + "Make empty notebook and return its buffer. +Automatically inserted cell for new notebook is deleted." + (let ((buffer (ein:testing-notebook-make-new name))) + (with-current-buffer buffer + (call-interactively #'ein:worksheet-delete-cell)) + buffer)) + +(defmacro ein:testing-with-one-cell (cell-type &rest body) + "Insert new cell of CELL-TYPE in a clean notebook and execute BODY. +The new cell is bound to a variable `cell'." + (declare (indent 1)) + `(with-current-buffer (ein:testing-notebook-make-empty) + (let ((cell (ein:worksheet-insert-cell-below ein:%worksheet% + ,cell-type nil t))) + ,@body))) + +(defun ein:testing-make-notebook-with-outputs (list-outputs) + "Make a new notebook with cells with output. +LIST-OUTPUTS is a list of list of strings (pyout text). Number +of LIST-OUTPUTS equals to the number cells to be contained in the +notebook." + (ein:testing-notebook-make-new + nil + (mapcar (lambda (outputs) + (ein:testing-codecell-data + nil nil (mapcar #'ein:testing-codecell-pyout-data outputs))) + list-outputs))) + +(provide 'ein-testing-notebook) + +;;; ein-testing-notebook.el ends here diff --git a/test/ein-testing.el b/test/ein-testing.el new file mode 100644 index 0000000..091fa12 --- /dev/null +++ b/test/ein-testing.el @@ -0,0 +1,76 @@ +;;; ein-testing.el --- Tools for testing + +;; Copyright (C) 2012 Takafumi Arakaki + +;; Author: Takafumi Arakaki + +;; This file is NOT part of GNU Emacs. + +;; ein-testing.el 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 of the License, or +;; (at your option) any later version. + +;; ein-testing.el 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 ein-testing.el. If not, see . + +;;; Commentary: + +;; + +;;; Code: + +(require 'ein-log) + +(defmacro ein:setq-if-not (sym val) + `(unless ,sym (setq ,sym ,val))) + +(defvar ein:testing-dump-file-log nil + "File to save buffer specified by `ein:log-all-buffer-name'.") + +(defvar ein:testing-dump-file-messages nil + "File to save the ``*Messages*`` buffer.") + +(defvar ein:testing-dump-file-debug nil) + +(defvar ein:testing-dump-server-log nil) + +(defun ein:testing-save-buffer (buffer-or-name file-name) + (when (and buffer-or-name (get-buffer buffer-or-name) file-name) + (with-current-buffer buffer-or-name + (write-region (point-min) (point-max) file-name)))) + +(defun ein:testing-dump-logs () + (ein:testing-save-buffer "*Messages*" ein:testing-dump-file-messages) + (ein:testing-save-buffer "*ein:jupyter-server*" ein:testing-dump-server-log) + (ein:testing-save-buffer ein:log-all-buffer-name ein:testing-dump-file-log)) + +(defvar ein:testing-dump-logs--saved nil) + +(defun ein:testing-dump-logs-noerror () + (if ein:testing-dump-logs--saved + (message "EIN:TESTING-DUMP-LOGS-NOERROR called but already saved.") + (condition-case err + (progn (ein:testing-dump-logs) + (setq ein:testing-dump-logs--saved t)) + (error + (message "Error while executing EIN:TESTING-DUMP-LOGS. err = %S" + err) + (when ein:testing-dump-file-debug + (signal (car err) (cdr err))))))) + +(defadvice ert-run-tests-batch (after ein:testing-dump-logs-hook activate) + "Hook `ein:testing-dump-logs-noerror' because `kill-emacs-hook' +is not run in batch mode before Emacs 24.1." + (ein:testing-dump-logs-noerror)) + +(add-hook 'kill-emacs-hook #'ein:testing-dump-logs-noerror) + +(provide 'ein-testing) + +;;; ein-testing.el ends here diff --git a/test/func-test.el b/test/func-test.el new file mode 100644 index 0000000..877ebc9 --- /dev/null +++ b/test/func-test.el @@ -0,0 +1,290 @@ +(eval-when-compile (require 'cl)) +(require 'ert) + +(require 'ein-loaddefs) +(require 'ein-notebooklist) +(require 'ein-jupyter) +(require 'wid-edit) +(require 'ein-testing) +(require 'ein-testing-cell) + +(let ((backend (getenv "EL_REQUEST_BACKEND"))) + (when (and backend (not (equal backend ""))) + (setq request-backend (intern backend)) + (message "Using request-backend = %S" request-backend))) + + +(ein:setq-if-not ein:testing-dump-file-log "func-test-batch-log.log") +(ein:setq-if-not ein:testing-dump-file-messages "func-test-batch-messages.log") +(ein:setq-if-not ein:testing-dump-server-log "func-test_server_batch_emacs.log") + +(setq message-log-max t) + +(defun ein:testing-wait-until (message predicate &optional predargs max-count) + "Wait until PREDICATE function returns non-`nil'. +PREDARGS is argument list for the PREDICATE function. +Make MAX-COUNT larger \(default 50) to wait longer before timeout." + (ein:log 'debug "TESTING-WAIT-UNTIL start") + (ein:log 'debug "TESTING-WAIT-UNTIL waiting on: %s" message) + (unless max-count (setq max-count 50)) + (unless (loop repeat max-count + when (apply predicate predargs) + return t + ;; borrowed from `deferred:sync!': + do (sit-for 0.2) + do (sleep-for 0.2)) + (error "Timeout")) + (ein:log 'debug "TESTING-WAIT-UNTIL end")) + +(defun ein:testing-get-notebook-by-name (url-or-port notebook-name &optional path) + (ein:log 'debug "TESTING-GET-NOTEBOOK-BY-NAME start") + (when path + (setq notebook-name (format "%s/%s" path notebook-name))) + (ein:notebooklist-open url-or-port path t) + (ein:testing-wait-until "ein:notebooklist-open" + (lambda () (and (bufferp (get-buffer (format ein:notebooklist-buffer-name-template url-or-port))) + (ein:notebooklist-get-buffer url-or-port)))) + (with-current-buffer (ein:notebooklist-get-buffer url-or-port) + (prog1 + (ignore-errors + (ein:notebooklist-open-notebook-by-name notebook-name url-or-port)) + (ein:log 'debug "TESTING-GET-NOTEBOOK-BY-NAME end")))) + +(defun ein:testing-get-untitled0-or-create (url-or-port &optional path) + (ein:log 'debug "TESTING-GET-UNTITLED0-OR-CREATE start") + (let ((notebook (ein:testing-get-notebook-by-name url-or-port "Untitled.ipynb" path))) + (if notebook + (progn (ein:log 'debug + "TESTING-GET-UNTITLED0-OR-CREATE notebook already exists") + notebook) + (ein:log 'debug + "TESTING-GET-UNTITLED0-OR-CREATE creating notebook") + (let ((created nil) + (kernelspec (ein:get-kernelspec url-or-port "python3"))) + (ein:notebooklist-new-notebook url-or-port kernelspec path + (lambda (&rest -ignore-) + (setq created t))) + (ein:testing-wait-until "ein:notebooklist-new-notebook" + (lambda () created))) + (prog1 + (ein:testing-get-notebook-by-name url-or-port "Untitled.ipynb" path) + (ein:log 'debug "TESTING-GET-UNTITLED0-OR-CREATE end"))))) + +(defvar ein:notebooklist-after-open-hook nil) + +(defadvice ein:notebooklist-url-retrieve-callback + (after ein:testing-notebooklist-url-retrieve-callback activate) + "Advice to add `ein:notebooklist-after-open-hook'." + (run-hooks 'ein:notebooklist-after-open-hook)) + +(defun ein:testing-delete-notebook (url-or-port notebook &optional path) + (ein:log 'debug "TESTING-DELETE-NOTEBOOK start") + (ein:notebooklist-open url-or-port (ein:$notebook-notebook-path notebook) t) + (ein:testing-wait-until "ein:notebooklist-open" + (lambda () + (bufferp (get-buffer (format ein:notebooklist-buffer-name-template url-or-port)))) + nil 50) + (with-current-buffer (ein:notebooklist-get-buffer url-or-port) + (ein:testing-wait-until "ein:notebooklist-get-buffer" + (lambda () (eql major-mode 'ein:notebooklist-mode)) + nil + 50) + (ein:log 'debug "TESTING-DELETE-NOTEBOOK deleting notebook") + (ein:notebooklist-delete-notebook (ein:$notebook-notebook-path notebook))) + (ein:log 'debug "TESTING-DELETE-NOTEBOOK end")) + +;; (ert-deftest 00-jupyter-start-server () +;; (ein:log 'verbose "ERT TESTING-JUPYTER-START-SERVER start") +;; (condition-case err +;; (ein:testing-start-server) +;; (error (ein:log 'verbose "ERT TESTING-JUPYTER-START-SERVER error when launching: %s" err) +;; (sit-for 10) +;; (ein:jupyter-server-login-and-open))) +;; (should (processp %ein:jupyter-server-session%)) +;; (ein:log 'verbose "ERT TESTING-JUPYTER-START-SERVER end")) + +(ert-deftest 01-open-notebooklist () + (ein:log 'verbose "ERT OPEN-NOTEBOOKLIST start") + (ein:notebooklist-open *ein:testing-port* "/" t) + (ein:testing-wait-until + "ein:notebooklist-open" + (lambda () + (ein:notebooklist-get-buffer *ein:testing-port*)) + nil 5000) + (with-current-buffer (ein:notebooklist-get-buffer *ein:testing-port*) + (should (eql major-mode 'ein:notebooklist-mode)))) + + +(ert-deftest 00-query-kernelspecs () + (ein:log 'verbose "ERT QUERY-KERNELSPECS") + (ein:log 'verbose (format "ERT QUERY-KERNELSPECS: Pre-query kernelspec count %s." (hash-table-count ein:available-kernelspecs))) + (ein:query-kernelspecs *ein:testing-port*) + (should (>= (hash-table-count ein:available-kernelspecs) 1)) + (ein:log 'verbose (format "ERT QUERY-KERNELSPECS: Post-query kernelspec count %s." (hash-table-count ein:available-kernelspecs))) + ) + +(ert-deftest 10-get-untitled0-or-create () + (ein:log 'verbose "ERT TESTING-GET-UNTITLED0-OR-CREATE start") + (let ((notebook (ein:testing-get-untitled0-or-create *ein:testing-port*))) + (ein:testing-wait-until + "ein:testing-get-untitled0-or-create" + (lambda () (ein:aand (ein:$notebook-kernel notebook) + (ein:kernel-live-p it))) + nil 50000) + (with-current-buffer (ein:notebook-buffer notebook) + (should (equal (ein:$notebook-notebook-name ein:%notebook%) + "Untitled.ipynb")))) + (ein:log 'verbose "ERT TESTING-GET-UNTITLED0-OR-CREATE end")) + +(ert-deftest 20-delete-untitled0 () + (ein:log 'verbose "----------------------------------") + (ein:log 'verbose "ERT TESTING-DELETE-UNTITLED0 start") + (ein:log 'verbose "ERT TESTING-DELETE-UNTITLED0 creating notebook") + (let ((notebook (ein:testing-get-untitled0-or-create *ein:testing-port*))) + (ein:testing-wait-until + "ein:test-get-untitled0-or-create" + (lambda () + (ein:aand notebook + (ein:$notebook-kernel it) + (ein:kernel-live-p it))) + nil 50) + (ein:log 'verbose "ERT TESTING-DELETE-UNTITLED0 deleting notebook") + (ein:testing-delete-notebook *ein:testing-port* notebook)) + (ein:log 'verbose + "ERT TESTING-DELETE-UNTITLED0 check that the notebook is deleted") + (let ((num-notebook + (length (ein:testing-get-notebook-by-name *ein:testing-port* + "Untitled.ipynb" + "")))) + (should (= num-notebook 0))) + (ein:log 'verbose "ERT TESTING-DELETE-UNTITLED0 end")) + +(ert-deftest 11-notebook-execute-current-cell-simple () + (let ((notebook (ein:testing-get-untitled0-or-create *ein:testing-port*))) + (ein:testing-wait-until + "ein:testing-get-untitled0-or-create" + (lambda () (ein:aand (ein:$notebook-kernel notebook) + (ein:kernel-live-p it))) + nil 50) + (with-current-buffer (ein:notebook-buffer notebook) + (call-interactively #'ein:worksheet-insert-cell-below) + (insert "a = 100\na") + (let ((cell (call-interactively #'ein:worksheet-execute-cell))) + (ein:testing-wait-until "ein:worksheet-execute-cell" + (lambda () (not (slot-value cell 'running))) + nil + 50000)) + ;; (message "%s" (buffer-string)) + (save-excursion + (should (search-forward-regexp "Out \\[[0-9]+\\]" nil t)) + (should (search-forward "100" nil t)))))) + +(defun ein:testing-image-type (image) + "Return the type of IMAGE. +See the definition of `create-image' for how it works." + (assert (and (listp image) (eq (car image) 'image)) nil + "%S is not an image." image) + (plist-get (cdr image) :type)) + +(ert-deftest 12-notebook-execute-current-cell-pyout-image () + (let ((notebook (ein:testing-get-untitled0-or-create *ein:testing-port*))) + (ein:testing-wait-until + "ein:testing-get-untitled0-or-create" + (lambda () (ein:aand (ein:$notebook-kernel notebook) + (ein:kernel-live-p it)))) + (with-current-buffer (ein:notebook-buffer notebook) + (call-interactively #'ein:worksheet-insert-cell-below) + ;; Use IPython.core.display rather than IPython.display to + ;; test it with older (< 0.13) IPython. + (insert (concat "from IPython.core.display import SVG\n" + (format "SVG(data=\"\"\"%s\"\"\")" + ein:testing-example-svg))) + (let ((cell (call-interactively #'ein:worksheet-execute-cell))) + ;; It seems in this case, watching `:running' does not work + ;; well sometimes. Probably "output reply" (iopub) comes + ;; before "execute reply" in this case. + (ein:testing-wait-until "ein:worksheet-execute-cell" + (lambda () (slot-value cell 'outputs)) + nil + 50) + ;; This cell has only one input + (should (= (length (oref cell :outputs)) 1)) + ;; This output is a SVG image + (let ((out (nth 0 (oref cell :outputs)))) + (should (equal (plist-get out :output_type) "execute_result")) + (should (plist-get out :svg)))) + ;; Check the actual output in the buffer: + (save-excursion + (should (search-forward-regexp "Out \\[[0-9]+\\]" nil t)) + (should (= (forward-line) 0)) + (if (image-type-available-p 'svg) + (let ((image (get-text-property (point) 'display))) + (should (eq (ein:testing-image-type image) 'svg))) + (ein:log 'info + "Skipping image check as SVG image type is not available.")))))) + +(ert-deftest 13-notebook-execute-current-cell-stream () + (let ((notebook (ein:testing-get-untitled0-or-create *ein:testing-port*))) + (ein:testing-wait-until + "ein:testing-get-untitled0-or-create" + (lambda () (ein:aand (ein:$notebook-kernel notebook) + (ein:kernel-live-p it))) + nil 50) + (with-current-buffer (ein:notebook-buffer notebook) + (call-interactively #'ein:worksheet-insert-cell-below) + (insert "print('Hello')") + (let ((cell (call-interactively #'ein:worksheet-execute-cell))) + (ein:testing-wait-until "ein:worksheet-execute-cell" + (lambda () (not (oref cell :running))) + nil + 50000)) + (save-excursion + (should-not (search-forward-regexp "Out \\[[0-9]+\\]" nil t)) + (should (search-forward-regexp "^Hello$" nil t)))))) + +(ert-deftest 14-notebook-execute-current-cell-question () + (let ((notebook (ein:testing-get-untitled0-or-create *ein:testing-port*))) + (ein:testing-wait-until + "ein:testing-get-untitled0-or-create" + (lambda () (ein:aand (ein:$notebook-kernel notebook) + (ein:kernel-live-p it))) + nil 50) + (with-current-buffer (ein:notebook-buffer notebook) + (call-interactively #'ein:worksheet-insert-cell-below) + (insert "range?") + (let ((cell (call-interactively #'ein:worksheet-execute-cell))) + (ein:testing-wait-until + "ein:worksheet-execute-cell" + (lambda () (not (oref cell :running))) + nil 50)) + (with-current-buffer (get-buffer (ein:$notebook-pager notebook)) + (should (search-forward "Docstring:")))))) + +(ert-deftest 15-notebook-request-help () + (let ((notebook (ein:testing-get-untitled0-or-create *ein:testing-port*))) + (ein:testing-wait-until + "ein:testing-get-untitled0-or-create" + (lambda () (ein:aand (ein:$notebook-kernel notebook) + (ein:kernel-live-p it))) + nil 50000) + (with-current-buffer (ein:notebook-buffer notebook) + (call-interactively #'ein:worksheet-insert-cell-below) + (let ((pager-name (ein:$notebook-pager ein:%notebook%))) + (ein:aif (get-buffer pager-name) + (kill-buffer it)) + (insert "file") + (call-interactively #'ein:pytools-request-help) + ;; Pager buffer will be created when got the response + (ein:testing-wait-until + "ein:pythools-request-help" + (lambda () (get-buffer pager-name)) + nil 50000) + (with-current-buffer (get-buffer pager-name) + (should (search-forward "Docstring:"))))))) + +(ert-deftest 30-testing-jupyter-stop-server () + (ein:log 'verbose "ERT TESTING-JUPYTER-STOP-SERVER start") + (cl-letf (((symbol-function 'y-or-n-p) #'ignore)) + (ein:jupyter-server-stop t)) + (should-not (processp %ein:jupyter-server-session%)) + (ein:log 'verbose "ERT TESTING-JUPYTER-STOP-SERVER end")) diff --git a/test/redirecting-server.py b/test/redirecting-server.py new file mode 100644 index 0000000..7953070 --- /dev/null +++ b/test/redirecting-server.py @@ -0,0 +1,17 @@ +from flask import Flask, redirect + +app = Flask(__name__) + +@app.route('/') +def jupyter_redirect(): + return redirect("http://127.0.0.1:8888/", code=302) + +@app.route('/api') +def api_check(): + return redirect("http://127.0.0.1:8888/api", code=302) + +if __name__=='__main__': + port = int(8000) + app.run(host='127.0.0.1', port=port) + + diff --git a/test/test-ein-ac.el b/test/test-ein-ac.el new file mode 100644 index 0000000..c69236b --- /dev/null +++ b/test/test-ein-ac.el @@ -0,0 +1,17 @@ +(eval-when-compile (require 'cl)) +(require 'ert) + +(require 'ein-ac) +(require 'ein-testing-kernel) + + +(ert-deftest ein:ac-set-document () + (let ((string "candidate string")) + (should-not (get-text-property 0 'document string)) + (ein:testing-kernel-construct-help-string-loop + (lambda (content result) + (ein:ac-set-document string content '-not-used-) + (let ((props (text-properties-at 0 string))) + ;; document property may be nil, but must be set. + (should (member 'document props)) + (should (equal (plist-get props 'document) result))))))) diff --git a/test/test-ein-cell-notebook.el b/test/test-ein-cell-notebook.el new file mode 100644 index 0000000..090b09d --- /dev/null +++ b/test/test-ein-cell-notebook.el @@ -0,0 +1,249 @@ +;; Tests for cell function that requires notebook buffer + +(eval-when-compile (require 'cl)) +(require 'ert) + +(require 'ein-notebook) +(require 'ein-testing-notebook) + + +;; ein:cell-location + +(ert-deftest ein:cell-location-codecell-prompt-beg () + (ein:testing-with-one-cell 'code + (should (equal (marker-position (ein:cell-location cell :prompt)) + (save-excursion + (goto-char (point-max)) + (search-backward "In [ ]:") + (point)))))) + +(ert-deftest ein:cell-location-codecell-prompt-end () + (ein:testing-with-one-cell 'code + (should (equal (marker-position (ein:cell-location cell :prompt t)) + (1- (point)))))) + +(ert-deftest ein:cell-location-codecell-input-beg () + (ein:testing-with-one-cell 'code + (insert "some text") + (should (equal (marker-position (ein:cell-location cell :input)) + (1- (point-at-bol)))))) + +(ert-deftest ein:cell-location-codecell-input-end () + (ein:testing-with-one-cell 'code + (insert "some text") + (should (equal (marker-position (ein:cell-location cell :input t)) + (1+ (point)))))) + + +;; from-json + +(ert-deftest eintest:cell-input-prompt-number () + (ein:testing-with-one-cell + (ein:cell-from-json + (list :cell_type "code" + :source "some input" + :metadata (list :collapsed json-false :autoscroll json-false) + :execution_count 111) + :ewoc (oref ein:%worksheet% :ewoc)) + (goto-char (ein:cell-location cell)) + (should (looking-at "\ +In \\[111\\]: +some input +")))) + +(ert-deftest eintest:cell-input-prompt-star () + (ein:testing-with-one-cell + (ein:cell-from-json + (list :cell_type "code" + :source "some input" + :metadata (list :collapsed json-false :autoscroll json-false) + :execution_count "*") + :ewoc (oref ein:%worksheet% :ewoc)) + (goto-char (ein:cell-location cell)) + (should (looking-at "\ +In \\[\\*\\]: +some input +")))) + +(ert-deftest eintest:cell-input-prompt-empty () + (ein:testing-with-one-cell + (ein:cell-from-json + (list :cell_type "code" + :metadata (list :collapsed json-false :autoscroll json-false) + :source "some input") + :ewoc (oref ein:%worksheet% :ewoc)) + (goto-char (ein:cell-location cell)) + (should (looking-at "\ +In \\[ \\]: +some input +")))) + + +;; Insert pyout/display_data + +(defun eintest:cell-insert-output (outputs regexp) + (let ((ein:output-type-preference + '(emacs-lisp image/svg image/png jpeg text/plain text/html text/latex text/javascript))) + (message "%S" (list :cell_type "code" + :outputs outputs + :source "Some input" + :metadata (list :collapsed json-false :autoscroll json-false) + :execution_count 111)) + (ein:testing-with-one-cell + (ein:cell-from-json + (list :cell_type "code" + :outputs outputs + :source "some input" + :metadata (list :collapsed json-false :autoscroll json-false) + :execution_count 111) + :ewoc (oref ein:%worksheet% :ewoc)) + (goto-char (ein:cell-location cell)) + (should (looking-at (format "\ +In \\[111\\]: +some input +%s" regexp)))))) + +(defmacro eintest:gene-test-cell-insert-output-pyout-and-display-data + (name regexps outputs) + (declare (indent defun)) + (let ((test-pyout + (intern (format "ein:cell-insert-output-pyout-%s" name))) + (test-display-data + (intern (format "ein:cell-insert-output-display-data-%s" name))) + (outputs-pyout + (loop for i from 1 + for x in outputs + collect + (append x (list :output_type "execute_result" :execution_count i :metadata nil)))) + (outputs-display-data + (mapcar (lambda (x) (append '(:output_type "display_data" :metadata nil) x)) + outputs)) + (regexp-pyout + (ein:join-str + "" + (loop for i from 1 + for x in regexps + collect (format "Out \\[%s\\]:\n%s\n" i x)))) + (regexp-display-data + (concat (ein:join-str "\n" regexps) "\n"))) + `(progn + (ert-deftest ,test-pyout () + (eintest:cell-insert-output ',outputs-pyout + ,regexp-pyout)) + (ert-deftest ,test-display-data () + (eintest:cell-insert-output ',outputs-display-data + ,regexp-display-data))))) + +(eintest:gene-test-cell-insert-output-pyout-and-display-data + text ("some output") ((:data (:text/plain "some output")))) + +(eintest:gene-test-cell-insert-output-pyout-and-display-data + latex + ("some output \\\\LaTeX") + ((:data (:text/latex "some output \\LaTeX")))) + +(when (image-type-available-p 'svg) + (eintest:gene-test-cell-insert-output-pyout-and-display-data + svg + (" ") + ((:data (:text/plain "some output text" :svg ein:testing-example-svg))))) + +(eintest:gene-test-cell-insert-output-pyout-and-display-data + html + ("some output text") + ((:data (:text/plain ("some output text") :text/html ("not shown"))))) + +(eintest:gene-test-cell-insert-output-pyout-and-display-data + javascript + ("some output text") + ((:data (:text/plain "some output text" :text/javascript "$.do.something()")))) + +(eintest:gene-test-cell-insert-output-pyout-and-display-data + text-two + ("first output text" "second output text") + ((:data (:text/plain "first output text")) (:text/plain "second output text"))) + +(eintest:gene-test-cell-insert-output-pyout-and-display-data + text-javascript + ("first output text" "second output text") + ((:data (:text/plain "first output text")) + (:data (:text/plain "second output text" :text/javascript "$.do.something()")))) + +(when (image-type-available-p 'svg) + (eintest:gene-test-cell-insert-output-pyout-and-display-data + text-latex-svg + ("first output text" "second output \\\\LaTeX" " ") + ((:data (:text/plain "first output text")) + (:data (:text/latex "second output \\LaTeX")) + (:data (:text/plain "some output text" :image/svg ein:testing-example-svg))))) + + +;; Insert pyerr + +(ert-deftest ein:cell-insert-output-pyerr-simple () + (eintest:cell-insert-output + (list (list :output_type "pyerr" + :traceback '("some traceback 1" + "some traceback 2"))) + "\ +some traceback 1 +some traceback 2 +")) + + +;; Insert stream + +(ert-deftest ein:cell-insert-output-stream-simple-stdout () + (eintest:cell-insert-output + (list (list :output_type "stream" + :name "stdout" + :text "some stdout 1")) + "\ +some stdout 1 +")) + +(ert-deftest ein:cell-insert-output-stream-stdout-stderr () + (eintest:cell-insert-output + (list (list :output_type "stream" + :name "stdout" + :text "some stdout 1") + (list :output_type "stream" + :name "stderr" + :text "some stderr 1")) + "\ +some stdout 1 +some stderr 1 +")) + +(ert-deftest ein:cell-insert-output-stream-flushed-stdout () + (eintest:cell-insert-output + (list (list :output_type "stream" + :name "stdout" + :text "some stdout 1") + (list :output_type "stream" + :name "stdout" + :text "some stdout 2")) + "\ +some stdout 1some stdout 2 +")) + +(ert-deftest ein:cell-insert-output-stream-flushed-stdout-and-stderr () + (eintest:cell-insert-output + (list (list :output_type "stream" + :name "stdout" + :text "some stdout 1") + (list :output_type "stream" + :name "stderr" + :text "some stderr 1") + (list :output_type "stream" + :name "stdout" + :text "some stdout 2") + (list :output_type "stream" + :name "stderr" + :text "some stderr 2")) + "\ +some stdout 1 +some stderr 1 +some stdout 2 +some stderr 2 +")) diff --git a/test/test-ein-cell.el b/test/test-ein-cell.el new file mode 100644 index 0000000..532d164 --- /dev/null +++ b/test/test-ein-cell.el @@ -0,0 +1,270 @@ +(eval-when-compile (require 'cl)) +(require 'ert) + +(when load-file-name + (add-to-list 'load-path + (concat (file-name-directory load-file-name) "mocker"))) +(require 'mocker) + +(require 'ein-cell) +(require 'ein-testing-cell) + + +;;; ein:cell-from-json + +(defun eintest:cell-from-json (data &rest args) + (let ((cell (apply #'ein:cell-from-json data args))) + (should-not (ein:cell-active-p cell)) + cell)) + +(ert-deftest ein:cell-from-json-code () + (let* ((input-prompt-number 111) + (output-prompt-number 222) + (input (ein:join-str "\n" '("first input" "second input"))) + (output-0 (list :output_type "execute_result" + :prompt_number output-prompt-number + :text (list "first output" + "second output"))) + (data (ein:testing-codecell-data + input input-prompt-number (list output-0))) + (cell (eintest:cell-from-json data))) + (should (ein:codecell-p cell)) + (should (equal (oref cell :input-prompt-number) input-prompt-number)) + (should (equal (oref cell :input) input)) + (should (equal (car (oref cell :outputs)) output-0)) + (should (equal (oref cell :collapsed) nil)))) + +(ert-deftest ein:cell-from-json-text () + (let* ((input (ein:join-str "\n" '("first input" "second input"))) + (data (list :cell_type "text" :source input)) + (cell (eintest:cell-from-json data))) + (should (ein:textcell-p cell)) + (should (equal (oref cell :input) input)))) + +(ert-deftest ein:cell-from-json-html () + (let* ((input (ein:join-str "\n" '("first input" "second input"))) + (data (list :cell_type "html" :source input)) + (cell (eintest:cell-from-json data))) + (should (ein:htmlcell-p cell)) + (should (equal (oref cell :input) input)))) + +(ert-deftest ein:cell-from-json-markdown () + (let* ((input (ein:join-str "\n" '("first input" "second input"))) + (data (list :cell_type "markdown" :source input)) + (cell (eintest:cell-from-json data))) + (should (ein:markdowncell-p cell)) + (should (equal (oref cell :input) input)))) + +(ert-deftest ein:cell-from-json-raw () + (let* ((input (ein:join-str "\n" '("first input" "second input"))) + (data (list :cell_type "raw" :source input)) + (cell (eintest:cell-from-json data))) + (should (ein:rawcell-p cell)) + (should (equal (oref cell :input) input)))) + +(ert-deftest ein:cell-from-json-heading () + (let* ((input (ein:join-str "\n" '("first input" "second input"))) + (data (list :cell_type "heading" :source input)) + (cell (eintest:cell-from-json data))) + (should (ein:headingcell-p cell)) + (should (equal (oref cell :input) input)))) + + +;; ein:cell-to-json + +(defun eintest:cell-to-json (cell input &optional discard-output) + (mocker-let ((ein:cell-get-text + (cell) + ((:input (list cell) :output input)))) + (ein:cell-to-json cell discard-output))) + +(ert-deftest ein:cell-to-json-code () + (let* ((input-prompt-number 111) + (output-prompt-number 222) + (input (ein:join-str "\n" '("first input" "second input"))) + (output-0 (list :output_type "execute_result" + :prompt_number output-prompt-number + :text (list "first output" + "second output"))) + (data (ein:testing-codecell-data + input input-prompt-number (list output-0))) + (cell (eintest:cell-from-json data)) + (alist (eintest:cell-to-json cell input))) + (should (equal (cdr (assq 'input alist)) "first input\nsecond input")) + (should (equal (cdr (assq 'cell_type alist)) "code")) + (should (equal (cdr (assq 'outputs alist)) `[,output-0])) + (should (equal (cdr (assq 'language alist)) "python")) + (should (equal (cdr (assq 'collapsed alist)) json-false)))) + +(ert-deftest ein:cell-to-json-code-discard-output () + (let* ((input-prompt-number 111) + (output-prompt-number 222) + (input (ein:join-str "\n" '("first input" "second input"))) + (output-0 (list :output_type "execute_result" + :prompt_number output-prompt-number + :text (list "first output" + "second output"))) + (data (ein:testing-codecell-data + input input-prompt-number (list output-0))) + (cell (eintest:cell-from-json data)) + (alist (eintest:cell-to-json cell input t))) + (should (equal (cdr (assq 'input alist)) "first input\nsecond input")) + (should (equal (cdr (assq 'cell_type alist)) "code")) + (should (equal (cdr (assq 'outputs alist)) [])) + (should (equal (cdr (assq 'language alist)) "python")) + (should (equal (cdr (assq 'collapsed alist)) json-false)))) + +(ert-deftest ein:cell-to-json-text () + (let* ((input (ein:join-str "\n" '("first input" "second input"))) + (data (list :cell_type "text" :source input)) + (cell (eintest:cell-from-json data)) + (alist (eintest:cell-to-json cell input))) + (should (equal (cdr (assq 'cell_type alist)) "text")) + (should (equal (cdr (assq 'source alist)) "first input\nsecond input")))) + +(ert-deftest ein:cell-to-json-html () + (let* ((input (ein:join-str "\n" '("first input" "second input"))) + (data (list :cell_type "html" :source input)) + (cell (eintest:cell-from-json data)) + (alist (eintest:cell-to-json cell input))) + (should (equal (cdr (assq 'cell_type alist)) "html")) + (should (equal (cdr (assq 'source alist)) "first input\nsecond input")))) + +(ert-deftest ein:cell-to-json-markdown () + (let* ((input (ein:join-str "\n" '("first input" "second input"))) + (data (list :cell_type "markdown" :source input)) + (cell (eintest:cell-from-json data)) + (alist (eintest:cell-to-json cell input))) + (should (equal (cdr (assq 'cell_type alist)) "markdown")) + (should (equal (cdr (assq 'source alist)) "first input\nsecond input")))) + +(ert-deftest ein:cell-to-json-raw () + (let* ((input (ein:join-str "\n" '("first input" "second input"))) + (data (list :cell_type "raw" :source input)) + (cell (eintest:cell-from-json data)) + (alist (eintest:cell-to-json cell input))) + (should (equal (cdr (assq 'cell_type alist)) "raw")) + (should (equal (cdr (assq 'source alist)) "first input\nsecond input")))) + +(ert-deftest ein:cell-to-json-heading () + (let* ((input (ein:join-str "\n" '("first input" "second input"))) + (data (list :cell_type "heading" :source input)) + (cell (eintest:cell-from-json data)) + (alist (eintest:cell-to-json cell input))) + (should (equal (cdr (assq 'cell_type alist)) "heading")) + (should (equal (cdr (assq 'source alist)) "first input\nsecond input")) + (should (equal (cdr (assq 'level alist)) 1)))) + + +;;; ein:cell-convert/copy + +(ert-deftest ein:cell-convert-code-to-markdown () + (let* ((input-prompt-number 111) + (output-prompt-number 222) + (input (ein:join-str "\n" '("first input" "second input"))) + (output-0 (list :output_type "execute_result" + :prompt_number output-prompt-number + :text (list "first output" + "second output"))) + (data (ein:testing-codecell-data + input input-prompt-number (list output-0))) + (dummy-ewoc (ewoc-create 'dummy)) + (old (eintest:cell-from-json data :ewoc dummy-ewoc)) + (new (ein:cell-convert old "markdown"))) + (should (ein:codecell-p old)) + (should (ein:markdowncell-p new)) + (should (equal (oref new :input) input)))) + +(ert-deftest ein:cell-convert-markdown-to-code () + (let* ((input (ein:join-str "\n" '("first input" "second input"))) + (dummy-ewoc (ewoc-create 'dummy)) + (data (list :cell_type "markdown" :source input)) + (old (eintest:cell-from-json data :ewoc dummy-ewoc)) + (new (ein:cell-convert old "code"))) + (should (ein:markdowncell-p old)) + (should (ein:codecell-p new)) + (should (equal (oref new :input) input)))) + +(ert-deftest ein:cell-copy-code () + (let* ((input-prompt-number 111) + (output-prompt-number 222) + (input (ein:join-str "\n" '("first input" "second input"))) + (output-0 (list :output_type "execute_result" + :prompt_number output-prompt-number + :text (list "first output" + "second output"))) + (data (ein:testing-codecell-data + input input-prompt-number (list output-0))) + (dummy-ewoc (ewoc-create 'dummy)) + (old (eintest:cell-from-json data :ewoc dummy-ewoc)) + (new (ein:cell-copy old))) + (should (ein:codecell-p old)) + (should (ein:codecell-p new)) + (should-not (equal (oref old :cell-id) + (oref new :cell-id))) + (should (equal (oref old :input) input)) + (should (equal (oref new :input) input)))) + +(ert-deftest ein:cell-copy-text-types () + (loop for cell-type in '("text" "html" "markdown" "raw" "heading") + for cell-p = (intern (format "ein:%scell-p" cell-type)) + do + (let* ((input (ein:join-str "\n" '("first input" "second input"))) + (data (list :cell_type cell-type :source input :metadata nil)) + (dummy-ewoc (ewoc-create 'dummy)) + (old (eintest:cell-from-json data :ewoc dummy-ewoc)) + (new (ein:cell-copy old))) + (should (funcall cell-p old)) + (should (funcall cell-p new)) + (should-not (equal (oref old :cell-id) + (oref new :cell-id))) + (should (equal (oref old :input) input)) + (should (equal (oref new :input) input))))) + + +;;; ein:cell-element-get + +(ert-deftest ein:cell-element-get-basecell () + (let ((cell (ein:basecell "Cell"))) + ;; it's not supported + (should-error (ein:cell-element-get :prompt)))) + +(ert-deftest ein:cell-element-get-codecell () + (let* ((element (list :prompt 1 + :input 2 + :output '(3 4) + :footer 5)) + (cell (ein:cell-from-type "code" :element element))) + (mapc (lambda (kv) + (should (equal (ein:cell-element-get cell (car kv)) (cdr kv)))) + (ein:plist-iter element)) + (should (equal (ein:cell-element-get cell :output 0) 3)) + (should (equal (ein:cell-element-get cell :output 1) 4)) + (should (equal (ein:cell-element-get cell :output 2) nil)) + (should (equal (ein:cell-element-get cell :after-input) 3)) + (should (equal (ein:cell-element-get cell :after-output) 5)) + (should (equal (ein:cell-element-get cell :before-input) 1)) + (should (equal (ein:cell-element-get cell :before-output) 2)) + (should (equal (ein:cell-element-get cell :last-output) 4)))) + +(ert-deftest ein:cell-element-get-codecell-no-ouput () + (let* ((element (list :prompt 1 + :input 2 + :footer 5)) + (cell (ein:cell-from-type "code" :element element))) + (mapc (lambda (kv) + (should (equal (ein:cell-element-get cell (car kv)) (cdr kv)))) + (ein:plist-iter element)) + (should (equal (ein:cell-element-get cell :after-input) 5)) + (should (equal (ein:cell-element-get cell :last-output) 2)))) + +(ert-deftest ein:cell-element-get-textcell () + (let* ((element (list :prompt 1 + :input 2 + :footer 5)) + (cell (ein:cell-from-type "text" :element element))) + (mapc (lambda (kv) + (should (equal (ein:cell-element-get cell (car kv)) (cdr kv)))) + (ein:plist-iter element)) + (should (equal (ein:cell-element-get cell :after-input) 5)) + (should (equal (ein:cell-element-get cell :before-input) 1)))) diff --git a/test/test-ein-completer.el b/test/test-ein-completer.el new file mode 100644 index 0000000..5f52338 --- /dev/null +++ b/test/test-ein-completer.el @@ -0,0 +1,23 @@ +(eval-when-compile (require 'cl)) +(require 'ert) + +(when load-file-name + (add-to-list 'load-path + (concat (file-name-directory load-file-name) "mocker"))) +(require 'mocker) + +(require 'ein-completer) + + +(ert-deftest ein:completer-finish-completing () + (let* ((matched-text 'dummy-matched-text-value) ; value can be anything + (matches 'dummy-matches-value) + (content (list :matched_text matched-text + :matches matches)) + (args '(:extend t))) + (mocker-let + ((ein:completer-choose () ((:output 'completer))) + (completer + (matched-text matches &rest args) + ((:input (list matched-text matches args))))) + (ein:completer-finish-completing args content '-not-used-)))) diff --git a/test/test-ein-connect.el b/test/test-ein-connect.el new file mode 100644 index 0000000..555e850 --- /dev/null +++ b/test/test-ein-connect.el @@ -0,0 +1,47 @@ +(require 'ein-connect) +(require 'ein-testing-notebook) + +(defmacro eintest:with-connected-buffer (&rest body) + (declare (indent 0)) + `(let* ((notebook-buffer (ein:testing-notebook-make-empty)) + (notebook (buffer-local-value 'ein:notebook notebook-buffer))) + (with-temp-buffer + (erase-buffer) + (ein:connect-buffer-to-notebook notebook) + ,@body))) + +(ert-deftest ein:get-url-or-port--connect () + (eintest:with-connected-buffer + (should (equal (ein:get-url-or-port) + (ein:$notebook-url-or-port notebook))))) + +(ert-deftest ein:get-notebook--connect () + (eintest:with-connected-buffer + (should (eq (ein:get-notebook) notebook)))) + +(ert-deftest ein:get-kernel--connect () + (eintest:with-connected-buffer + (should (eq (ein:get-kernel) + (ein:$notebook-kernel notebook))))) + +(ert-deftest ein:get-cell-at-point--connect () + "`ein:get-cell-at-point' is in empty context in connected buffer." + (eintest:with-connected-buffer + (should-not (ein:get-cell-at-point)))) + +(ert-deftest ein:get-traceback-data--connect () + (eintest:with-connected-buffer + ;; FIXME: write test with non-empty TB + (should-not (ein:get-traceback-data)))) + +(ert-deftest ein:connect-mode-revert-buffer-resistance () + (let ((temp-file (make-temp-file "ein"))) + (unwind-protect + (with-temp-buffer + (setq buffer-file-name temp-file) + (ein:connect-mode 1) + (setq ein:%connect% 'very-important-value) + (revert-buffer t t nil) + (should ein:connect-mode) + (should (eq ein:%connect% 'very-important-value))) + (delete-file temp-file)))) diff --git a/test/test-ein-console.el b/test/test-ein-console.el new file mode 100644 index 0000000..c435567 --- /dev/null +++ b/test/test-ein-console.el @@ -0,0 +1,25 @@ +(require 'ein-console) + +(ert-deftest ein:console-security-dir-string () + (let ((ein:console-security-dir "/some/dir/")) + (should (equal (ein:console-security-dir-get "DUMMY-URL-OR-PORT") + ein:console-security-dir)))) + +(ert-deftest ein:console-security-dir-list () + (let ((ein:console-security-dir + '((8888 . "/dir/8888/") + ("htttp://dummy.org" . "/dir/http/") + (7777 . my-secret-directory) + (default . "/dir/default/"))) + (my-secret-directory "/dir/secret/")) + (should (equal (ein:console-security-dir-get 8888) "/dir/8888/")) + (should (equal (ein:console-security-dir-get "htttp://dummy.org") + "/dir/http/")) + (should (equal (ein:console-security-dir-get 7777) "/dir/secret/")) + (should (equal (ein:console-security-dir-get 9999) "/dir/default/")))) + +(ert-deftest ein:console-security-dir-func () + (let ((ein:console-security-dir + '(lambda (x) (should (equal x "DUMMY-URL-OR-PORT")) "/dir/"))) + (should (equal (ein:console-security-dir-get "DUMMY-URL-OR-PORT") + "/dir/")))) diff --git a/test/test-ein-content.el b/test/test-ein-content.el new file mode 100644 index 0000000..ac7c4be --- /dev/null +++ b/test/test-ein-content.el @@ -0,0 +1,38 @@ +;;; test-ein-content.el --- Testing content interface + +;; Copyright (C) 2015 John Miller + +;; Authors: Takafumi Arakaki +;; John M. Miller + +;; This file is NOT part of GNU Emacs. + +;; test-ein-content.el 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 of +;; the License, or (at your option) any later version. + +;; test-ein-content.el 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 ein-testing-notebook.el. +;; If not, see . + +;;; Commentary: + +;; + +;;; Code: + +(eval-when-compile (require 'cl)) + +(require 'ein-contents-api) + +(defvar *list-content-result* nil) + +(defun ein-test-list-contents-1 () + (ein:content-list-contents "" ) + ) diff --git a/test/test-ein-core.el b/test/test-ein-core.el new file mode 100644 index 0000000..840d45e --- /dev/null +++ b/test/test-ein-core.el @@ -0,0 +1,92 @@ +(eval-when-compile (require 'cl)) +(require 'ert) + +(require 'ein-core) + + + +;;; `ein:version' + +(ert-deftest ein:version () + "Check if `ein:version' can be parsed by `version-to-list'." + (version-to-list ein:version)) + +(ert-deftest ein:version-func-prefix-is-the-variable () + (should (string-prefix-p ein:version (ein:version))) + (let ((default-directory "/tmp/")) + (should (string-prefix-p ein:version (ein:version))))) + +(ert-deftest ein:version-func-outside-of-git-repo () + (flet ((ein:git-root-p (dir) nil)) + (should (equal (ein:version) ein:version))) + (flet ((ein:git-revision-dirty () nil)) + (should (equal (ein:version) ein:version)))) + + + +;; Generic getter + +(defmacro eintest:generic-getter-should-return-nil (func) + "In an \"empty\" context, generic getter should return nil." + `(ert-deftest ,(intern (format "%s--nil name" func)) () + (with-temp-buffer + (should (not (,func)))))) + +(eintest:generic-getter-should-return-nil ein:get-url-or-port) +(eintest:generic-getter-should-return-nil ein:get-notebook) +(eintest:generic-getter-should-return-nil ein:get-kernel) +(eintest:generic-getter-should-return-nil ein:get-cell-at-point) +(eintest:generic-getter-should-return-nil ein:get-traceback-data) + + + +;;; File name translation + +;; Requiring `tramp' during (inside of) tests yields error from +;; MuMaMo. Although I don't understand the reason, requiring it +;; before running tests workarounds this problem. +(require 'tramp) + +(ert-deftest ein:filename-translations-from-to-tramp () + (loop with ein:filename-translations = + `((8888 . ,(ein:tramp-create-filename-translator "HOST" "USER"))) + with filename = "/file/name" + for port in '(7777 8888) ; check for the one w/o translation + for emacs-filename = (ein:filename-from-python port filename) + do (message "emacs-filename = %s" emacs-filename) + do (should + (equal (ein:filename-to-python port emacs-filename) + filename)))) + +(ert-deftest ein:filename-translations-to-from-tramp () + (loop with ein:filename-translations = + `((8888 . ,(ein:tramp-create-filename-translator "HOST" "USER"))) + with filename = "/USER@HOST:/filename" + for port in '(8888) + do (should + (equal (ein:filename-from-python + port (ein:filename-to-python port filename)) + filename)))) + +(ert-deftest ein:filename-to-python-tramp () + (let* ((port 8888) + (ein:filename-translations + `((,port . ,(ein:tramp-create-filename-translator "DUMMY"))))) + (loop with python-filename = "/file/name" + for emacs-filename in '("/scpc:HOST:/file/name" + "/USER@HOST:/file/name") + do (should + (equal (ein:filename-to-python port emacs-filename) + python-filename))) + ;; Error: Not a Tramp file name: /file/name + (should-error (ein:filename-to-python port "/file/name")))) + +(ert-deftest ein:filename-from-python-tramp () + (loop with ein:filename-translations = + `((8888 . ,(ein:tramp-create-filename-translator "HOST" "USER"))) + with python-filename = "/file/name" + for emacs-filename in '("/USER@HOST:/file/name" "/file/name") + for port in '(8888 7777) + do (should + (equal (ein:filename-from-python port python-filename) + emacs-filename)))) diff --git a/test/test-ein-iexec.el b/test/test-ein-iexec.el new file mode 100644 index 0000000..898aa5c --- /dev/null +++ b/test/test-ein-iexec.el @@ -0,0 +1,43 @@ +(eval-when-compile (require 'cl)) +(require 'ert) + +(require 'ein-iexec) +(require 'ein-testing-notebook) + + +;;; `ein:iexec-should-execute-p' + +(defun* eintest:iexec-should-execute-p (cell &key (this-command t) beg end) + "Simple wrapper for `ein:iexec-should-execute-p' which +returns `t' by default, if the CELL is code cell." + (unless beg (setq beg (ein:cell-input-pos-min cell))) + (unless end (setq end (ein:cell-input-pos-max cell))) + (ein:iexec-should-execute-p cell beg end)) + +;; cell types + +(ert-deftest ein:iexec-should-execute-p-codecell () + (ein:testing-with-one-cell 'code + (should (eintest:iexec-should-execute-p cell)))) + +(ert-deftest ein:iexec-should-execute-p-markdowncell () + (ein:testing-with-one-cell 'markdown + (should-not (eintest:iexec-should-execute-p cell)))) + +(ert-deftest ein:iexec-should-execute-p-dead-cell () + (ein:testing-with-one-cell 'code + (should-not (eintest:iexec-should-execute-p (ein:cell-copy cell))))) + +;; other + +(ert-deftest ein:iexec-should-execute-p-non-interactive () + (ein:testing-with-one-cell 'code + (should-not (eintest:iexec-should-execute-p cell :this-command nil)))) + +(ert-deftest ein:iexec-should-execute-p-beg-too-small () + (ein:testing-with-one-cell 'code + (should-not (eintest:iexec-should-execute-p cell :beg (point-min))))) + +(ert-deftest ein:iexec-should-execute-p-end-too-big () + (ein:testing-with-one-cell 'code + (should-not (eintest:iexec-should-execute-p cell :end (point-max))))) diff --git a/test/test-ein-kernel.el b/test/test-ein-kernel.el new file mode 100644 index 0000000..be6a231 --- /dev/null +++ b/test/test-ein-kernel.el @@ -0,0 +1,80 @@ +(eval-when-compile (require 'cl)) +(require 'ert) + +(require 'ein-kernel) +(require 'ein-testing-kernel) + + +(defun eintest:kernel-new (port) + (ein:kernel-new port "/api/kernels" + (get-buffer-create "*eintest: dummy for kernel test*"))) + +(ert-deftest ein:kernel-start-check-url () + (let* ((kernel (eintest:kernel-new 8888)) + (notebook-id "NOTEBOOK-ID") + (desired-url "http://127.0.0.1:8888/api/sessions") + (dummy-response (make-request-response)) + got-url) + (flet ((request (url &rest ignore) (setq got-url url) dummy-response) + (set-process-query-on-exit-flag (process flag))) + (ein:kernel-start kernel notebook-id) + (should (equal got-url desired-url))))) + +(ert-deftest ein:kernel-restart-check-url () + (let* ((kernel (eintest:kernel-new 8888)) + (kernel-id "KERNEL-ID") + (session-id "SESSION-ID") + (desired-url "http://127.0.0.1:8888/api/kernels/KERNEL-ID/restart") + (dummy-response (make-request-response)) + got-url) + (flet ((request (url &rest ignore) (setq got-url url) dummy-response) + (set-process-query-on-exit-flag (process flag)) + (ein:kernel-stop-channels (&rest ignore)) + (ein:websocket (&rest ignore) (make-ein:$websocket)) + (ein:events-trigger (&rest ignore))) + (ein:kernel--kernel-started + kernel :data (list :ws_url "ws://127.0.0.1:8888" :id kernel-id)) + (ein:kernel-restart kernel) + (should (equal got-url desired-url))))) + + +(ert-deftest ein:kernel-interrupt-check-url () + (let* ((kernel (eintest:kernel-new 8888)) + (kernel-id "KERNEL-ID") + (desired-url "http://127.0.0.1:8888/api/kernels/KERNEL-ID/interrupt") + (dummy-response (make-request-response)) + got-url) + (flet ((request (url &rest ignore) (setq got-url url) dummy-response) + (set-process-query-on-exit-flag (process flag)) + (ein:kernel-stop-channels (&rest ignore)) + (ein:websocket (&rest ignore) (make-ein:$websocket))) + (ein:kernel--kernel-started + kernel :data (list :ws_url "ws://127.0.0.1:8888" :id kernel-id)) + (ein:kernel-interrupt kernel) + (should (equal got-url desired-url))))) + +(ert-deftest ein:kernel-kill-check-url () + (let* ((kernel (eintest:kernel-new 8888)) + (kernel-id "KERNEL-ID") + (desired-url "http://127.0.0.1:8888/api/kernels/KERNEL-ID") + (dummy-response (make-request-response)) + got-url) + (flet ((request (url &rest ignore) (setq got-url url) dummy-response) + (set-process-query-on-exit-flag (process flag)) + (ein:kernel-stop-channels (&rest ignore)) + (ein:websocket (&rest ignore) (make-ein:$websocket))) + (ein:kernel--kernel-started + kernel :data (list :ws_url "ws://127.0.0.1:8888" :id kernel-id)) + (ein:kernel-kill kernel) + (should (equal got-url desired-url))))) + + +;;; Test `ein:kernel-construct-help-string' + +(ert-deftest ein:kernel-construct-help-string-when-found () + (ein:testing-kernel-construct-help-string-loop)) + +(ert-deftest ein:kernel-construct-help-string-when-not-found () + (should (equal (ein:kernel-construct-help-string nil) nil))) +;; Included in `ein:kernel-construct-help-string-when-found', but test +;; it explicitly to be sure. diff --git a/test/test-ein-kill-ring.el b/test/test-ein-kill-ring.el new file mode 100644 index 0000000..977df60 --- /dev/null +++ b/test/test-ein-kill-ring.el @@ -0,0 +1,41 @@ +(eval-when-compile (require 'cl)) +(require 'ert) + +(require 'ein-kill-ring) + +(ert-deftest ein:kill-ring-simple () + (let (ein:kill-ring + ein:kill-ring-yank-pointer) + (ein:kill-new 1) + (should (equal (ein:current-kill 0) 1)))) + +(defun eintest:kill-ring-simple-repeat-setup () + (loop for i from 0 below 5 + do (ein:kill-new i) + do (should (equal (ein:current-kill 0) i)))) + +(ert-deftest ein:kill-ring-simple-repeat () + (let (ein:kill-ring + ein:kill-ring-yank-pointer) + (eintest:kill-ring-simple-repeat-setup) + (should (equal ein:kill-ring ein:kill-ring-yank-pointer)) + (should (equal ein:kill-ring '(4 3 2 1 0))))) + +(ert-deftest ein:kill-ring-repeat-n-1 () + (let (ein:kill-ring + ein:kill-ring-yank-pointer) + (eintest:kill-ring-simple-repeat-setup) + (loop for i in '(3 2 1 0 4 3 2) + do (should (equal (ein:current-kill 1) i))) + (should-not (equal ein:kill-ring ein:kill-ring-yank-pointer)) + (should (equal ein:kill-ring '(4 3 2 1 0))) + (should (equal ein:kill-ring-yank-pointer '(2 1 0))))) + +(ert-deftest ein:kill-ring-exceeds-max () + (let (ein:kill-ring + ein:kill-ring-yank-pointer + (ein:kill-ring-max 3)) + (eintest:kill-ring-simple-repeat-setup) + (should (equal ein:kill-ring ein:kill-ring-yank-pointer)) + (should (equal (length ein:kill-ring) ein:kill-ring-max)) + (should (equal ein:kill-ring '(4 3 2))))) diff --git a/test/test-ein-node.el b/test/test-ein-node.el new file mode 100644 index 0000000..b4cca3b --- /dev/null +++ b/test/test-ein-node.el @@ -0,0 +1,82 @@ +(eval-when-compile (require 'cl)) +(require 'ert) + +(require 'ein-node) + +(defun ein:testing-node-dummy-ewco-node (data) + `[nil nil ,data]) + +(defun ein:testing-node-ewoc-data (ewoc-node) + (ein:$node-data (ewoc-data ewoc-node))) + +(ert-deftest ein:testing-node-dummy-ewco-node () + (let* ((obj "some-object") + (ewoc-node (ein:testing-node-dummy-ewco-node obj))) + (should (eq (ewoc-data ewoc-node) obj)))) + +(ert-deftest ein:node-filter-is () + (let ((en-list (mapcar + #'ein:testing-node-dummy-ewco-node + (list (ein:node-new nil "s" '(spam sag)) + (ein:node-new nil "p" '(spam)) + (ein:node-new nil "e" '(egg)) + (ein:node-new nil "a" '(spam sag)) + (ein:node-new nil "g" '(egg sag)) + (ein:node-new nil "m" '(spam)) + (ein:node-new nil "g" '(egg)))))) + (should (equal (mapcar #'ein:testing-node-ewoc-data + (ein:node-filter en-list :is 'spam)) + '("s" "p" "a" "m"))) + (should (equal (mapcar #'ein:testing-node-ewoc-data + (ein:node-filter en-list :is 'egg)) + '("e" "g" "g"))) + (should (equal (mapcar #'ein:testing-node-ewoc-data + (ein:node-filter en-list :is 'sag)) + '("s" "a" "g"))))) + +(ert-deftest ein:node-filter-not () + (let ((en-list (mapcar + #'ein:testing-node-dummy-ewco-node + (list (ein:node-new nil "s" '(spam sag)) + (ein:node-new nil "p" '(spam)) + (ein:node-new nil "e" '(egg)) + (ein:node-new nil "a" '(spam sag)) + (ein:node-new nil "g" '(egg sag)) + (ein:node-new nil "m" '(spam)) + (ein:node-new nil "g" '(egg)))))) + (should (equal (mapcar #'ein:testing-node-ewoc-data + (ein:node-filter en-list :not 'spam)) + '("e" "g" "g"))) + (should (equal (mapcar #'ein:testing-node-ewoc-data + (ein:node-filter en-list :not 'egg)) + '("s" "p" "a" "m"))) + (should (equal (mapcar #'ein:testing-node-ewoc-data + (ein:node-filter en-list :not 'sag)) + '("p" "e" "m" "g"))))) + +(ert-deftest ein:node-filter-is-and-not () + (let ((en-list (mapcar + #'ein:testing-node-dummy-ewco-node + (list (ein:node-new nil "s" '(spam sag)) + (ein:node-new nil "p" '(spam)) + (ein:node-new nil "e" '(egg)) + (ein:node-new nil "a" '(spam sag)) + (ein:node-new nil "g" '(egg sag)) + (ein:node-new nil "m" '(spam)) + (ein:node-new nil "g" '(egg)))))) + (should (equal (mapcar #'ein:testing-node-ewoc-data + (ein:node-filter en-list :not 'spam :is 'sag)) + '("g"))) + (should (equal (mapcar #'ein:testing-node-ewoc-data + (ein:node-filter en-list :is 'sag :not 'spam)) + '("g"))) + (should (equal (mapcar #'ein:testing-node-ewoc-data + (ein:node-filter en-list :is 'spam :is 'sag)) + '("s" "a"))) + (should (equal (mapcar #'ein:testing-node-ewoc-data + (ein:node-filter en-list :is 'sag :not 'spam + :not 'not-existing)) + '("g"))) + (should (equal (mapcar #'ein:testing-node-ewoc-data + (ein:node-filter en-list :is 'sag :is 'spam)) + '("s" "a"))))) diff --git a/test/test-ein-notebook.el b/test/test-ein-notebook.el new file mode 100644 index 0000000..118ecfe --- /dev/null +++ b/test/test-ein-notebook.el @@ -0,0 +1,1347 @@ +(eval-when-compile (require 'cl)) +(require 'ert) + +(when load-file-name + (add-to-list 'load-path + (concat (file-name-directory load-file-name) "mocker"))) +(require 'mocker) + +(require 'ein-notebook) +(require 'ein-testing-notebook) +(require 'ein-testing-cell) + + +;; Test utils + +;;; This is the content portion of a response fromt he content API. +(defvar eintest:notebook-data-simple-json + "{ + \"name\": \"Untitled0.ipynb\", + \"path\": \"\", + \"type\": \"notebook\", + \"content\" : { + \"metadata\": { + }, + \"nbformat\": 3, + \"nbformat_minor\": 0, + + \"worksheets\": [ + { + \"cells\": [ + { + \"cell_type\": \"code\", + \"collapsed\": false, + \"input\": \"1 + 1\", + \"language\": \"python\", + \"outputs\": [ + { + \"output_type\": \"pyout\", + \"prompt_number\": 1, + \"text\": \"2\" + } + ], + \"prompt_number\": 1 + } + ] + } + ] + } +} +") + +(defvar eintest:notebook-data-simple-json-v4 + "{ + \"metadata\": { + \"name\": \"Untitled0.ipynb\" + }, + \"nbformat\": 4, + \"nbformat_minor\": 0, + \"cells\": [ + { + \"cell_type\": \"code\", + \"metadata\" : { + \"collapsed\": false, + }, + \"source\": \"1 + 1\", + \"outputs\": [ + { + \"name\": \"stdout\", + \"output_type\": \"stream\", + \"text\": \"2\" + } + ], + \"prompt_number\": 1 + } + ] +} +") + + +(defun eintest:notebook-enable-mode (buffer) + (with-current-buffer buffer (ein:notebook-plain-mode) buffer)) + +(defun eintest:kernel-fake-execute-reply (kernel msg-id execution-count) + (let* ((payload nil) + (content (list :execution_count 1 :payload payload)) + (packet (list :header (list :msg_type "execute_reply") + :parent_header (list :msg_id msg-id) + :content content))) + (ein:kernel--handle-shell-reply kernel (json-encode packet)))) + +(defun eintest:kernel-fake-stream (kernel msg-id data) + (let* ((content (list :data data + :name "stdout")) + (packet (list :header (list :msg_type "stream") + :parent_header (list :msg_id msg-id) + :content content))) + (ein:kernel--handle-iopub-reply kernel (json-encode packet)))) + +(defun eintest:check-search-forward-from (start string &optional null-string) + "Search STRING from START and check it is found. +When non-`nil' NULL-STRING is given, it is searched from the +position where the search of the STRING ends and check that it +is not found." + (save-excursion + (goto-char start) + (should (search-forward string nil t)) + (when null-string + (should-not (search-forward null-string nil t))))) + +(defun eintest:cell-check-output (cell regexp) + (save-excursion + (goto-char (ein:cell-location cell :after-input)) + (should (looking-at-p (concat "\\=" regexp "\n"))))) + + +;; from-json + +(ert-deftest ein:notebook-from-json-simple () + (with-current-buffer (ein:testing-notebook-from-json + eintest:notebook-data-simple-json) + (should (ein:$notebook-p ein:%notebook%)) + (should (equal (ein:$notebook-notebook-name ein:%notebook%) "Untitled0.ipynb")) + (should (equal (ein:worksheet-ncells ein:%worksheet%) 1)) + (let ((cell (car (ein:worksheet-get-cells ein:%worksheet%)))) + (should (ein:codecell-p cell)) + (should (equal (oref cell :input) "1 + 1")) + (should (equal (oref cell :input-prompt-number) 1)) + (let ((outputs (oref cell :outputs))) + (should (equal (length outputs) 1)) + (let ((o1 (car outputs))) + (should (equal (plist-get o1 :output_type) "pyout")) + (should (equal (plist-get o1 :prompt_number) 1)) + (should (equal (plist-get o1 :text) "2"))))))) + +(ert-deftest ein:notebook-from-json-empty () + (with-current-buffer (ein:testing-notebook-make-empty) + (should (ein:$notebook-p ein:%notebook%)) + (should (equal (ein:$notebook-notebook-name ein:%notebook%) "Dummy Name.ipynb")) + (should (equal (ein:worksheet-ncells ein:%worksheet%) 0)))) + +(ert-deftest ein:notebook-from-json-all-cell-types () + (with-current-buffer + (ein:testing-notebook-make-new + nil (list (ein:testing-codecell-data "import numpy") + (ein:testing-markdowncell-data "*markdown* text") + (ein:testing-rawcell-data "`raw` cell text") + (ein:testing-htmlcell-data "HTML text") + (ein:testing-headingcell-data "Heading 1" 1) + (ein:testing-headingcell-data "Heading 2" 2) + (ein:testing-headingcell-data "Heading 3" 3) + (ein:testing-headingcell-data "Heading 4" 4) + (ein:testing-headingcell-data "Heading 5" 5) + (ein:testing-headingcell-data "Heading 6" 6))) + (should (ein:$notebook-p ein:%notebook%)) + (should (equal (ein:$notebook-notebook-name ein:%notebook%) "Dummy Name.ipynb")) + (should (equal (ein:worksheet-ncells ein:%worksheet%) 10)) + (let ((cells (ein:worksheet-get-cells ein:%worksheet%))) + (should (ein:codecell-p (nth 0 cells))) + (should (ein:markdowncell-p (nth 1 cells))) + (should (ein:rawcell-p (nth 2 cells))) + (should (ein:htmlcell-p (nth 3 cells))) + (should (equal (ein:cell-get-text (nth 0 cells)) "import numpy")) + (should (equal (ein:cell-get-text (nth 1 cells)) "*markdown* text")) + (should (equal (ein:cell-get-text (nth 2 cells)) "`raw` cell text")) + (should (equal (ein:cell-get-text (nth 3 cells)) "HTML text")) + (loop for i from 4 to 9 + for level from 1 + for cell = (nth i cells) + do (should (ein:headingcell-p cell)) + do (should (equal (ein:cell-get-text cell) + (format "Heading %s" level))) + do (should (= (oref cell :level) level)))))) + + +;;; Destructor + +(defvar ein:testing-notebook-del-args-log 'nolog) + +(defadvice ein:notebook-del (before ein:testing-notebook-del activate) + "Log argument passed to" + (when (listp ein:testing-notebook-del-args-log) + (push (ad-get-args 0) ein:testing-notebook-del-args-log))) + +(defun ein:testing-assert-notebook-del-not-called () + (should-not ein:testing-notebook-del-args-log)) + +(defun ein:testing-assert-notebook-del-called-once-with (notebook) + (should (= (length ein:testing-notebook-del-args-log) 1)) + (mapc (lambda (arg) (should (= (length arg) 1))) + ein:testing-notebook-del-args-log) + (should (eq (caar ein:testing-notebook-del-args-log) notebook))) + +(defun ein:testing-notebook-close-scratchsheet-open-and-close + (num-open num-close) + "Test for closing scratch sheet using `ein:notebook-close-worksheet'. + +1. Open NUM-OPEN scratch sheets. +2. Close an existing worksheet. +3. Close NUM-CLOSE scratch sheets. + +When NUM-OPEN = NUM-CLOSE, notebook should be closed." + (should (> num-open 0)) + (let ((notebook (buffer-local-value 'ein:%notebook% + (ein:testing-notebook-make-empty))) + ein:testing-notebook-del-args-log) + (symbol-macrolet ((ss-list (ein:$notebook-scratchsheets notebook))) + ;; Add scratchsheets. They can be just empty instance for this test. + (dotimes (_ num-open) + (setq ss-list + (append ss-list (list (make-instance 'ein:scratchsheet))))) + ;; Close worksheet + (let ((ws (car (ein:$notebook-worksheets notebook))) + (ein:notebook-kill-buffer-ask nil)) + (ein:notebook-close-worksheet notebook ws) + (kill-buffer (ein:worksheet-buffer ws))) + ;; Make sure adding scratchsheet work. + (should (= (length ss-list) num-open)) + (mapc (lambda (ws) (should (ein:scratchsheet-p ws))) ss-list) + ;; Close scratchsheets + (dotimes (_ num-close) + (ein:notebook-close-worksheet notebook (car ss-list))) + ;; Actual tests: + (should (= (length ss-list) (- num-open num-close))) + (if (= num-open num-close) + (ein:testing-assert-notebook-del-called-once-with notebook) + (ein:testing-assert-notebook-del-not-called))))) + +(ert-deftest ein:notebook-close-scratchsheet/open-one-close-one () + (ein:testing-notebook-close-scratchsheet-open-and-close 1 1)) + +(ert-deftest ein:notebook-close-scratchsheet/open-two-close-two () + (ein:testing-notebook-close-scratchsheet-open-and-close 2 2)) + +(ert-deftest ein:notebook-close-scratchsheet/open-two-close-one () + (ein:testing-notebook-close-scratchsheet-open-and-close 2 1)) + + +;;; Insertion and deletion of cells + +(ert-deftest ein:notebook-insert-cell-below-command-simple () + (with-current-buffer (ein:testing-notebook-make-empty) + (call-interactively #'ein:worksheet-insert-cell-below) + (call-interactively #'ein:worksheet-insert-cell-below) + (call-interactively #'ein:worksheet-insert-cell-below) + (should (equal (ein:worksheet-ncells ein:%worksheet%) 3)))) + +(ert-deftest ein:notebook-insert-cell-above-command-simple () + (with-current-buffer (ein:testing-notebook-make-empty) + (call-interactively #'ein:worksheet-insert-cell-above) + (call-interactively #'ein:worksheet-insert-cell-above) + (call-interactively #'ein:worksheet-insert-cell-above) + (should (equal (ein:worksheet-ncells ein:%worksheet%) 3)))) + +(ert-deftest ein:notebook-delete-cell-command-simple () + (with-current-buffer (ein:testing-notebook-make-empty) + (loop repeat 3 + do (call-interactively #'ein:worksheet-insert-cell-above)) + (should (equal (ein:worksheet-ncells ein:%worksheet%) 3)) + (loop repeat 3 + do (call-interactively #'ein:worksheet-delete-cell)) + (should (equal (ein:worksheet-ncells ein:%worksheet%) 0)))) + +(ert-deftest ein:notebook-delete-cell-command-no-undo () + (with-current-buffer (ein:testing-notebook-make-empty) + (call-interactively #'ein:worksheet-insert-cell-above) + (insert "some text") + (should (equal (buffer-string) " +In [ ]: +some text + +")) + (call-interactively #'ein:worksheet-delete-cell) + (should (equal (buffer-string) "\n")) + (should-error (undo)) ; should be ignore-error? + (should (equal (buffer-string) "\n")))) + +(ert-deftest ein:notebook-kill-cell-command-simple () + (with-current-buffer (ein:testing-notebook-make-empty) + (let (ein:kill-ring ein:kill-ring-yank-pointer) + (loop repeat 3 + do (call-interactively #'ein:worksheet-insert-cell-above)) + (should (equal (ein:worksheet-ncells ein:%worksheet%) 3)) + (loop for i from 1 to 3 + do (call-interactively #'ein:worksheet-kill-cell) + do (should (equal (length ein:kill-ring) i)) + do (should (equal (ein:worksheet-ncells ein:%worksheet%) (- 3 i))))))) + +(ert-deftest ein:notebook-copy-cell-command-simple () + (with-current-buffer (ein:testing-notebook-make-empty) + (let (ein:kill-ring ein:kill-ring-yank-pointer) + (loop repeat 3 + do (call-interactively #'ein:worksheet-insert-cell-above)) + (should (equal (ein:worksheet-ncells ein:%worksheet%) 3)) + (loop repeat 3 + do (call-interactively #'ein:worksheet-copy-cell)) + (should (equal (ein:worksheet-ncells ein:%worksheet%) 3)) + (should (equal (length ein:kill-ring) 3))))) + +(ert-deftest ein:notebook-yank-cell-command-simple () + (with-current-buffer (ein:testing-notebook-make-empty) + (let (ein:kill-ring ein:kill-ring-yank-pointer) + (loop repeat 3 + do (call-interactively #'ein:worksheet-insert-cell-above)) + (should (equal (ein:worksheet-ncells ein:%worksheet%) 3)) + (loop repeat 3 + do (call-interactively #'ein:worksheet-kill-cell)) + (should (equal (ein:worksheet-ncells ein:%worksheet%) 0)) + (should (equal (length ein:kill-ring) 3)) + (loop repeat 3 + do (call-interactively #'ein:worksheet-yank-cell)) + (should (equal (ein:worksheet-ncells ein:%worksheet%) 3)) + (loop for cell in (ein:worksheet-get-cells ein:%worksheet%) + do (should (ein:codecell-p cell)) + do (should (slot-boundp cell :kernel)) + do (should (slot-boundp cell :events)))))) + +(ert-deftest ein:notebook-yank-cell-command-two-buffers () + (let (ein:kill-ring ein:kill-ring-yank-pointer) + (with-current-buffer (ein:testing-notebook-make-empty "NB1") + (call-interactively #'ein:worksheet-insert-cell-above) + (should (equal (ein:worksheet-ncells ein:%worksheet%) 1)) + (call-interactively #'ein:worksheet-kill-cell) + (should (equal (ein:worksheet-ncells ein:%worksheet%) 0)) + (flet ((y-or-n-p (&rest ignore) t) + (ein:notebook-del (&rest ignore))) + ;; FIXME: are there anyway to skip confirmation? + (kill-buffer))) + (with-current-buffer (ein:testing-notebook-make-empty "NB2") + (call-interactively #'ein:worksheet-yank-cell) + (should (equal (ein:worksheet-ncells ein:%worksheet%) 1))))) + +(ert-deftest ein:notebook-toggle-cell-type-simple () + (with-current-buffer (ein:testing-notebook-make-empty) + (call-interactively #'ein:worksheet-insert-cell-above) + (insert "some text") + (should (ein:codecell-p (ein:worksheet-get-current-cell))) + (should (slot-boundp (ein:worksheet-get-current-cell) :kernel)) + ;; toggle to markdown + (call-interactively #'ein:worksheet-toggle-cell-type) + (should (ein:markdowncell-p (ein:worksheet-get-current-cell))) + (should (looking-back "some text")) + ;; toggle to raw + (call-interactively #'ein:worksheet-toggle-cell-type) + (should (ein:rawcell-p (ein:worksheet-get-current-cell))) + (should (looking-back "some text")) + ;; toggle to heading + (call-interactively #'ein:worksheet-toggle-cell-type) + (should (ein:headingcell-p (ein:worksheet-get-current-cell))) + (should (looking-back "some text")) + ;; toggle to code + (call-interactively #'ein:worksheet-toggle-cell-type) + (should (ein:codecell-p (ein:worksheet-get-current-cell))) + (should (slot-boundp (ein:worksheet-get-current-cell) :kernel)) + (should (looking-back "some text")))) + +(ert-deftest ein:notebook-change-cell-type-cycle-through () + (with-current-buffer (ein:testing-notebook-make-empty) + (call-interactively #'ein:worksheet-insert-cell-above) + (insert "some text") + ;; start with code cell + (should (ein:codecell-p (ein:worksheet-get-current-cell))) + (should (slot-boundp (ein:worksheet-get-current-cell) :kernel)) + (let ((check + (lambda (type &optional level) + (let ((cell-p (intern (format "ein:%scell-p" type))) + (cell (ein:worksheet-get-current-cell))) + (ein:worksheet-change-cell-type ein:%worksheet% cell + type level t) + (let ((new (ein:worksheet-get-current-cell))) + (should-not (eq new cell)) + (should (funcall cell-p new))) + (should (looking-back "some text")))))) + ;; change type: code (no change) -> markdown -> raw + (loop for type in '("code" "markdown" "raw") + do (funcall check type)) + ;; change level: 1 to 6 + (loop for level from 1 to 6 + do (funcall check "heading" level)) + ;; back to code + (funcall check "code") + (should (slot-boundp (ein:worksheet-get-current-cell) :kernel))))) + +(defun eintest:notebook-split-cell-at-point + (insert-text search-text head-text tail-text &optional no-trim) + "Test `ein:notebook-split-cell-at-point' by the following procedure. + +1. Insert, INSERT-TEXT. +2. Split cell just before SEARCH-TEXT. +3. Check that head cell has HEAD-TEXT. +4. Check that tail cell has TAIL-TEXT. + +NO-TRIM is passed to `ein:notebook-split-cell-at-point'." + (with-current-buffer (ein:testing-notebook-make-empty) + (call-interactively #'ein:worksheet-insert-cell-above) + (insert insert-text) + (when search-text + (search-backward search-text)) + ;; do it + (let ((current-prefix-arg no-trim)) + (call-interactively #'ein:worksheet-split-cell-at-point)) + ;; check the "tail" cell + (let ((cell (ein:worksheet-get-current-cell))) + (ein:cell-goto cell) + (should (equal (ein:cell-get-text cell) tail-text)) + (should (ein:codecell-p cell)) + (should (slot-boundp cell :kernel))) + ;; check the "head" cell + (call-interactively #'ein:worksheet-goto-prev-input) + (let ((cell (ein:worksheet-get-current-cell))) + (ein:cell-goto cell) + (should (equal (ein:cell-get-text cell) head-text)) + (should (ein:codecell-p cell)) + (should (slot-boundp cell :kernel))))) + +(ert-deftest ein:notebook-split-cell-at-point-before-newline () + (eintest:notebook-split-cell-at-point + "some\ntext" "text" "some" "text")) + +(ert-deftest ein:notebook-split-cell-at-point-after-newline () + (eintest:notebook-split-cell-at-point + "some\ntext" "\ntext" "some" "text")) + +(ert-deftest ein:notebook-split-cell-at-point-before-newline-no-trim () + (eintest:notebook-split-cell-at-point + "some\ntext" "text" "some\n" "text" t)) + +(ert-deftest ein:notebook-split-cell-at-point-after-newline-no-trim () + (eintest:notebook-split-cell-at-point + "some\ntext" "\ntext" "some" "\ntext" t)) + +(ert-deftest ein:notebook-split-cell-at-point-no-head () + (eintest:notebook-split-cell-at-point + "some" "some" "" "some")) + +(ert-deftest ein:notebook-split-cell-at-point-no-tail () + (eintest:notebook-split-cell-at-point + "some" nil "some" "")) + +(ert-deftest ein:notebook-merge-cell-command-next () + (with-current-buffer (ein:testing-notebook-make-empty) + (call-interactively #'ein:worksheet-insert-cell-above) + (insert "Cell 1") + (call-interactively #'ein:worksheet-insert-cell-above) + (insert "Cell 0") + (let ((current-prefix-arg t)) + (call-interactively #'ein:worksheet-merge-cell)) + (ein:cell-goto (ein:worksheet-get-current-cell)) + (should (looking-at "Cell 0\nCell 1")))) + +(ert-deftest ein:notebook-merge-cell-command-prev () + (with-current-buffer (ein:testing-notebook-make-empty) + (call-interactively #'ein:worksheet-insert-cell-below) + (insert "Cell 0") + (call-interactively #'ein:worksheet-insert-cell-below) + (insert "Cell 1") + (call-interactively #'ein:worksheet-merge-cell) + (ein:cell-goto (ein:worksheet-get-current-cell)) + (should (looking-at "Cell 0\nCell 1")))) + + +;;; Cell selection. + +(ert-deftest ein:notebook-goto-next-input-command-simple () + (with-current-buffer (ein:testing-notebook-make-empty) + (loop for i downfrom 2 to 0 + do (call-interactively #'ein:worksheet-insert-cell-above) + do (insert (format "Cell %s" i))) + (should (equal (ein:worksheet-ncells ein:%worksheet%) 3)) + ;; (message "%s" (buffer-string)) + (loop for i from 0 below 2 + do (beginning-of-line) ; This is required, I need to check why + do (should (looking-at (format "Cell %s" i))) + do (call-interactively #'ein:worksheet-goto-next-input) + do (should (looking-at (format "Cell %s" (1+ i))))))) + +(ert-deftest ein:notebook-goto-prev-input-command-simple () + (with-current-buffer (ein:testing-notebook-make-empty) + (loop for i from 0 below 3 + do (call-interactively #'ein:worksheet-insert-cell-below) + do (insert (format "Cell %s" i))) + (should (equal (ein:worksheet-ncells ein:%worksheet%) 3)) + ;; (message "%s" (buffer-string)) + (loop for i downfrom 2 to 1 + do (beginning-of-line) ; This is required, I need to check why + do (should (looking-at (format "Cell %s" i))) + do (call-interactively #'ein:worksheet-goto-prev-input) + do (should (looking-at (format "Cell %s" (1- i))))))) + +(defun ein:testing-beginning-of-cell-input (num-cells + initial-point + before-callback + arg + should-looking-at-this) + (with-current-buffer (ein:testing-notebook-make-empty) + (ein:testing-insert-cells-with-format num-cells) + (goto-char (point-min)) + (search-forward initial-point) + (when before-callback (funcall before-callback)) + (should-not (looking-at-p should-looking-at-this)) + (ein:worksheet-beginning-of-cell-input arg) + (should (looking-at-p should-looking-at-this)))) + +(ert-deftest ein:worksheet-beginning-of-cell-input-with-no-arg () + (ein:testing-beginning-of-cell-input 1 "Cell 0" nil nil "Cell 0")) + +(ert-deftest ein:worksheet-beginning-of-cell-input-with-arg-two () + (ein:testing-beginning-of-cell-input 2 "Cell 1" nil 2 "Cell 0")) + +(ert-deftest ein:worksheet-beginning-of-cell-input-with-arg-minus-one () + (ein:testing-beginning-of-cell-input 2 "Cell 0" nil -1 "Cell 1")) + +(ert-deftest ein:worksheet-beginning-of-cell-input-with-no-arg-at-footer () + (ein:testing-beginning-of-cell-input 1 "Cell 0" + #'forward-line + nil "Cell 0")) + +(ert-deftest ein:worksheet-beginning-of-cell-input-with-arg-two-at-footer () + (ein:testing-beginning-of-cell-input 2 "Cell 1" + #'forward-line + 2 "Cell 0")) + +(ert-deftest ein:worksheet-beginning-of-cell-input-with-arg-minus-one-at-footer + () + (ein:testing-beginning-of-cell-input 2 "Cell 0" + #'forward-line + -1 "Cell 1")) + +(ert-deftest ein:worksheet-beginning-of-cell-input-with-no-arg-at-prompt () + (ein:testing-beginning-of-cell-input 2 "Cell 1" + (lambda () (forward-line -1)) + nil "Cell 0")) + +(ert-deftest ein:worksheet-beginning-of-cell-input-with-arg-two-at-prompt () + (ein:testing-beginning-of-cell-input 2 "Cell 1" + (lambda () (forward-line -1)) + 2 "Cell 0")) + +(ert-deftest ein:worksheet-beginning-of-cell-input-with-arg-minus-one-at-prompt + () + (ein:testing-beginning-of-cell-input 2 "Cell 0" + ;; I need two cells to make it fail + ;; without (forward-line -1) + (lambda () (forward-line -1)) + -1 "Cell 0")) + +(ert-deftest ein:worksheet-beginning-of-cell-input-repeat () + (with-current-buffer (ein:testing-notebook-make-empty) + (ein:testing-insert-cells-with-format 3) + (goto-char (point-min)) + (search-forward "Cell 2") + (should-not (looking-at-p "Cell 2")) + (ein:worksheet-beginning-of-cell-input) + (should (looking-at-p "Cell 2")) + (should-not (looking-at-p "Cell 1")) + (ein:worksheet-beginning-of-cell-input) + (should (looking-at-p "Cell 1")) + (should-not (looking-at-p "Cell 0")) + (ein:worksheet-beginning-of-cell-input) + (should (looking-at-p "Cell 0")))) + +(defun ein:testing-end-of-cell-input (num-cells + initial-point + before-callback + arg + should-looking-back-this) + (with-current-buffer (ein:testing-notebook-make-empty) + (ein:testing-insert-cells-with-format num-cells) + (goto-char (point-min)) + (search-forward initial-point) + (beginning-of-line) + (when before-callback (funcall before-callback)) + (should-not (looking-back should-looking-back-this)) + (ein:worksheet-end-of-cell-input arg) + (should (looking-back should-looking-back-this)))) + +(ert-deftest ein:worksheet-end-of-cell-input-with-no-arg () + (ein:testing-end-of-cell-input 1 "Cell 0" nil nil "Cell 0")) + +(ert-deftest ein:worksheet-end-of-cell-input-with-arg-two () + (ein:testing-end-of-cell-input 2 "Cell 0" nil 2 "Cell 1")) + +(ert-deftest ein:worksheet-end-of-cell-input-with-arg-minus-one () + (ein:testing-end-of-cell-input 2 "Cell 1" nil -1 "Cell 0")) + +(ert-deftest ein:worksheet-end-of-cell-input-with-no-arg-at-footer () + (ein:testing-end-of-cell-input 2 "Cell 0" + #'forward-line + nil "Cell 1")) + +(ert-deftest ein:worksheet-end-of-cell-input-with-arg-two-at-footer () + (ein:testing-end-of-cell-input 3 "Cell 0" + #'forward-line + 2 "Cell 2")) + +(ert-deftest ein:worksheet-end-of-cell-input-with-arg-minus-one-at-footer + () + (ein:testing-end-of-cell-input 2 "Cell 0" + #'forward-line + -1 "Cell 0")) + +(ert-deftest ein:worksheet-end-of-cell-input-with-no-arg-at-prompt () + (ein:testing-end-of-cell-input 2 "Cell 1" + (lambda () (forward-line -1)) + nil "Cell 1")) + +(ert-deftest ein:worksheet-end-of-cell-input-with-arg-two-at-prompt () + (ein:testing-end-of-cell-input 3 "Cell 0" + (lambda () (forward-line -1)) + 2 "Cell 1")) + +(ert-deftest ein:worksheet-end-of-cell-input-with-arg-minus-one-at-prompt + () + (ein:testing-end-of-cell-input 2 "Cell 1" + (lambda () (forward-line -1)) + -1 "Cell 0")) + + +;;; Cell movement + +(ert-deftest ein:notebook-move-cell-up-command-simple () + (with-current-buffer (ein:testing-notebook-make-empty) + (loop for i from 0 below 3 + do (call-interactively #'ein:worksheet-insert-cell-below) + do (insert (format "Cell %s" i))) + (beginning-of-line) + (should (looking-at "Cell 2")) + (loop repeat 2 + do (call-interactively #'ein:worksheet-move-cell-up)) + ;; (message "%s" (buffer-string)) + (beginning-of-line) + (should (looking-at "Cell 2")) + (should (search-forward "Cell 0" nil t)) + (should (search-forward "Cell 1" nil t)) + (should-not (search-forward "Cell 2" nil t)))) + +(ert-deftest ein:notebook-move-cell-down-command-simple () + (with-current-buffer (ein:testing-notebook-make-empty) + (loop for i from 0 below 3 + do (call-interactively #'ein:worksheet-insert-cell-above) + do (insert (format "Cell %s" i))) + (loop repeat 2 + do (call-interactively #'ein:worksheet-move-cell-down)) + (beginning-of-line) + (should (looking-at "Cell 2")) + (should (search-backward "Cell 0" nil t)) + (should (search-backward "Cell 1" nil t)) + (should-not (search-backward "Cell 2" nil t)))) + + +;;; Cell collapsing and output clearing + +(ert-deftest ein:worksheet-toggle-output () + (with-current-buffer (ein:testing-notebook-make-empty) + (let ((cell (call-interactively #'ein:worksheet-insert-cell-below))) + (should-not (oref cell :collapsed)) + (call-interactively #'ein:worksheet-toggle-output) + (should (oref cell :collapsed)) + (call-interactively #'ein:worksheet-toggle-output) + (should-not (oref cell :collapsed))))) + +(defun ein:testing-insert-cells (list-type-or-cell &optional pivot callback) + (loop with ws = ein:%worksheet% + with cell = pivot + for type in list-type-or-cell + for i from 0 + do (setq cell (ein:worksheet-insert-cell-below ws type cell t)) + if callback + do (funcall callback i cell))) + +(defun* ein:testing-insert-cells-with-format (num &optional + (format "Cell %s") + (type 'code)) + (ein:testing-insert-cells (loop repeat num collect type) + nil + (lambda (i &rest _) (insert (format format i)))) + (should (equal (ein:worksheet-ncells ein:%worksheet%) num))) + +(defun ein:testing-test-output-visibility-all (collapsed) + (mapc (lambda (cell) (should (eq (oref cell :collapsed) collapsed))) + (ein:worksheet-get-cells ein:%worksheet%))) + +(ert-deftest ein:worksheet-set-output-visibility-all/visible-from-all-hidden () + (with-current-buffer (ein:testing-notebook-make-empty) + ;; Prepare cells + (ein:testing-insert-cells '(code code code)) + (mapc (lambda (cell) (ein:cell-set-collapsed cell nil)) + (ein:worksheet-get-cells ein:%worksheet%)) + ;; Call the command + (call-interactively #'ein:worksheet-set-output-visibility-all) + ;; Check it worked + (ein:testing-test-output-visibility-all nil))) + +(ert-deftest ein:worksheet-set-output-visibility-all/hidden-from-all-visible () + (with-current-buffer (ein:testing-notebook-make-empty) + ;; Prepare cells + (ein:testing-insert-cells '(code code code)) + ;; Call the command + (let ((current-prefix-arg t)) + (call-interactively #'ein:worksheet-set-output-visibility-all)) + ;; Check it worked + (ein:testing-test-output-visibility-all t))) + +(ert-deftest ein:worksheet-set-output-visibility-all/visible-from-part-hidden () + (with-current-buffer (ein:testing-notebook-make-empty) + ;; Prepare cells + (ein:testing-insert-cells '(code code code)) + (ein:cell-set-collapsed + (nth 1 (ein:worksheet-get-cells ein:%worksheet%)) nil) + ;; Call the command + (call-interactively #'ein:worksheet-set-output-visibility-all) + ;; Check it worked + (ein:testing-test-output-visibility-all nil))) + +(ert-deftest ein:worksheet-set-output-visibility-all/hidden-from-part-visible () + (with-current-buffer (ein:testing-notebook-make-empty) + ;; Prepare cells + (ein:testing-insert-cells '(code code code)) + (ein:cell-set-collapsed + (nth 1 (ein:worksheet-get-cells ein:%worksheet%)) nil) + ;; Call the command + (call-interactively #'ein:worksheet-set-output-visibility-all) + (let ((current-prefix-arg t)) + (call-interactively #'ein:worksheet-set-output-visibility-all)) + ;; Check it worked + (ein:testing-test-output-visibility-all t))) + +(defun ein:testing-assert-cell-output-num (cell num-outputs) + (should (ein:codecell-p cell)) + (should (= (length (oref cell :outputs)) num-outputs))) + +(ert-deftest ein:worksheet-clear-output/simple () + (with-current-buffer + (ein:testing-make-notebook-with-outputs '(("'cell output'") + ("'cell output'"))) + (should (= (ein:worksheet-ncells ein:%worksheet%) 2)) + (let* ((cells (ein:worksheet-get-cells ein:%worksheet%))) + (ein:testing-assert-cell-output-num (nth 0 cells) 1) + (ein:testing-assert-cell-output-num (nth 1 cells) 1) + (ein:cell-goto (nth 0 cells)) + (call-interactively #'ein:worksheet-clear-output) + (ein:testing-assert-cell-output-num (nth 0 cells) 0) ; cleared + (ein:testing-assert-cell-output-num (nth 1 cells) 1)))) + +(ert-deftest ein:worksheet-clear-output/preserve-input-prompt () + (with-current-buffer + (ein:testing-make-notebook-with-outputs '(("'cell output'") + ("'cell output'") + ("'cell output'"))) + (should (= (ein:worksheet-ncells ein:%worksheet%) 3)) + (let* ((cells (ein:worksheet-get-cells ein:%worksheet%))) + (ein:cell-set-input-prompt (nth 0 cells) 111) + (ein:cell-set-input-prompt (nth 1 cells) 222) + (ein:cell-set-input-prompt (nth 2 cells) 333) + ;; Call `ein:worksheet-clear-output' with/without prefix argument. + (ein:cell-goto (nth 0 cells)) + (call-interactively #'ein:worksheet-clear-output) + (ein:cell-goto (nth 2 cells)) + (let ((current-prefix-arg '(4))) + (call-interactively #'ein:worksheet-clear-output)) + ;; Check cells' prompt number + (should (eq (oref (nth 0 cells) :input-prompt-number) nil)) + (should (eq (oref (nth 1 cells) :input-prompt-number) 222)) + (should (eq (oref (nth 2 cells) :input-prompt-number) 333))))) + +(ert-deftest ein:worksheet-clear-all-output/simple () + (with-current-buffer + (ein:testing-make-notebook-with-outputs '(("'cell output'") + ("'cell output'"))) + (should (= (ein:worksheet-ncells ein:%worksheet%) 2)) + (let* ((cells (ein:worksheet-get-cells ein:%worksheet%))) + (ein:testing-assert-cell-output-num (nth 0 cells) 1) + (ein:testing-assert-cell-output-num (nth 1 cells) 1) + (call-interactively #'ein:worksheet-clear-all-output) + (ein:testing-assert-cell-output-num (nth 0 cells) 0) + (ein:testing-assert-cell-output-num (nth 1 cells) 0)))) + + +;; Kernel related things + +(defun eintest:notebook-check-kernel-and-codecell (kernel cell) + (should (ein:$kernel-p kernel)) + (should (ein:codecell-p cell)) + (should (ein:$kernel-p (oref cell :kernel)))) + +(defun eintest:notebook-fake-execution (kernel text msg-id callbacks) + (mocker-let ((ein:kernel-execute + (kernel code callbacks kwd-silent silent) + ((:input (list kernel text callbacks :silent nil)))) + (ein:kernel-live-p + (kernel) + ((:input (list kernel) :output t)))) + (call-interactively #'ein:worksheet-execute-cell)) + (ein:kernel-set-callbacks-for-msg kernel msg-id callbacks)) + +(ert-deftest ein:notebook-execute-current-cell () + (with-current-buffer (ein:testing-notebook-make-empty) + (call-interactively #'ein:worksheet-insert-cell-below) + (let* ((text "print 'Hello World'") + (cell (ein:worksheet-get-current-cell)) + (kernel (ein:$notebook-kernel ein:%notebook%)) + (msg-id "DUMMY-MSG-ID") + (callbacks (ein:cell-make-callbacks cell))) + (eintest:notebook-check-kernel-and-codecell kernel cell) + ;; Execute + (insert text) + (eintest:notebook-fake-execution kernel text msg-id callbacks) + ;; Execute reply + (should-error (eintest:check-search-forward-from (point-min) "In [1]:")) + (eintest:kernel-fake-execute-reply kernel msg-id 1) + (should (= (oref cell :input-prompt-number) 1)) + (eintest:check-search-forward-from (point-min) "In [1]:") + ;; Stream output + (eintest:kernel-fake-stream kernel msg-id "Hello World") + (should (= (ein:cell-num-outputs cell) 1)) + (save-excursion + (goto-char (point-min)) + (should (search-forward "In [1]:" nil t)) + (should (search-forward "print 'Hello World'" nil t)) + (should (search-forward "\nHello World\n" nil t)) ; stream output + (should-not (search-forward "Hello World" nil t)))))) + +(defmacro eintest:worksheet-execute-cell-and-*-deftest + (do-this cell-type has-next-p insert-p) + "Define: +ein:worksheet-execute-cell-and-{DO-THIS}/on-{CELL-TYPE}cell-{no,with}-next + +For example, when `goto-next', \"code\", `nil', `nil' is given, +`ein:worksheet-execute-cell-and-goto-next/on-codecell-no-next' is +defined." + (let ((test-name + (intern (format "ein:worksheet-execute-cell-and-%s/on-%scell-%s" + do-this cell-type + (if has-next-p "with-next" "no-next")))) + (command + (intern (format "ein:worksheet-execute-cell-and-%s" do-this)))) + `(ert-deftest ,test-name () + (with-current-buffer (ein:testing-notebook-make-empty) + (let* ((ws ein:%worksheet%) + (current (ein:worksheet-insert-cell-below ws ,cell-type nil t)) + ,@(when has-next-p + '((next + (ein:worksheet-insert-cell-below ws "code" current))))) + (mocker-let ((ein:worksheet-execute-cell + (ws cell) + (,@(when (equal cell-type "code") + '((:input (list ein:%worksheet% current))))))) + (call-interactively #',command) + (let ((cell (ein:worksheet-get-current-cell))) + (should (eq (ein:cell-prev cell) current)) + ,(when has-next-p + (if insert-p + '(should-not (eq cell next)) + '(should (eq cell next))))))))))) + +(eintest:worksheet-execute-cell-and-*-deftest goto-next "code" nil t ) +(eintest:worksheet-execute-cell-and-*-deftest goto-next "code" t nil) +(eintest:worksheet-execute-cell-and-*-deftest goto-next "markdown" nil t ) +(eintest:worksheet-execute-cell-and-*-deftest goto-next "markdown" t nil) +(eintest:worksheet-execute-cell-and-*-deftest insert-below "code" nil t ) +(eintest:worksheet-execute-cell-and-*-deftest insert-below "code" t t ) +(eintest:worksheet-execute-cell-and-*-deftest insert-below "markdown" nil t ) +(eintest:worksheet-execute-cell-and-*-deftest insert-below "markdown" t t ) + + +;;; Persistence and loading + +(defun ein:testin-notebook-close (num-ws num-ss) + (should (= num-ws 1)) ; currently EIN only supports 1 WS + (should (>= num-ss 0)) + (let ((notebook (buffer-local-value 'ein:%notebook% + (ein:testing-notebook-make-empty))) + ein:testing-notebook-del-args-log) + (dotimes (_ num-ss) + (ein:notebook-scratchsheet-render-new notebook)) + (let ((buffers (ein:notebook-buffer-list notebook))) + (should (= (length buffers) (+ num-ws num-ss))) + (ein:notebook-close notebook) + (mapc (lambda (b) (should-not (buffer-live-p b))) buffers) + (ein:testing-assert-notebook-del-called-once-with notebook)))) + +(ert-deftest ein:notebook-close/one-ws-no-ss () + (ein:testin-notebook-close 1 0)) + +(ert-deftest ein:notebook-close/one-ws-one-ss () + (ein:testin-notebook-close 1 1)) + +(ert-deftest ein:notebook-close/one-ws-five-ss () + (ein:testin-notebook-close 1 5)) + +(defun ein:testing-notebook-data-assert-one-worksheet-one-cell (notebook text) + (let* ((data (ein:notebook-to-json notebook)) + (worksheets (assoc-default 'worksheets data #'eq)) + (cells (assoc-default 'cells (elt worksheets 0) #'eq)) + (cell-0 (elt cells 0)) + (input (assoc-default 'input cell-0 #'eq))) + (should (= (length worksheets) 1)) + (should (= (length cells) 1)) + (should (equal input text)))) + +(defun ein:testing-notebook-data-assert-one-worksheet-no-cell (notebook) + (let* ((data (ein:notebook-to-json notebook)) + (worksheets (assoc-default 'worksheets data #'eq)) + (cells (assoc-default 'cells (elt worksheets 0) #'eq))) + (should (= (length worksheets) 1)) + (should (= (length cells) 0)))) + +(ert-deftest ein:notebook-to-json-after-closing-a-worksheet () + (with-current-buffer (ein:testing-notebook-make-new) + (let ((buffer (current-buffer)) + (notebook ein:%notebook%)) + ;; Edit notebook. + (ein:cell-goto (ein:get-cell-at-point)) + (insert "some text") + (ein:testing-notebook-data-assert-one-worksheet-one-cell notebook + "some text") + (should (ein:notebook-modified-p notebook)) + ;; Open scratch sheet. + (ein:notebook-scratchsheet-open notebook) + ;; Pretend that notebook is saved + (ein:notebook-save-notebook-success notebook) + (should-not (ein:notebook-modified-p notebook)) + ;; Kill a worksheet buffer + (kill-buffer buffer) + (should (ein:notebook-live-p notebook)) + ;; to-json should still work + (ein:testing-notebook-data-assert-one-worksheet-one-cell notebook + "some text")))) + +(ert-deftest ein:notebook-to-json-after-discarding-a-worksheet () + (with-current-buffer (ein:testing-notebook-make-new) + (let ((buffer (current-buffer)) + (notebook ein:%notebook%)) + ;; Edit notebook. + (ein:cell-goto (ein:get-cell-at-point)) + (insert "some text") + (ein:testing-notebook-data-assert-one-worksheet-one-cell notebook + "some text") + (should (ein:notebook-modified-p notebook)) + ;; Open scratch sheet. + (ein:notebook-scratchsheet-open notebook) + ;; Discard a worksheet buffer + (should (ein:notebook-modified-p notebook)) + (let (ein:notebook-kill-buffer-ask) + (kill-buffer buffer)) + (should (ein:notebook-live-p notebook)) + ;; to-json should still work + (ein:testing-notebook-data-assert-one-worksheet-no-cell notebook)))) + +(defun ein:testing-notebook-should-be-closed (notebook buffer) + (should-not (buffer-live-p buffer)) + (should-not (ein:notebook-live-p notebook))) + +(ert-deftest ein:notebook-kill-kernel-then-close-when-its-alive () + (with-current-buffer (ein:testing-notebook-make-new) + (let ((buffer (current-buffer)) + (notebook ein:%notebook%) + (kernel (ein:$notebook-kernel ein:%notebook%)) + (ein:notebook-kill-buffer-ask nil)) + (mocker-let + ((ein:kernel-live-p + (kernel) + ((:input (list kernel) :output t))) + (ein:kernel-kill + (kernel &optional callback cbargs) + ((:input (list kernel #'ein:notebook-close (list notebook)))))) + (call-interactively #'ein:notebook-kill-kernel-then-close-command)) + (should (buffer-live-p buffer)) + ;; Pretend that `ein:notebook-close' is called. + (ein:notebook-close notebook) + (ein:testing-notebook-should-be-closed notebook buffer)))) + +(ert-deftest ein:notebook-kill-kernel-then-close-when-already-dead () + (with-current-buffer (ein:testing-notebook-make-new) + (let ((buffer (current-buffer)) + (notebook ein:%notebook%) + (kernel (ein:$notebook-kernel ein:%notebook%)) + (ein:notebook-kill-buffer-ask nil)) + (mocker-let + ((ein:kernel-live-p + (kernel) + ((:input (list kernel) :output nil)))) + (call-interactively #'ein:notebook-kill-kernel-then-close-command)) + (ein:testing-notebook-should-be-closed notebook buffer)))) + + +;; Notebook undo + +(defun eintest:notebook-undo-after-insert-above () + (with-current-buffer (ein:testing-notebook-make-empty) + (let ((text "some text")) + (call-interactively #'ein:worksheet-insert-cell-above) + (insert text) + (undo-boundary) + (call-interactively #'ein:worksheet-insert-cell-above) + (call-interactively #'ein:worksheet-goto-next-input) + (should (equal (ein:cell-get-text (ein:worksheet-get-current-cell)) text)) + (if (eq ein:worksheet-enable-undo 'full) + (undo) + (should-error (undo))) + (when (eq ein:worksheet-enable-undo 'full) + ;; FIXME: Known bug. (this must succeed.) + (should-error (should (equal (buffer-string) " +In [ ]: + + +In [ ]: + + +"))))))) + +(defun eintest:notebook-undo-after-split () + (with-current-buffer (ein:testing-notebook-make-empty) + (let ((line-1 "first line") + (line-2 "second line")) + (call-interactively #'ein:worksheet-insert-cell-below) + (insert line-1 "\n" line-2) + (undo-boundary) + (move-beginning-of-line 1) + (call-interactively #'ein:worksheet-split-cell-at-point) + (undo-boundary) + (should (equal (ein:cell-get-text (ein:worksheet-get-current-cell)) + line-2)) + (if (eq ein:worksheet-enable-undo 'full) + (undo) + (should-error (undo))) + (when (eq ein:worksheet-enable-undo 'full) + (should (equal (buffer-string) " +In [ ]: + + +In [ ]: +first line +second line + +")))))) + +(defun eintest:notebook-undo-after-merge () + (with-current-buffer (ein:testing-notebook-make-empty) + (let ((line-1 "first line") + (line-2 "second line")) + (call-interactively #'ein:worksheet-insert-cell-below) + (call-interactively #'ein:worksheet-insert-cell-below) + ;; Extra cells to avoid "Changes to be undone are outside visible + ;; portion of buffer" user-error: + (call-interactively #'ein:worksheet-insert-cell-below) + (call-interactively #'ein:worksheet-insert-cell-below) + (goto-char (point-min)) + (call-interactively #'ein:worksheet-goto-next-input) + + (insert line-1) + (undo-boundary) + + (call-interactively #'ein:worksheet-goto-next-input) + (insert line-2) + (undo-boundary) + + (call-interactively #'ein:worksheet-merge-cell) + (undo-boundary) + + (should (equal (ein:cell-get-text (ein:worksheet-get-current-cell)) + (concat line-1 "\n" line-2))) + (if (not (eq ein:worksheet-enable-undo 'full)) + (should-error (undo)) + (undo) + (should (equal (buffer-string) " +In [ ]: +second line + +In [ ]: + + +In [ ]: + + +"))) + (when (eq ein:worksheet-enable-undo 'yes) + ;; FIXME: `undo' should work... + (should-error (undo-more 1))) + (when (eq ein:worksheet-enable-undo 'full) + (undo) + ;; FIXME: Known bug... What should the result be? + (should-error (should (equal (buffer-string) " +In [ ]: + + +In [ ]: + + +In [ ]: + + +"))))))) + +(defun eintest:notebook-undo-after-execution-1-cell () + (with-current-buffer (ein:testing-notebook-make-empty) + (call-interactively #'ein:worksheet-insert-cell-below) + (let* ((text "print 'Hello World'") + (output-text "Hello World\n") + (cell (ein:worksheet-get-current-cell)) + (kernel (ein:$notebook-kernel ein:%notebook%)) + (msg-id "DUMMY-MSG-ID") + (callbacks (ein:cell-make-callbacks cell)) + (check-output + (lambda () + (eintest:cell-check-output cell output-text)))) + (eintest:notebook-check-kernel-and-codecell kernel cell) + ;; Execute + (insert text) + (undo-boundary) + (eintest:notebook-fake-execution kernel text msg-id callbacks) + (ein:kernel-set-callbacks-for-msg kernel msg-id callbacks) + ;; Stream output + (eintest:kernel-fake-stream kernel msg-id output-text) + (funcall check-output) + ;; Undo + (should (equal (ein:cell-get-text cell) text)) + (if (eq ein:worksheet-enable-undo 'full) + (undo) + (should-error (undo))) + (when (eq ein:worksheet-enable-undo 'full) + (should (equal (ein:cell-get-text cell) "")) + ;; FIXME: Known bug. (it must succeed.) + (should-error (funcall check-output)))))) + +(defun eintest:notebook-undo-after-execution-2-cells () + (with-current-buffer (ein:testing-notebook-make-empty) + (call-interactively #'ein:worksheet-insert-cell-below) + (call-interactively #'ein:worksheet-insert-cell-above) + (let* ((text "print 'Hello World\\n' * 10") + (next-text "something") + (output-text + (apply #'concat (loop repeat 10 collect "Hello World\n"))) + (cell (ein:worksheet-get-current-cell)) + (next-cell (ein:cell-next cell)) + (kernel (ein:$notebook-kernel ein:%notebook%)) + (msg-id "DUMMY-MSG-ID") + (callbacks (ein:cell-make-callbacks cell)) + (check-output + (lambda () + (eintest:cell-check-output cell output-text)))) + (eintest:notebook-check-kernel-and-codecell kernel cell) + ;; Execute + (insert text) + (undo-boundary) + (let ((pos (point))) + ;; Do not use `save-excursion' because it does not record undo. + (call-interactively #'ein:worksheet-goto-next-input) + (insert next-text) + (undo-boundary) + (goto-char pos)) + (eintest:notebook-fake-execution kernel text msg-id callbacks) + (ein:kernel-set-callbacks-for-msg kernel msg-id callbacks) + ;; Stream output + (eintest:kernel-fake-stream kernel msg-id output-text) + (funcall check-output) + ;; Undo + (should (equal (ein:cell-get-text cell) text)) + (should (equal (ein:cell-get-text next-cell) next-text)) + (if (eq ein:worksheet-enable-undo 'full) + (undo) + (should-error (undo))) + (when (eq ein:worksheet-enable-undo 'full) + (should (equal (ein:cell-get-text cell) text)) + ;; FIXME: Known bug. (these two must succeed.) + (should-error (should (equal (ein:cell-get-text next-cell) ""))) + (should-error (funcall check-output)))))) + +(defmacro eintest:notebook-undo-make-tests (name) + "Define three tests ein:NANE/no, ein:NANE/yes and ein:NANE/full +from a function named eintest:NAME where `no'/`yes'/`full' is the +value of `ein:worksheet-enable-undo'." + (let ((func (intern (format "eintest:%s" name))) + (test/no (intern (format "ein:%s/no" name))) + (test/yes (intern (format "ein:%s/yes" name))) + (test/full (intern (format "ein:%s/full" name)))) + `(progn + (ert-deftest ,test/no () + (let ((ein:worksheet-enable-undo 'no)) + (,func))) + (ert-deftest ,test/yes () + (let ((ein:worksheet-enable-undo 'yes)) + (,func))) + (ert-deftest ,test/full () + (let ((ein:worksheet-enable-undo 'full)) + (,func)))))) + +(eintest:notebook-undo-make-tests notebook-undo-after-insert-above) +(eintest:notebook-undo-make-tests notebook-undo-after-split) +(eintest:notebook-undo-make-tests notebook-undo-after-merge) +(eintest:notebook-undo-make-tests notebook-undo-after-execution-1-cell) +(eintest:notebook-undo-make-tests notebook-undo-after-execution-2-cells) + +(ert-deftest ein:notebook-undo-via-events () + (with-current-buffer (ein:testing-notebook-make-empty) + (call-interactively #'ein:worksheet-insert-cell-below) + (loop with events = (ein:$notebook-events ein:%notebook%) + for ein:worksheet-enable-undo in '(no yes full) do + (let ((buffer-undo-list '(dummy)) + (cell (ein:worksheet-get-current-cell))) + (with-temp-buffer + (should-not (equal buffer-undo-list '(dummy))) + (ein:events-trigger events 'maybe_reset_undo.Worksheet cell)) + (if (eq ein:worksheet-enable-undo 'yes) + (should (equal buffer-undo-list nil)) + (should (equal buffer-undo-list '(dummy)))))))) + + +;; Generic getter + +(ert-deftest ein:get-url-or-port--notebook () + (with-current-buffer (ein:testing-notebook-make-empty) + (should (equal (ein:get-url-or-port) "DUMMY-URL")))) + +(ert-deftest ein:get-notebook--notebook () + (with-current-buffer (ein:testing-notebook-make-empty) + (should (eq (ein:get-notebook) ein:%notebook%)))) + +(ert-deftest ein:get-kernel--notebook () + (with-current-buffer (ein:testing-notebook-make-empty) + (let ((kernel (ein:$notebook-kernel ein:%notebook%))) + (should (ein:$kernel-p kernel)) + (should (eq (ein:get-kernel) kernel))))) + +(ert-deftest ein:get-cell-at-point--notebook () + (with-current-buffer (ein:testing-notebook-make-empty) + ;; FIXME: write test with non-empty worksheet + (should-not (ein:get-cell-at-point)))) + +(ert-deftest ein:get-traceback-data--notebook () + (with-current-buffer (ein:testing-notebook-make-empty) + ;; FIXME: write test with non-empty TB + (should-not (ein:get-traceback-data)))) + + +;; Notebook mode + +(ert-deftest ein:notebook-ask-before-kill-emacs-simple () + (let ((ein:notebook--opened-map (make-hash-table :test 'equal))) + (should (ein:notebook-ask-before-kill-emacs)) + (with-current-buffer + (eintest:notebook-enable-mode + (ein:testing-notebook-make-empty "Modified Notebook.ipynb")) + (call-interactively #'ein:worksheet-insert-cell-below) + (should (ein:notebook-modified-p))) + (with-current-buffer + (eintest:notebook-enable-mode + (ein:testing-notebook-make-empty "Saved Notebook.ipynb")) + (ein:notebook-save-notebook-success ein:%notebook%) + (should-not (ein:notebook-modified-p))) + (flet ((y-or-n-p (&rest ignore) t) + (ein:notebook-del (&rest ignore))) + (kill-buffer + (eintest:notebook-enable-mode + (ein:testing-notebook-make-empty "Killed Notebook.ipynb")))) + (should (gethash '("DUMMY-URL" "Modified Notebook.ipynb") ein:notebook--opened-map)) + (should (gethash '("DUMMY-URL" "Saved Notebook.ipynb") ein:notebook--opened-map)) + (should (gethash '("DUMMY-URL" "Killed Notebook.ipynb") ein:notebook--opened-map)) + (should (= (hash-table-count ein:notebook--opened-map) 3)) + (mocker-let ((y-or-n-p + (prompt) + ((:input '("You have 1 unsaved notebook(s). Discard changes?") + :output t)))) + (should (ein:notebook-ask-before-kill-emacs))))) + + +;;; Buffer and kill hooks + +(ert-deftest ein:notebook-ask-before-kill-buffer/no-ein-buffer () + (with-temp-buffer + (mocker-let ((y-or-n-p (prompt) ())) + (should (ein:notebook-ask-before-kill-buffer))))) + +(ert-deftest ein:notebook-ask-before-kill-buffer/new-notebook () + (with-current-buffer (ein:testing-make-notebook-with-outputs '(nil)) + (mocker-let ((y-or-n-p (prompt) ())) + (should (ein:notebook-ask-before-kill-buffer))))) + +(ert-deftest ein:notebook-ask-before-kill-buffer/modified-notebook () + (with-current-buffer (ein:testing-make-notebook-with-outputs '(nil)) + (call-interactively #'ein:worksheet-insert-cell-below) + (mocker-let ((y-or-n-p + (prompt) + ((:input '("You have unsaved changes. Discard changes?") + :output t)))) + (should (ein:notebook-ask-before-kill-buffer))))) + +(ert-deftest ein:notebook-ask-before-kill-buffer/modified-scratchsheet () + (with-current-buffer (ein:testing-make-notebook-with-outputs '(nil)) + (with-current-buffer (ein:worksheet-buffer + (ein:notebook-scratchsheet-open ein:%notebook%)) + (should (= (ein:worksheet-ncells ein:%worksheet%) 1)) + (call-interactively #'ein:worksheet-insert-cell-below) + (should (= (ein:worksheet-ncells ein:%worksheet%) 2)) + (should (ein:worksheet-modified-p ein:%worksheet%)) + (mocker-let ((y-or-n-p (prompt) ())) + (should (ein:notebook-ask-before-kill-buffer)))) + (should-not (ein:worksheet-modified-p ein:%worksheet%)) + (mocker-let ((y-or-n-p (prompt) ())) + (should (ein:notebook-ask-before-kill-buffer))))) + + +;; Misc unit tests + +(ert-deftest ein:notebook-test-notebook-name-simple () + (should-not (ein:notebook-test-notebook-name nil)) + (should-not (ein:notebook-test-notebook-name "")) + (should-not (ein:notebook-test-notebook-name "/")) + (should-not (ein:notebook-test-notebook-name "\\")) + (should-not (ein:notebook-test-notebook-name ":")) + (should-not (ein:notebook-test-notebook-name "a/b")) + (should-not (ein:notebook-test-notebook-name "a\\b")) + (should-not (ein:notebook-test-notebook-name "a:b")) + (should (ein:notebook-test-notebook-name "This is a OK notebook name"))) + +(defun* eintest:notebook--check-nbformat (&optional orig_nbformat + orig_nbformat_minor + nbformat + nbformat_minor + &key data) + (let ((data (or data + (list :nbformat nbformat :nbformat_minor nbformat_minor + :orig_nbformat orig_nbformat + :orig_nbformat_minor orig_nbformat_minor)))) + (ein:notebook--check-nbformat data))) + +(ert-deftest ein:notebook--check-nbformat-nothing () + (mocker-let ((ein:display-warning (message) ())) + (eintest:notebook--check-nbformat) + (eintest:notebook--check-nbformat :data nil) + (eintest:notebook--check-nbformat 2 0) + (eintest:notebook--check-nbformat 2 0 2) + (eintest:notebook--check-nbformat 2 0 2 0))) + +(defmacro ein:notebook--check-nbformat-assert-match (regexp &rest args) + `(mocker-let ((ein:display-warning + (message) + ((:input-matcher + (lambda (m) (string-match ,regexp m)))))) + (eintest:notebook--check-nbformat ,@args))) + +(ert-deftest ein:notebook--check-nbformat-warn-major () + (ein:notebook--check-nbformat-assert-match "v2 -> v3" 2 nil 3) + (ein:notebook--check-nbformat-assert-match "v2 -> v3" 2 0 3 0)) + +(ert-deftest ein:notebook--check-nbformat-warn-minor () + (ein:notebook--check-nbformat-assert-match + "version v2\\.1, [^\\.]* up to v2.0" 2 1 2 0)) diff --git a/test/test-ein-notebooklist.el b/test/test-ein-notebooklist.el new file mode 100644 index 0000000..e6033a1 --- /dev/null +++ b/test/test-ein-notebooklist.el @@ -0,0 +1,24 @@ +(require 'ein-notebooklist) + +(defun eintest:notebooklist-make-empty (&optional url-or-port) + "Make empty notebook list buffer." + (ein:notebooklist-url-retrieve-callback (or url-or-port "DUMMY-URL") + (ein:query-ipython-version) + "")) + +(defmacro eintest:notebooklist-is-empty-context-of (func) + `(ert-deftest ,(intern (format "%s--notebooklist" func)) () + (with-current-buffer (eintest:notebooklist-make-empty) + (should-not (,func))))) + + +;; Generic getter + +(ert-deftest ein:get-url-or-port--notebooklist () + (with-current-buffer (eintest:notebooklist-make-empty "DUMMY-URL") + (should (equal (ein:get-url-or-port) "DUMMY-URL")))) + +(eintest:notebooklist-is-empty-context-of ein:get-notebook) +(eintest:notebooklist-is-empty-context-of ein:get-kernel) +(eintest:notebooklist-is-empty-context-of ein:get-cell-at-point) +(eintest:notebooklist-is-empty-context-of ein:get-traceback-data) diff --git a/test/test-ein-notification.el b/test/test-ein-notification.el new file mode 100644 index 0000000..9ee3ad3 --- /dev/null +++ b/test/test-ein-notification.el @@ -0,0 +1,79 @@ +(eval-when-compile (require 'cl)) +(require 'ert) + +(require 'ein-notification) + +(defun ein:testing-notification-tab-mock () + (make-instance 'ein:notification-tab + :get-list (lambda () '(a b c)) + :get-current (lambda () 'a) + :get-name #'ignore)) + +(ert-deftest ein:header-line-normal () + (let* ((ein:%notification% (ein:notification "NotificationTest")) + (kernel (oref ein:%notification% :kernel))) + (oset ein:%notification% :tab (ein:testing-notification-tab-mock)) + (should (equal (ein:header-line) + "IP[y]: /1\\ /2\\ /3\\ [+]")))) + +(ert-deftest ein:header-line-kernel-status-busy () + (let* ((ein:%notification% (ein:notification "NotificationTest")) + (kernel (oref ein:%notification% :kernel))) + (oset ein:%notification% :tab (ein:testing-notification-tab-mock)) + (ein:notification-status-set kernel + 'status_busy.Kernel) + (should (equal (ein:header-line) + "IP[y]: Kernel is busy... | /1\\ /2\\ /3\\ [+]")))) + +(ert-deftest ein:header-line-notebook-status-busy () + (let* ((ein:%notification% (ein:notification "NotificationTest")) + (notebook (oref ein:%notification% :notebook))) + (oset ein:%notification% :tab (ein:testing-notification-tab-mock)) + (ein:notification-status-set notebook + 'notebook_saved.Notebook) + (should (equal (ein:header-line) + "IP[y]: Notebook is saved | /1\\ /2\\ /3\\ [+]")))) + +(ert-deftest ein:header-line-notebook-complex () + (let* ((ein:%notification% (ein:notification "NotificationTest")) + (kernel (oref ein:%notification% :kernel)) + (notebook (oref ein:%notification% :notebook))) + (oset ein:%notification% :tab (ein:testing-notification-tab-mock)) + (ein:notification-status-set kernel + 'status_dead.Kernel) + (ein:notification-status-set notebook + 'notebook_saving.Notebook) + (should (equal + (ein:header-line) + (concat "IP[y]: Saving Notebook... | " + "Kernel is dead. Need restart. | " + "/1\\ /2\\ /3\\ [+]"))))) + +(ert-deftest ein:notification-and-events () + (let* ((notification (ein:notification "NotificationTest")) + (kernel (oref notification :kernel)) + (notebook (oref notification :notebook)) + (events (ein:events-new)) + (event-symbols + '(notebook_saved.Notebook + notebook_saving.Notebook + notebook_save_failed.Notebook + execution_count.Kernel + status_restarting.Kernel + status_idle.Kernel + status_busy.Kernel + status_dead.Kernel + )) + (callbacks (oref events :callbacks))) + (ein:notification-bind-events notification events) + (mapc (lambda (s) (should (gethash s callbacks))) event-symbols) + (should (= (hash-table-count callbacks) (length event-symbols))) + (should (equal (oref kernel :status) nil)) + (loop for et in (mapcar #'car (oref kernel :s2m)) + do (ein:events-trigger events et) + do (should (equal (oref kernel :status) et)) + do (should (equal (oref notebook :status) nil))) + (loop for et in (mapcar #'car (oref notebook :s2m)) + do (ein:events-trigger events et) + do (should (equal (oref kernel :status) 'status_dead.Kernel)) + do (should (equal (oref notebook :status) et))))) diff --git a/test/test-ein-output-area.el b/test/test-ein-output-area.el new file mode 100644 index 0000000..688a81f --- /dev/null +++ b/test/test-ein-output-area.el @@ -0,0 +1,44 @@ +(eval-when-compile (require 'cl)) +(require 'ert) + +(require 'ein-output-area) + +(defun ein:testing-insert-html--fix-urls-do-test (source desired) + (setq source (ein:xml-parse-html-string source)) + (setq desired (ein:xml-parse-html-string desired)) + (ein:insert-html--fix-urls source 8888) + (should (equal source desired))) + +(defmacro ein:testing-insert-html--fix-urls-deftests (args-list) + `(progn + ,@(loop for i from 0 + for args in args-list + for test = (intern (format "ein:insert-html--fix-urls/%s" i)) + collect + `(ert-deftest ,test () + (ein:testing-insert-html--fix-urls-do-test ,@args))))) + +(when (require 'shr nil t) + (ein:testing-insert-html--fix-urls-deftests + (;; Simple replaces + ("text" + "text") + ("text" + "text") + ("" + "") + ("" + "") + ;; Do not modify dom in these cases: + ("text" + "text") + ("text" + "text") + ("" + "") + ;; Bit more complicated cases: + ("

link normal

" + "

link normal

") + (" normal

" + " normal

") + ))) diff --git a/test/test-ein-pytools.el b/test/test-ein-pytools.el new file mode 100644 index 0000000..e38a30c --- /dev/null +++ b/test/test-ein-pytools.el @@ -0,0 +1,27 @@ +(require 'ert) + +(when load-file-name + (add-to-list 'load-path + (concat (file-name-directory load-file-name) "mocker"))) +(require 'mocker) + +(require 'ein-pytools) +(require 'ein-testing-kernel) + + +(ert-deftest ein:pytools-finish-tooltip () + (ein:testing-kernel-construct-help-string-loop + (lambda (content result) + (if result + (mocker-let + ((featurep + (feature) + ((:input '(pos-tip) :output t))) + (pos-tip-show + (string &optional tip-color pos window timeout) + ((:input (list result 'ein:pos-tip-face nil nil 0))))) + (let ((window-system t)) + (ein:pytools-finish-tooltip '-not-used- content '-not-used-))) + (mocker-let + ((featurep (feature) ())) + (ein:pytools-finish-tooltip '-not-used- content '-not-used-)))))) diff --git a/test/test-ein-shared-output.el b/test/test-ein-shared-output.el new file mode 100644 index 0000000..9a5d791 --- /dev/null +++ b/test/test-ein-shared-output.el @@ -0,0 +1,28 @@ +(require 'ein-shared-output) + +(defmacro eintest:shared-output-with-buffer (&rest body) + (declare (indent 0)) + `(with-current-buffer (ein:shared-output-create-buffer) + (ein:shared-output-get-or-create) + ,@body)) + +(defmacro eintest:shared-output-is-empty-context-of (func) + `(ert-deftest ,(intern (format "%s--shared-output" func)) () + (eintest:shared-output-with-buffer + (should-not (,func))))) + + +;; Generic getter + +(ert-deftest ein:get-cell-at-point--shared-output () + (eintest:shared-output-with-buffer + (should (eq (ein:get-cell-at-point) + (ein:shared-output-get-cell)))) + (with-temp-buffer + (should-not (ein:get-cell-at-point--shared-output)))) + +;; FIXME: Add tests with non-empty shared output buffer. +(eintest:shared-output-is-empty-context-of ein:get-url-or-port) +(eintest:shared-output-is-empty-context-of ein:get-notebook) +(eintest:shared-output-is-empty-context-of ein:get-kernel) +(eintest:shared-output-is-empty-context-of ein:get-traceback-data) diff --git a/test/test-ein-smartrep.el b/test/test-ein-smartrep.el new file mode 100644 index 0000000..2c1b2e7 --- /dev/null +++ b/test/test-ein-smartrep.el @@ -0,0 +1,8 @@ +(eval-when-compile (require 'cl)) +(require 'ert) + +(require 'ein-smartrep) + +(ert-deftest ein:smartrep-notebook-mode-alist-fboundp () + (loop for (k . f) in ein:smartrep-notebook-mode-alist + do (should (fboundp f)))) diff --git a/test/test-ein-utils.el b/test/test-ein-utils.el new file mode 100644 index 0000000..c57357c --- /dev/null +++ b/test/test-ein-utils.el @@ -0,0 +1,182 @@ +(eval-when-compile (require 'cl)) +(require 'ert) + +(require 'ein) ; for `ein:version' +(require 'ein-utils) + +(ert-deftest ein-url-simple () + (should (equal (ein:url 8888) "http://127.0.0.1:8888")) + (should (equal (ein:url "http://localhost") "http://localhost"))) + +(ert-deftest ein-url-slashes () + (loop for a in '("a" "a/" "/a") + do (loop for b in '("b" "/b") + do (should (equal (ein:url 8888 a b) + "http://127.0.0.1:8888/a/b"))) + do (should (equal (ein:url 8888 a "b/") + "http://127.0.0.1:8888/a/b/")))) + +(ert-deftest ein-trim-simple () + (should (equal (ein:trim "a") "a")) + (should (equal (ein:trim " a ") "a")) + (should (equal (ein:trim "\na\n") "a"))) + +(ert-deftest ein-trim-middle-spaces () + (should (equal (ein:trim "a b") "a b")) + (should (equal (ein:trim " a b ") "a b")) + (should (equal (ein:trim "\na b\n") "a b"))) + +(ert-deftest ein-trim-left-simple () + (should (equal (ein:trim-left "a") "a")) + (should (equal (ein:trim-left " a ") "a ")) + (should (equal (ein:trim-left "\na\n") "a\n"))) + +(ert-deftest ein-trim-right-simple () + (should (equal (ein:trim-right "a") "a")) + (should (equal (ein:trim-right " a ") " a")) + (should (equal (ein:trim-right "\na\n") "\na"))) + +(ert-deftest ein:trim-indent-empty () + (should (equal (ein:trim-indent "") ""))) + +(ert-deftest ein:trim-indent-one-line () + (should (equal (ein:trim-indent "one line") "one line"))) + +(ert-deftest ein:trim-indent-one-newline () + (should (equal (ein:trim-indent "one line\n") "one line\n"))) + +(ert-deftest ein:trim-indent-multi-lines-no-trim () + (let ((original "\ +def func(): + pass +") + (trimmed "\ +def func(): + pass +")) + (should (equal (ein:trim-indent original) trimmed)))) + +(ert-deftest ein:trim-indent-multi-lines-one-trim () + (let ((original "\ + def func(): + pass +") + (trimmed "\ +def func(): + pass +")) + (should (equal (ein:trim-indent original) trimmed)))) + +(ert-deftest ein:trim-indent-multi-lines-with-empty-lines () + (let ((original "\ + + def func(): + pass + +") + (trimmed "\ + +def func(): + pass + +")) + (should (equal (ein:trim-indent original) trimmed)))) + + +;;; Text manipulation on buffer + +(ert-deftest ein:find-leftmot-column-simple-cases () + (loop for (indent text) in + '(;; No indent + (0 "\ +def f(): + pass") + ;; Indented python code + (4 "\ + def f(): + pass") + ;; Deeper indent can come first + (4 "\ + # indent = 8 + # indent 4") + ;; With empty lines + (4 "\ + + # indent = 8 + + # indent 4 + +") + ) + do (with-temp-buffer + (insert text) + (should (= (ein:find-leftmot-column (point-min) (point-max)) + indent))))) + + +;;; Misc + +(ert-deftest ein:list-insert-after () + (should (equal (ein:list-insert-after '(a) 'a 'X) '(a X))) + (should (equal (ein:list-insert-after '(a b c) 'a 'X) '(a X b c))) + (should (equal (ein:list-insert-after '(a b c) 'b 'X) '(a b X c))) + (should (equal (ein:list-insert-after '(a b c) 'c 'X) '(a b c X))) + (should-error (ein:list-insert-after '(a b c) 'd 'X))) + +(ert-deftest ein:list-insert-before () + (should (equal (ein:list-insert-before '(a) 'a 'X) '(X a))) + (should (equal (ein:list-insert-before '(a b c) 'a 'X) '(X a b c))) + (should (equal (ein:list-insert-before '(a b c) 'b 'X) '(a X b c))) + (should (equal (ein:list-insert-before '(a b c) 'c 'X) '(a b X c))) + (should-error (ein:list-insert-before '(a b c) 'd 'X))) + +(ert-deftest ein:list-move-left () + (should (equal (ein:list-move-left '(a) 'a) '(a))) + (should (equal (ein:list-move-left '(a b) 'a) '(b a))) + (should (equal (ein:list-move-left '(a b) 'b) '(b a))) + (should (equal (ein:list-move-left '(a b c d) 'a) '(b c d a))) + (should (equal (ein:list-move-left '(a b c d) 'b) '(b a c d))) + (should (equal (ein:list-move-left '(a b c d) 'c) '(a c b d))) + (should (equal (ein:list-move-left '(a b c d) 'd) '(a b d c))) + (should-error (ein:list-move-left '(a b c d) 'X))) + +(ert-deftest ein:list-move-right () + (should (equal (ein:list-move-right '(a) 'a) '(a))) + (should (equal (ein:list-move-right '(a b) 'a) '(b a))) + (should (equal (ein:list-move-right '(a b) 'b) '(b a))) + (should (equal (ein:list-move-right '(a b c d) 'a) '(b a c d))) + (should (equal (ein:list-move-right '(a b c d) 'b) '(a c b d))) + (should (equal (ein:list-move-right '(a b c d) 'c) '(a b d c))) + (should (equal (ein:list-move-right '(a b c d) 'd) '(d a b c))) + (should-error (ein:list-move-right '(a b c d) 'X))) + +(defun ein:testing-choose-setting-should-equal + (setting value desired &optional single-p) + (let ((setting setting)) + (should (equal (ein:choose-setting 'setting value single-p) desired)))) + +(ert-deftest ein:choose-setting-single-string () + (let ((test 'ein:testing-choose-setting-should-equal)) + (funcall test "a" nil "a") + (funcall test "a" 'whatever "a"))) + +(ert-deftest ein:choose-setting-single-int () + (let ((test #'ein:testing-choose-setting-should-equal)) + (funcall test 1 nil 1 #'integerp) + (funcall test 1 'whatever 1 #'integerp))) + +(ert-deftest ein:choose-setting-alist () + (let* ((test (lambda (&rest args) + (apply #'ein:testing-choose-setting-should-equal + '(("a" . 1) ("b" . 2) ("c" . 3)) + args)))) + (funcall test "a" 1) + (funcall test "b" 2))) + +(ert-deftest ein:choose-setting-func () + (let* ((test (lambda (&rest args) + (apply #'ein:testing-choose-setting-should-equal + (lambda (x) 1) + args)))) + (funcall test nil 1) + (funcall test 'whatever 1))) diff --git a/test/test-ein-worksheet-notebook.el b/test/test-ein-worksheet-notebook.el new file mode 100644 index 0000000..06cd3af --- /dev/null +++ b/test/test-ein-worksheet-notebook.el @@ -0,0 +1,47 @@ +(eval-when-compile (require 'cl)) +(require 'ert) + +(require 'ein-notebook) +(require 'ein-testing-notebook) + + +;;; Event handler + +(defun ein:testing-worksheet-set-dirty + (pre-dirty value post-dirty fired-in) + (with-current-buffer (ein:testing-make-notebook-with-outputs '(nil)) + (when pre-dirty + (ein:cell-goto (ein:worksheet-get-current-cell)) + (insert "something")) + (should (equal (ein:worksheet-modified-p ein:%worksheet%) pre-dirty)) + (with-current-buffer (funcall fired-in) + (let ((events (oref ein:%worksheet% :events)) + (cell (ein:worksheet-get-current-cell))) + (ein:events-trigger events 'set_dirty.Worksheet + (list :cell cell :value value)))) + (should (equal (ein:worksheet-modified-p ein:%worksheet%) post-dirty)))) + +(defun ein:testing-scratchsheet-buffer () + (ein:worksheet-buffer (ein:notebook-scratchsheet-open ein:%notebook%))) + +(defmacro ein:testing-worksheet-set-dirty-deftest + (pre-dirty value post-dirty &optional fired-in) + (let ((name (intern (format "ein:worksheet-set-dirty/%s-to-%s-fired-in-%s" + pre-dirty value + (or fired-in "current-buffer")))) + (fired-in-defun + (case fired-in + (scratchsheet 'ein:testing-scratchsheet-buffer) + (t 'current-buffer)))) + `(ert-deftest ,name () + (ein:testing-worksheet-set-dirty ,pre-dirty ,value ,post-dirty + #',fired-in-defun)))) + +(ein:testing-worksheet-set-dirty-deftest t nil nil) +(ein:testing-worksheet-set-dirty-deftest t t t ) +(ein:testing-worksheet-set-dirty-deftest nil nil nil) +(ein:testing-worksheet-set-dirty-deftest nil t t ) +(ein:testing-worksheet-set-dirty-deftest t nil t scratchsheet) +(ein:testing-worksheet-set-dirty-deftest t t t scratchsheet) +(ein:testing-worksheet-set-dirty-deftest nil nil nil scratchsheet) +(ein:testing-worksheet-set-dirty-deftest nil t nil scratchsheet) diff --git a/test/test-ein-worksheet.el b/test/test-ein-worksheet.el new file mode 100644 index 0000000..5065cbe --- /dev/null +++ b/test/test-ein-worksheet.el @@ -0,0 +1,40 @@ +(require 'ert) + +(require 'ein-worksheet) +(require 'ein-testing-cell) + +(defvar ein:testing-worksheet-example-data + (list (ein:testing-codecell-data "code example input") + (ein:testing-markdowncell-data "markdown example input") + (ein:testing-rawcell-data "raw example input") + (ein:testing-htmlcell-data "html example input") + (ein:testing-headingcell-data "heading example input"))) + +(defun ein:testing-worksheet-new () + (make-instance 'ein:worksheet + :discard-output-p (cons #'ignore nil))) + +(defun ein:testing-worksheet-to-json (cells &optional metadata) + (let* ((ws-0 (ein:worksheet-from-json (ein:testing-worksheet-new) + (list :cells cells + :metadata metadata))) + (ws-1 (ein:testing-worksheet-new)) + (json-0 (ein:worksheet-to-json ws-0)) + (json-1 (ein:worksheet-to-json + (ein:worksheet-from-json ws-1 + (ein:json-read-from-string + (json-encode json-0)))))) + (let* ((found (assoc 'metadata json-0))) + (when found + (should (cdr found)))) + (should (equal json-0 json-1)))) + +(ert-deftest ein:worksheet-to-json/empty () + (ein:testing-worksheet-to-json nil)) + +(ert-deftest ein:worksheet-to-json/example-data () + (ein:testing-worksheet-to-json ein:testing-worksheet-example-data)) + +(ert-deftest ein:worksheet-to-json/example-data-with-metadata () + (ein:testing-worksheet-to-json ein:testing-worksheet-example-data + '(:name "Worksheet name"))) diff --git a/test/test-load.el b/test/test-load.el new file mode 100644 index 0000000..1cf27c4 --- /dev/null +++ b/test/test-load.el @@ -0,0 +1,44 @@ +;; Load all test-ein-*.el files for interactive/batch use. + +;; Usage: +;; emacs -Q -batch -L ... -l tests/test-load.el -f ert-run-tests-batch +;; You will need to set load paths using `-L' switch. + +(prefer-coding-system 'utf-8) + +(require 'ein-dev) +(require 'ein-testing) +(require 'ein-jupyter) +(require 'deferred) + +(ein:log 'info "Starting jupyter notebook server.") + +(defvar *ein:testing-jupyter-server-command* (or (getenv "JUPYTER_TESTING_COMMAND") + (executable-find "jupyter")) + "Path to command that starts the jupyter notebook server.") + +(defvar *ein:testing-jupyter-server-directory* (or (getenv "JUPYTER_TESTING_DIR") (concat default-directory "test")) + "Location where to start the jupyter notebook server.") + +(defvar *ein:testing-port* nil) +(defvar *ein:testing-token* nil) + +(ein:setq-if-not ein:testing-dump-file-log "test-batch-log.log") +(ein:setq-if-not ein:testing-dump-file-messages "test-batch-messages.log") +(setq message-log-max t) +(setq ein:force-sync t) + +(ein:log 'info "Staring local jupyter notebook server.") + +(setq ein:jupyter-server-args '("--no-browser" "--debug")) + +(deferred:sync! (ein:jupyter-server-start *ein:testing-jupyter-server-command* *ein:testing-jupyter-server-directory*)) +(multiple-value-bind (url token) (ein:jupyter-server-conn-info) + (ein:log 'info (format "testing-start-server url: %s, token: %s" url token)) + (setq *ein:testing-port* url) + (setq *ein:testing-token* token) + (ein:log 'info "testing-start-server succesfully logged in.")) + +(ein:load-files "^test-ein-.*\\.el$" + "./" ;(file-name-directory load-file-name) + t) ; ignore-compiled