;;; test-flan-dev.el --- Drive the client against a running program -*- lexical-binding: t; -*- ;; Run as: emacs -Q --batch -L emacs -l emacs/test-flan-dev.el -- ;; ;; This is the client half of test_dev.ml. The OCaml test proves the daemon ;; answers correctly; this proves the elisp actually talks to it — the framing, ;; the reply reader, and C-c C-c picking the right form out of a buffer. A ;; protocol bug that only shows up under Emacs' coding systems would pass the ;; OCaml test and fail here, which is the whole reason it exists. ;;; Code: (require 'flan-mode) (require 'flan-dev) (require 'flan-repl) (defvar test-flan--failures 0) (defun test-flan--check (name ok) (if ok (message " ok %s" name) (setq test-flan--failures (1+ test-flan--failures)) (message " FAIL %s" name))) (let* ((args (cdr (member "--" command-line-args))) (socket (nth 0 args)) (file (nth 1 args))) (find-file file) ;; The copy comes out of a build directory, so it may arrive read-only. ;; Set the flag directly: `read-only-mode' asks about the file on disk, and ;; a question in a batch run is a hang waiting to happen. (setq buffer-read-only nil) (test-flan--check "flan-mode is on for a .flan file" (eq major-mode 'flan-mode)) (flan-connect socket) (test-flan--check "connected" (process-live-p flan-dev--connection)) (let ((r (flan-dev--request '(:op "describe")))) (test-flan--check "describe lists the program's functions" (member "step" (plist-get r :fns))) (test-flan--check "describe lists the program's globals" (member "ticks" (plist-get r :globals)))) ;; C-c C-c on the form at point: put point inside `step' and send it. The ;; text comes from the buffer, so this exercises `beginning-of-defun' against ;; Flan's own syntax table as much as it does the wire. (goto-char (point-min)) (search-forward "(defn step") (goto-char (match-beginning 0)) (save-excursion (search-forward "(+ ticks 1)") (replace-match "(+ ticks 41)")) (let ((form (flan-dev--defun-at-point))) (test-flan--check "the form at point is the defn" (and (string-prefix-p "(defn step" (string-trim form)) (string-match-p "41" form)))) (flan-eval-defun) ;; And an error: the daemon answers with a location, the client raises. (let ((raised nil)) (condition-case err (flan-dev--eval "(defn step [] i64 nonsense)" "form") (user-error (setq raised (error-message-string err)))) (test-flan--check "a form that does not check is reported" (and raised (string-match-p "unknown name" raised)))) ;; The session is not poisoned by that: a good form still lands. (flan-dev--eval "(defn step [] i64 (set ticks (+ ticks 100)) ticks)" "form") ;; The program's own output arrives on replies and lands in its buffer, so ;; a long-running program is not writing into a terminal nobody is watching. (flan-dev--eval "(defn step [] i64 (do (print-line \"HELLO\") ticks))" "form") (let ((seen nil) (deadline (+ (float-time) 10))) (while (and (not seen) (< (float-time) deadline)) (ignore-errors (flan-dev--request '(:op "describe"))) (setq seen (with-current-buffer (get-buffer-create flan-dev-output-buffer) (string-match-p "HELLO" (buffer-string))))) (test-flan--check "the program's output reaches its buffer" seen)) ;; The REPL buffer: typed input goes through the same eval-expr request, and ;; the value lands at the prompt while the program's own output goes to ;; *flan-output*. Conflating those two is the bug worth testing for. (test-flan--check "an incomplete form is not sent" (not (flan-repl--complete-p "(+ 1"))) (test-flan--check "a whole form is sent" (flan-repl--complete-p "(+ 1 2)")) (test-flan--check "a paren in a string does not count" (not (flan-repl--complete-p "(f \"(\""))) (flan-repl) (with-current-buffer flan-repl-buffer (goto-char (point-max)) (insert "(+ 20 3)") (flan-repl-return) (let ((deadline (+ (float-time) 15))) (while (and (not (string-match-p "23" (buffer-string))) (< (float-time) deadline)) (accept-process-output nil 0.05))) (test-flan--check "the REPL shows a value" (string-match-p "23" (buffer-string))) (goto-char (point-max)) (insert "no-such-thing") (flan-repl-return) (let ((deadline (+ (float-time) 15))) (while (and (not (string-match-p "unknown name" (buffer-string))) (< (float-time) deadline)) (accept-process-output nil 0.05))) (test-flan--check "the REPL shows an error" (string-match-p "unknown name" (buffer-string))) ;; A value and the program's output travel by different routes: the value ;; is the result of the request, the output rides along with the reply. ;; Showing them in one place would be convenient and wrong. (goto-char (point-max)) (insert "(print-line \"PRINTED\")") (flan-repl-return) (let ((deadline (+ (float-time) 15))) (while (and (not (with-current-buffer flan-dev-output-buffer (string-match-p "PRINTED" (buffer-string)))) (< (float-time) deadline)) (ignore-errors (flan-dev--request '(:op "describe"))) (accept-process-output nil 0.05))) (test-flan--check "printed text goes to the output buffer" (with-current-buffer flan-dev-output-buffer (string-match-p "PRINTED" (buffer-string)))) ;; ...and the prompt got the *value*, which for a call made for its effect ;; is Unit. The text it printed is not the value and does not belong here. (test-flan--check "and the prompt got the value, not the text" (string-match-p "()" (buffer-string)))) (flan-disconnect) (test-flan--check "disconnected" (not (process-live-p flan-dev--connection))) (if (zerop test-flan--failures) (message "flan-dev.el: all tests passed") (message "\n%d failure(s)" test-flan--failures) (kill-emacs 1))) ;;; test-flan-dev.el ends here