(require 'slime)
(require 'ert nil t)
(require 'ert "lib/ert" t) (require 'cl-lib)
(require 'bytecomp)
(defun slime-shuffle-list (list)
(let* ((len (length list))
(taken (make-vector len nil))
(result (make-vector len nil)))
(dolist (e list)
(while (let ((i (random len)))
(cond ((aref taken i))
(t (aset taken i t)
(aset result i e)
nil)))))
(append result '())))
(defun slime-batch-test (&optional test-name randomize)
"Run the test suite in batch-mode.
Exits Emacs when finished. The exit code is the number of failed tests."
(interactive)
(let ((ert-debug-on-error nil)
(timeout 60)
(slime-background-message-function #'ignore))
(slime)
(let (timed-out)
(run-with-timer timeout nil
(lambda () (setq timed-out t)))
(while (not (slime-connected-p))
(sit-for 1)
(when timed-out
(when noninteractive
(kill-emacs 252)))))
(slime-sync-to-top-level 5)
(let* ((selector (if randomize
`(member ,@(slime-shuffle-list
(ert-select-tests (or test-name t) t)))
(or test-name t)))
(ert-fun (if noninteractive
'ert-run-tests-batch
'ert)))
(let ((stats (funcall ert-fun selector)))
(if noninteractive
(kill-emacs (ert-stats-completed-unexpected stats)))))))
(defun slime-skip-test (message)
(if (fboundp 'ert-skip)
(ert-skip message)
(message (concat "SKIPPING: " message))
(ert-pass)))
(defun slime-tests--undefine-all ()
(dolist (test (ert-select-tests t t))
(let* ((sym (ert-test-name test)))
(cl-assert (eq (get sym 'ert--test) test))
(cl-remprop sym 'ert--test))))
(slime-tests--undefine-all)
(eval-and-compile
(defun slime-tests-auto-tags ()
(append '(slime)
(let ((file-name (or load-file-name
byte-compile-current-file)))
(if (and file-name
(string-match "contrib/test/slime-\\(.*\\)\.elc?$"
file-name))
(list 'contrib (intern (match-string 1 file-name)))
'(core)))))
(defmacro define-slime-ert-test (name &rest args)
"Like `ert-deftest', but set tags automatically.
Also don't error if `ert.el' is missing."
(if (not (featurep 'ert))
(warn "No ert.el found: not defining test %s"
name)
(let* ((docstring (and (stringp (cl-second args))
(cl-second args)))
(args (if docstring
(cddr args)
(cdr args)))
(tags (slime-tests-auto-tags)))
`(ert-deftest ,name () ,(or docstring "No docstring for this test.")
:tags ',tags
,@args))))
(defun slime-test-ert-test-for (name input i doc _body fails-for style fname)
`(define-slime-ert-test
,(intern (format "%s-%d" name i)) ()
,(format "For input %s, %s" (truncate-string-to-width
(format "%s" input)
15 nil nil 'ellipsis)
(replace-regexp-in-string "^.??\\(\\w+\\)"
(lambda (s) (downcase s))
doc
t))
,@(if fails-for
`(:expected-result '(satisfies
(lambda (result)
(ert-test-result-type-p
result
(if (member
(slime-lisp-implementation-name)
',fails-for)
:failed
:passed))))))
,@(when style
`((let ((style (slime-communication-style)))
(when (not (member style ',style))
(slime-skip-test (format "test not applicable for style %s"
style))))))
(apply #',fname ',input))))
(defmacro def-slime-test (name args doc inputs &rest body)
"Define a test case.
NAME ::= SYMBOL | (SYMBOL OPTION*) is a symbol naming the test.
OPTION ::= (:fails-for IMPLEMENTATION*) | (:style COMMUNICATION-STYLE*)
ARGS is a lambda-list.
DOC is a docstring.
INPUTS is a list of argument lists, each tested separately.
BODY is the test case. The body can use `slime-check' to test
conditions (assertions)."
(declare (debug (&define name sexp sexp sexp &rest def-form)))
(if (not (featurep 'ert))
(warn "No ert.el found: not defining test %s"
name)
`(progn
,@(cl-destructuring-bind (name &rest options)
(if (listp name) name (list name))
(let ((fname (intern (format "slime-test-%s" name))))
(cons `(defun ,fname ,args
(slime-sync-to-top-level 0.3)
,@body
(slime-sync-to-top-level 0.3))
(cl-loop for input in (eval inputs)
for i from 1
with fails-for = (cdr (assoc :fails-for options))
with style = (cdr (assoc :style options))
collect (slime-test-ert-test-for name
input
i
doc
body
fails-for
style
fname))))))))
(put 'def-slime-test 'lisp-indent-function 4)
(defmacro slime-check (check &rest body)
(declare (indent defun))
`(unless (progn ,@body)
(ert-fail ,(cl-etypecase check
(cons `(concat "Ooops, " ,(cons 'format check)))
(string `(concat "Check failed: " ,check))
(symbol `(concat "Check failed: " ,(symbol-name check)))))))
(defun slime-check-top-level () (accept-process-output nil 0.001)
(slime-check "At the top level (no debugging or pending RPCs)"
(slime-at-top-level-p)))
(defun slime-at-top-level-p ()
(and (not (sldb-get-default-buffer))
(null (slime-rex-continuations))))
(defun slime-wait-condition (name predicate timeout)
(let ((end (time-add (current-time) (seconds-to-time timeout))))
(while (not (funcall predicate))
(message "waiting for condition: %s [%s]" name
(format-time-string "%H:%M:%S.%6N"))
(cond ((time-less-p end (current-time))
(error "Timeout waiting for condition: %S" name))
(t
(accept-process-output nil 0.1))))))
(defun slime-sync-to-top-level (timeout)
(slime-wait-condition "top-level" #'slime-at-top-level-p timeout))
(defun slime-check-sldb-level (expected)
(let ((sldb-level (let ((sldb (sldb-get-default-buffer)))
(if sldb
(with-current-buffer sldb
sldb-level)))))
(slime-check ("SLDB level (%S) is %S" expected sldb-level)
(equal expected sldb-level))))
(defun slime-test-expect (_name expected actual &optional test)
(when (stringp expected) (setq expected (substring-no-properties expected)))
(when (stringp actual) (setq actual (substring-no-properties actual)))
(if test
(should (funcall test expected actual))
(should (equal expected actual))))
(defun sldb-level ()
(let ((sldb (sldb-get-default-buffer)))
(if sldb
(with-current-buffer sldb
sldb-level))))
(defun slime-sldb-level= (level)
(equal level (sldb-level)))
(eval-when-compile
(defvar slime-test-symbols
'(("foobar") ("foo@bar") ("@foobar") ("foobar@") ("\\@foobar")
("|asdf||foo||bar|")
("\\#<Foo@Bar>")
("\\(setf\\ car\\)"))))
(defun slime-check-symbol-at-point (prefix symbol suffix)
(with-temp-buffer
(lisp-mode)
(insert prefix)
(let ((start (point)))
(insert symbol suffix)
(dotimes (i (length symbol))
(goto-char (+ start i))
(slime-test-expect (format "Check `%s' (at %d)..."
(buffer-string) (point))
symbol
(slime-symbol-at-point)
#'equal)))))
(def-slime-test symbol-at-point.2 (sym)
"fancy symbol-name _not_ at BOB/EOB"
slime-test-symbols
(slime-check-symbol-at-point "(foo " sym " bar)"))
(def-slime-test symbol-at-point.3 (sym)
"fancy symbol-name with leading ,"
(cl-remove-if (lambda (s) (eq (aref (car s) 0) ?@)) slime-test-symbols)
(slime-check-symbol-at-point "," sym ""))
(def-slime-test symbol-at-point.4 (sym)
"fancy symbol-name with leading ,@"
slime-test-symbols
(slime-check-symbol-at-point ",@" sym ""))
(def-slime-test symbol-at-point.5 (sym)
"fancy symbol-name with leading `"
slime-test-symbols
(slime-check-symbol-at-point "`" sym ""))
(def-slime-test symbol-at-point.6 (sym)
"fancy symbol-name wrapped in ()"
slime-test-symbols
(slime-check-symbol-at-point "(" sym ")"))
(def-slime-test symbol-at-point.7 (sym)
"fancy symbol-name wrapped in #< {DEADBEEF}>"
slime-test-symbols
(slime-check-symbol-at-point "#<" sym " {DEADBEEF}>"))
(def-slime-test symbol-at-point.9 (sym)
"fancy symbol-name wrapped in #| ... |#"
slime-test-symbols
(slime-check-symbol-at-point "#|\n" sym "\n|#"))
(def-slime-test symbol-at-point.10 (sym)
"fancy symbol-name after #| )))(( |# (1)"
slime-test-symbols
(slime-check-symbol-at-point "#| )))(( #|\n" sym ""))
(def-slime-test symbol-at-point.11 (sym)
"fancy symbol-name after #| )))(( |# (2)"
slime-test-symbols
(slime-check-symbol-at-point "#| )))(( #|" sym ""))
(def-slime-test symbol-at-point.12 (sym)
"fancy symbol-name wrapped in \"...\""
slime-test-symbols
(slime-check-symbol-at-point "\"\n" sym "\"\n"))
(def-slime-test symbol-at-point.13 (sym)
"fancy symbol-name wrapped in \" )))(( \" (1)"
slime-test-symbols
(slime-check-symbol-at-point "\" )))(( \"\n" sym ""))
(def-slime-test symbol-at-point.14 (sym)
"fancy symbol-name wrapped in \" )))(( \" (1)"
slime-test-symbols
(slime-check-symbol-at-point "\" )))(( \"" sym ""))
(def-slime-test symbol-at-point.15 (sym)
"symbol-at-point after #."
slime-test-symbols
(slime-check-symbol-at-point "#." sym ""))
(def-slime-test symbol-at-point.16 (sym)
"symbol-at-point after #+"
slime-test-symbols
(slime-check-symbol-at-point "#+" sym ""))
(def-slime-test sexp-at-point.1 (string)
"symbol-at-point after #'"
'(("foo")
("#:foo")
("#'foo")
("#'(lambda (x) x)")
("()"))
(with-temp-buffer
(lisp-mode)
(insert string)
(goto-char (point-min))
(slime-test-expect (format "Check sexp `%s' (at %d)..."
(buffer-string) (point))
string
(slime-sexp-at-point)
#'equal)))
(def-slime-test narrowing ()
"Check that narrowing is properly sustained."
'()
(slime-check-top-level)
(let ((random-buffer-name (symbol-name (cl-gensym)))
(defun-pos) (tmpbuffer))
(with-temp-buffer
(dotimes (i 100) (insert (format ";;; %d. line\n" i)))
(setq tmpbuffer (current-buffer))
(setq defun-pos (point))
(insert (concat "(defun __foo__ (x y)" "\n"
" 'nothing)" "\n"))
(dotimes (i 100) (insert (format ";;; %d. line\n" (+ 100 i))))
(slime-check "Checking that newly created buffer is not narrowed."
(not (slime-buffer-narrowed-p)))
(goto-char defun-pos)
(narrow-to-defun)
(slime-check "Checking that narrowing succeeded."
(slime-buffer-narrowed-p))
(slime-with-popup-buffer (random-buffer-name)
(slime-check ("Checking that we're in Slime's temp buffer `%s'"
random-buffer-name)
(equal (buffer-name (current-buffer)) random-buffer-name)))
(with-current-buffer random-buffer-name
(quit-window t))
(slime-check ("Checking that we've got back from `%s'"
random-buffer-name)
(and (eq (current-buffer) tmpbuffer)
(= (point) defun-pos)))
(slime-check "Checking that narrowing sustained \
after quitting Slime's temp buffer."
(slime-buffer-narrowed-p))
(let ((slime-buffer-package "SWANK")
(symbol '*buffer-package*))
(slime-edit-definition (symbol-name symbol))
(slime-check ("Checking that we've got M-. into swank.lisp. %S" symbol)
(string= (file-name-nondirectory (buffer-file-name))
"swank.lisp"))
(slime-pop-find-definition-stack)
(slime-check ("Checking that we've got back.")
(and (eq (current-buffer) tmpbuffer)
(= (point) defun-pos)))
(slime-check "Checking that narrowing sustained after M-,"
(slime-buffer-narrowed-p)))
))
(slime-check-top-level))
(defun slime-test--display-region-eval-arg (line window-height)
(cl-etypecase line
(number line)
(cons (slime-dcase line
((+h line)
(+ (slime-test--display-region-eval-arg line window-height)
window-height))
((-h line)
(- (slime-test--display-region-eval-arg line window-height)
window-height))))))
(defun slime-test--display-region-line-to-position (line window-height)
(let ((line (slime-test--display-region-eval-arg line window-height)))
(save-excursion
(goto-char (point-min))
(forward-line (1- line))
(line-beginning-position))))
(def-slime-test display-region
(start end pos window-start expected-window-start expected-point)
"Test `slime-display-region'."
'( (2 4 3 1 1 3)
(2 4 5 1 1 5)
(2 (+h 2) 2 1 2 2)
((+h 2) (+h 500) (+h 2) 1 (+h 2) (+h 2))
((+h 2) (+h 500) (+h 6) 1 (+h 2) (+h 6))
((+h 2) (+h 7) (+h 10) 1 (-h (+h 7)) (+h 6))
((+h -2) (+h (+h -3)) (+h -2) 1 (+h -3) (+h -2))
(2 (+h 1) 3 1 1 3)
(2 (+h 0) 3 1 1 3)
(2 (+h -1) 3 1 1 3)
(1 1 1 1 1 1)
(1 (+h 1) (+h 22) (+h 20) 1 (+h 0))
)
(when noninteractive
(slime-skip-test "Can't test slime-display-region in batch mode"))
(with-temp-buffer
(dotimes (i 1000)
(insert (format "%09d\n" i)))
(let* ((win (display-buffer (current-buffer) t))
(wh (window-text-height win)))
(cl-macrolet ((l2p (l)
`(slime-test--display-region-line-to-position ,l wh)))
(select-window win)
(set-window-start win (l2p window-start))
(redisplay)
(goto-char (l2p pos))
(cl-assert (= (l2p window-start) (window-start win)))
(cl-assert (= (point) (l2p pos)))
(slime--display-region (l2p start) (l2p end))
(redisplay)
(cl-assert (= (l2p expected-window-start) (window-start)))
(cl-assert (= (l2p expected-point) (point)))
))))
(def-slime-test find-definition
(name buffer-package snippet)
"Find the definition of a function or macro in swank.lisp."
'(("start-server" "SWANK" "(defun start-server ")
("swank::start-server" "CL-USER" "(defun start-server ")
("swank:start-server" "CL-USER" "(defun start-server ")
("swank::connection" "CL-USER" "(defstruct (connection")
("swank::*emacs-connection*" "CL-USER" "(defvar \\*emacs-connection\\*")
)
(switch-to-buffer "*scratch*") (slime-check-top-level)
(let ((orig-buffer (current-buffer))
(orig-pos (point))
(enable-local-variables nil) (slime-buffer-package buffer-package))
(slime-edit-definition name)
(slime-check ("Definition of `%S' is in swank.lisp." name)
(string= (file-name-nondirectory (buffer-file-name)) "swank.lisp"))
(slime-check ("Looking at '%s'." snippet) (looking-at snippet))
(slime-pop-find-definition-stack)
(slime-check "Returning from definition restores original buffer/position."
(and (eq orig-buffer (current-buffer))
(= orig-pos (point)))))
(slime-check-top-level))
(def-slime-test (find-definition.2 (:fails-for "allegro" "lispworks"))
(buffer-content buffer-package snippet)
"Check that we're able to find definitions even when
confronted with nasty #.-fu."
'(("#.(prog1 nil (defvar *foobar* 42))
(defun .foo. (x)
(+ x #.*foobar*))
#.(prog1 nil (makunbound '*foobar*))
"
"SWANK"
"[ \t]*(defun .foo. "
)
("#.(prog1 nil (defvar *foobar* 42))
;; some comment
(defun .foo. (x)
(+ x #.*foobar*))
#.(prog1 nil (makunbound '*foobar*))
"
"SWANK"
"[ \t]*(defun .foo. "
)
("(in-package swank)
(eval-when (:compile-toplevel) (defparameter *bar* 456))
(eval-when (:load-toplevel :execute) (makunbound '*bar*))
(defun bar () #.*bar*)
(defun .foo. () 123)"
"SWANK"
"[ \t]*(defun .foo. () 123)"))
(let ((slime-buffer-package buffer-package))
(with-temp-buffer
(insert buffer-content)
(slime-check-top-level)
(slime-eval
`(swank:compile-string-for-emacs
,buffer-content
,(buffer-name)
'((:position 0) (:line 1 1))
,nil
,nil))
(let ((bufname (buffer-name)))
(slime-edit-definition ".foo.")
(slime-check ("Definition of `.foo.' is in buffer `%s'." bufname)
(string= (buffer-name) bufname))
(slime-check "Definition now at point." (looking-at snippet))))))
(def-slime-test (find-definition.3
(:fails-for "abcl" "allegro" "clisp" "lispworks" "ecl"))
(name source regexp)
"Extra tests for defstruct."
'(("swank::foo-struct"
"(progn
(defun foo-fun ())
(defstruct (foo-struct (:constructor nil) (:predicate nil)))
)"
"(defstruct (foo-struct"))
(switch-to-buffer "*scratch*")
(with-temp-buffer
(insert source)
(let ((slime-buffer-package "SWANK"))
(slime-eval
`(swank:compile-string-for-emacs
,source
,(buffer-name)
'((:position 0) (:line 1 1))
,nil
,nil)))
(let ((temp-buffer (current-buffer)))
(with-current-buffer "*scratch*"
(slime-edit-definition name)
(slime-check ("Definition of %S is in buffer `%s'."
name temp-buffer)
(eq (current-buffer) temp-buffer))
(slime-check "Definition now at point." (looking-at regexp)))
)))
(def-slime-test complete-symbol
(prefix expected-completions)
"Find the completions of a symbol-name prefix."
'(("cl:compile" ("cl:compile" "cl:compile-file" "cl:compile-file-pathname"
"cl:compiled-function" "cl:compiled-function-p"
"cl:compiler-macro" "cl:compiler-macro-function"))
("cl:foobar" ())
("swank::compile-file" ("swank::compile-file"
"swank::compile-file-for-emacs"
"swank::compile-file-if-needed"
"swank::compile-file-output"
"swank::compile-file-pathname"))
("cl:m-v-l" ()))
(let ((completions (slime-simple-completions prefix)))
(slime-test-expect "Completion set" expected-completions completions)))
(def-slime-test read-from-minibuffer
(input-keys expected-result)
"Test `slime-read-from-minibuffer' with INPUT-KEYS as events."
'(("( r e v e TAB SPC ' ( 1 SPC 2 SPC 3 ) ) RET"
"(reverse '(1 2 3))")
("( c l : c o n TAB s t a n t l TAB SPC 4 2 ) RET"
"(cl:constantly 42)"))
(when noninteractive
(slime-skip-test "Can't use unread-command-events in batch mode"))
(let ((keys (eval `(kbd ,input-keys)))) (setq unread-command-events (listify-key-sequence keys)))
(let ((actual-result (slime-read-from-minibuffer "Test: ")))
(accept-process-output) (slime-test-expect "Completed string" expected-result actual-result)))
(def-slime-test arglist
(function-name expected-arglist)
"Lookup the argument list for FUNCTION-NAME.
Confirm that EXPECTED-ARGLIST is displayed."
'(("swank::operator-arglist" "(swank::operator-arglist name package)")
("swank::compute-backtrace" "(swank::compute-backtrace start end)")
("swank::emacs-connected" "(swank::emacs-connected)")
("swank::compile-string-for-emacs"
"(swank::compile-string-for-emacs \
string buffer position filename policy)")
("swank::connection.socket-io"
"(swank::connection.socket-io \
\\(struct\\(ure\\)?\\|object\\|instance\\|x\\|connection\\))")
("cl:lisp-implementation-type" "(cl:lisp-implementation-type)")
("cl:class-name"
"(cl:class-name \\(class\\|object\\|instance\\|structure\\))"))
(let ((arglist (slime-eval `(swank:operator-arglist ,function-name
"swank"))))
(slime-test-expect "Argument list is as expected"
expected-arglist (and arglist (downcase arglist))
(lambda (pattern arglist)
(and arglist (string-match pattern arglist))))))
(defun slime-test--compile-defun (program subform)
(slime-check-top-level)
(with-temp-buffer
(lisp-mode)
(insert program)
(let ((font-lock-verbose nil))
(setq slime-buffer-package ":swank")
(slime-compile-string (buffer-string) 1)
(setq slime-buffer-package ":cl-user")
(slime-sync-to-top-level 5)
(goto-char (point-max))
(slime-previous-note)
(slime-check error-location-correct
(equal (read (current-buffer)) subform))))
(slime-check-top-level))
(def-slime-test (compile-defun (:fails-for "allegro" "lispworks" "clisp"))
(program subform)
"Compile PROGRAM containing errors.
Confirm that SUBFORM is correctly located."
'(("(defun cl-user::foo () (cl-user::bar))" (cl-user::bar))
("(defun cl-user::foo ()
#\\space
;;Sdf
(cl-user::bar))"
(cl-user::bar))
("(defun cl-user::foo ()
#+(or)skipped
#| #||#
#||# |#
(cl-user::bar))"
(cl-user::bar))
("(defun cl-user::foo ()
\"\\\" bla bla \\\"\"
(cl-user::bar))"
(cl-user::bar))
("(defun cl-user::foo ()
#.*log-events*
(cl-user::bar))"
(cl-user::bar))
("#.'(defun x () (/ 1 0))
(defun foo ()
(cl-user::bar))
"
(cl-user::bar)))
(slime-test--compile-defun program subform))
(def-slime-test (compile-defun-with-reader-conditionals
(:fails-for "allegro" "lispworks" "clisp" "ccl"))
(program subform)
"Compile PROGRAM containing errors.
Confirm that SUBFORM is correctly located."
'(("(defun foo ()
#+#.'(:and) (/ 1 0))"
(/ 1 0)))
(slime-test--compile-defun program subform))
(def-slime-test (compile-defun-with-backquote
(:fails-for "allegro" "lispworks" "clisp" "sbcl"))
(program subform)
"Compile PROGRAM containing errors.
Confirm that SUBFORM is correctly located."
'(("(defun cl-user::foo ()
(list `(1 ,(random 10) 2 ,@(make-list (random 10)) 3
,(cl-user::bar))))"
(cl-user::bar)))
(slime-test--compile-defun program subform))
(def-slime-test (compile-file (:fails-for "allegro" "clisp"))
(string)
"Insert STRING in a file, and compile it."
`((,(pp-to-string '(defun foo () nil))))
(let ((filename "/tmp/slime-tmp-file.lisp"))
(with-temp-file filename
(insert string))
(let ((cell (cons nil nil)))
(slime-eval-async
`(swank:compile-file-for-emacs ,filename nil)
(slime-rcurry (lambda (result cell)
(setcar cell t)
(setcdr cell result))
cell))
(slime-wait-condition "Compilation finished" (lambda () (car cell))
5)
(let ((result (cdr cell)))
(slime-check "Compilation successfull"
(eq (slime-compilation-result.successp result) t))))))
(def-slime-test utf-8-source
(input output)
"Source code containing utf-8 should work"
(list (let* ((bytes "\000\343\201\212\343\201\257\343\202\210\343\201\206")
(string (decode-coding-string bytes 'utf-8-unix)))
(cl-assert (equal bytes (encode-coding-string string 'utf-8-unix)))
(list (concat "(defun cl-user::foo () \"" string "\")")
string)))
(slime-eval `(cl:eval (cl:read-from-string ,input)))
(slime-test-expect "Eval result correct"
output (slime-eval '(cl-user::foo)))
(let ((cell (cons nil nil)))
(let ((hook (slime-curry (lambda (cell &rest _) (setcar cell t)) cell)))
(add-hook 'slime-compilation-finished-hook hook)
(unwind-protect
(progn
(slime-compile-string input 0)
(slime-wait-condition "Compilation finished"
(lambda () (car cell))
5)
(slime-test-expect "Compile-string result correct"
output (slime-eval '(cl-user::foo))))
(remove-hook 'slime-compilation-finished-hook hook))
(let ((filename "/tmp/slime-tmp-file.lisp"))
(setcar cell nil)
(add-hook 'slime-compilation-finished-hook hook)
(unwind-protect
(with-temp-buffer
(when (fboundp 'set-buffer-multibyte)
(set-buffer-multibyte t))
(setq buffer-file-coding-system 'utf-8-unix)
(setq buffer-file-name filename)
(insert ";; -*- coding: utf-8-unix -*- \n")
(insert input)
(let ((coding-system-for-write 'utf-8-unix))
(write-region nil nil filename nil t))
(let ((slime-load-failed-fasl 'always))
(slime-compile-and-load-file)
(slime-wait-condition "Compilation finished"
(lambda () (car cell))
5))
(slime-test-expect "Compile-file result correct"
output (slime-eval '(cl-user::foo))))
(remove-hook 'slime-compilation-finished-hook hook)
(ignore-errors (delete-file filename)))))))
(def-slime-test async-eval-debugging (depth)
"Test recursive debugging of asynchronous evaluation requests."
'((1) (2) (3))
(let ((depth depth)
(debug-hook-max-depth 0))
(let ((debug-hook
(lambda ()
(with-current-buffer (sldb-get-default-buffer)
(when (> sldb-level debug-hook-max-depth)
(setq debug-hook-max-depth sldb-level)
(if (= sldb-level depth)
(sldb-quit)
(slime-eval-async '(error))))))))
(let ((sldb-hook (cons debug-hook sldb-hook)))
(slime-eval-async '(error))
(slime-sync-to-top-level 5)
(slime-check ("Maximum depth reached (%S) is %S."
debug-hook-max-depth depth)
(= debug-hook-max-depth depth))))))
(def-slime-test unwind-to-previous-sldb-level (level2 level1)
"Test recursive debugging and returning to lower SLDB levels."
'((2 1) (4 2))
(slime-check-top-level)
(let ((level2 level2)
(level1 level1)
(state 'enter)
(max-depth 0))
(let ((debug-hook
(lambda ()
(with-current-buffer (sldb-get-default-buffer)
(setq max-depth (max sldb-level max-depth))
(cl-ecase state
(enter
(cond ((= sldb-level level2)
(setq state 'leave)
(sldb-invoke-restart (sldb-first-abort-restart)))
(t
(slime-eval-async `(cl:aref cl:nil ,sldb-level)))))
(leave
(cond ((= sldb-level level1)
(setq state 'ok)
(sldb-quit))
(t
(sldb-invoke-restart (sldb-first-abort-restart))
))))))))
(let ((sldb-hook (cons debug-hook sldb-hook)))
(slime-eval-async `(cl:aref cl:nil 0))
(slime-sync-to-top-level 15)
(slime-check-top-level)
(slime-check ("Maximum depth reached (%S) is %S." max-depth level2)
(= max-depth level2))
(slime-check ("Final state reached.")
(eq state 'ok))))))
(defun sldb-first-abort-restart ()
(let ((case-fold-search t))
(cl-position-if (lambda (x) (string-match "abort" (car x)))
sldb-restarts)))
(def-slime-test loop-interrupt-quit
()
"Test interrupting a loop."
'(())
(slime-check-top-level)
(slime-eval-async '(cl:loop) (lambda (_) ) "CL-USER")
(accept-process-output nil 1)
(slime-check "In eval state." (slime-busy-p))
(slime-interrupt)
(slime-wait-condition "First interrupt" (lambda () (slime-sldb-level= 1)) 5)
(with-current-buffer (sldb-get-default-buffer)
(sldb-quit))
(slime-sync-to-top-level 5)
(slime-check-top-level))
(def-slime-test loop-interrupt-continue-interrupt-quit
()
"Test interrupting a previously interrupted but continued loop."
'(())
(slime-check-top-level)
(slime-eval-async '(cl:loop) (lambda (_) ) "CL-USER")
(sleep-for 1)
(slime-wait-condition "running" #'slime-busy-p 5)
(slime-interrupt)
(slime-wait-condition "First interrupt" (lambda () (slime-sldb-level= 1)) 5)
(with-current-buffer (sldb-get-default-buffer)
(sldb-continue))
(slime-wait-condition "running" (lambda ()
(and (slime-busy-p)
(not (sldb-get-default-buffer)))) 5)
(slime-interrupt)
(slime-wait-condition "Second interrupt" (lambda () (slime-sldb-level= 1)) 5)
(with-current-buffer (sldb-get-default-buffer)
(sldb-quit))
(slime-sync-to-top-level 5)
(slime-check-top-level))
(def-slime-test interactive-eval
()
"Test interactive eval and continuing from the debugger."
'(())
(slime-check-top-level)
(let ((done nil))
(let ((sldb-hook (lambda () (sldb-continue) (setq done t))))
(slime-interactive-eval
"(progn\
(cerror \"foo\" \"restart\")\
(cerror \"bar\" \"restart\")\
(+ 1 2))")
(while (not done) (accept-process-output))
(slime-sync-to-top-level 5)
(slime-check-top-level)
(unless noninteractive
(let ((message (current-message)))
(slime-check "Minibuffer contains: \"3\""
(equal "=> 3 (2 bits, #x3, #o3, #b11)" message)))))))
(def-slime-test report-condition-with-circular-list
(format-control format-argument)
"Test conditions involving circular lists."
'(("~a" "(let ((x (cons nil nil))) (setf (cdr x) x))")
("~a" "(let ((x (cons nil nil))) (setf (car x) x))")
("~a" "(let ((x (cons (make-string 100000 :initial-element #\\X) nil)))\
(setf (cdr x) x))"))
(slime-check-top-level)
(let ((done nil))
(let ((sldb-hook (lambda () (sldb-continue) (setq done t))))
(slime-interactive-eval
(format "(with-standard-io-syntax (cerror \"foo\" \"%s\" %s) (+ 1 2))"
format-control format-argument))
(while (not done) (accept-process-output))
(slime-sync-to-top-level 5)
(slime-check-top-level)
(unless noninteractive
(let ((message (current-message)))
(slime-check "Minibuffer contains: \"3\""
(equal "=> 3 (2 bits, #x3, #o3, #b11)" message)))))))
(def-slime-test interrupt-bubbling-idiot
()
"Test interrupting a loop that sends a lot of output to Emacs."
'(())
(accept-process-output nil 1)
(slime-check-top-level)
(slime-eval-async '(cl:loop :for i :from 0 :do (cl:progn (cl:print i)
(cl:finish-output)))
(lambda (_) )
"CL-USER")
(sleep-for 1)
(slime-interrupt)
(slime-wait-condition "Debugger visible"
(lambda ()
(and (slime-sldb-level= 1)
(get-buffer-window (sldb-get-default-buffer))))
30)
(with-current-buffer (sldb-get-default-buffer)
(sldb-quit))
(slime-sync-to-top-level 5))
(def-slime-test (interrupt-encode-message (:style :sigio))
()
"Test interrupt processing during swank::encode-message"
'(())
(slime-eval-async '(cl:loop :for i :from 0
:do (swank::background-message "foo ~d" i)))
(sleep-for 1)
(slime-eval-async '(cl:/ 1 0))
(slime-wait-condition "Debugger visible"
(lambda ()
(and (slime-sldb-level= 1)
(get-buffer-window (sldb-get-default-buffer))))
30)
(with-current-buffer (sldb-get-default-buffer)
(sldb-quit))
(slime-sync-to-top-level 5))
(def-slime-test inspector
(exp)
"Test basic inspector workingness."
'(((let ((h (make-hash-table)))
(loop for i below 10 do (setf (gethash i h) i))
h))
((make-array 10))
((make-list 10))
('cons)
(#'cons))
(slime-inspect (prin1-to-string exp))
(cl-assert (not (slime-inspector-visible-p)))
(slime-wait-condition "Inspector visible" #'slime-inspector-visible-p 5)
(with-current-buffer (window-buffer (selected-window))
(slime-inspector-quit))
(slime-wait-condition "Inspector closed"
(lambda () (not (slime-inspector-visible-p)))
5)
(slime-sync-to-top-level 1))
(defun slime-buffer-visible-p (name)
(let ((buffer (window-buffer (selected-window))))
(string-match name (buffer-name buffer))))
(defun slime-inspector-visible-p ()
(slime-buffer-visible-p (slime-buffer-name :inspector)))
(defun slime-execute-as-command (name)
"Execute `name' as if it was done by the user through the
Command Loop. Similiar to `call-interactively' but also pushes on
the buffer's undo-list."
(undo-boundary)
(call-interactively name))
(def-slime-test macroexpand
(macro-defs bufcontent expansion1 search-str expansion2)
"foo"
'((("(defmacro qwertz (&body body) `(list :qwertz ',body))"
"(defmacro yxcv (&body body) `(list :yxcv (qwertz ,@body)))")
"(yxcv :A :B :C)"
"(list :yxcv (qwertz :a :b :c))"
"(qwertz"
"(list :yxcv (list :qwertz '(:a :b :c)))"))
(slime-check-top-level)
(setq slime-buffer-package ":swank")
(with-temp-buffer
(lisp-mode)
(dolist (def macro-defs)
(slime-compile-string def 0)
(slime-sync-to-top-level 5))
(insert bufcontent)
(goto-char (point-min))
(slime-execute-as-command 'slime-macroexpand-1)
(slime-wait-condition "Macroexpansion buffer visible"
(lambda ()
(slime-buffer-visible-p
(slime-buffer-name :macroexpansion)))
5)
(with-current-buffer (get-buffer (slime-buffer-name :macroexpansion))
(slime-test-expect "Initial macroexpansion is correct"
expansion1
(downcase (buffer-string))
#'slime-test-macroexpansion=)
(search-forward search-str)
(backward-up-list)
(slime-execute-as-command 'slime-macroexpand-1-inplace)
(slime-sync-to-top-level 3)
(slime-test-expect "In-place macroexpansion is correct"
expansion2
(downcase (buffer-string))
#'slime-test-macroexpansion=)
(slime-execute-as-command 'slime-macroexpand-undo)
(slime-test-expect "Expansion after undo is correct"
expansion1
(downcase (buffer-string))
#'slime-test-macroexpansion=)))
(setq slime-buffer-package ":cl-user"))
(defun slime-test-macroexpansion= (string1 string2)
(let ((string1 (replace-regexp-in-string " *\n *" " " string1))
(string2 (replace-regexp-in-string " *\n *" " " string2)))
(equal string1 string2)))
(def-slime-test indentation (buffer-content point-markers)
"Check indentation update to work correctly."
'(("
\(in-package :swank)
\(defmacro with-lolipop (&body body)
`(progn ,@body))
\(defmacro lolipop (&body body)
`(progn ,@body))
\(with-lolipop
1
2
42)
\(lolipop
1
2
23)
"
("23" "42")))
(with-temp-buffer
(lisp-mode)
(slime-lisp-mode-hook)
(insert buffer-content)
(slime-compile-region (point-min) (point-max))
(slime-sync-to-top-level 3)
(slime-update-indentation)
(slime-sync-to-top-level 3)
(dolist (marker point-markers)
(search-backward marker)
(beginning-of-defun)
(indent-sexp))
(slime-test-expect "Correct buffer content"
buffer-content
(substring-no-properties (buffer-string)))))
(def-slime-test indentation.2 (buffer-content point-markers)
"Check indentation update to work correctly."
'(("
\(in-package :swank)
\(defmacro lolipop (&body body)
`(progn ,@body))
\(defmacro lolipop (&rest body)
`(progn ,@body))
\(lolipop 1
2
23)
"
("23")))
(with-temp-buffer
(lisp-mode)
(slime-lisp-mode-hook)
(insert buffer-content)
(slime-compile-region (point-min) (point-max))
(slime-sync-to-top-level 3)
(slime-update-indentation)
(slime-sync-to-top-level 3)
(dolist (marker point-markers)
(search-backward marker)
(beginning-of-defun)
(indent-sexp))
(slime-test-expect "Correct buffer content"
buffer-content
(substring-no-properties (buffer-string)))))
(def-slime-test break
(times exp)
"Test whether BREAK invokes SLDB."
(let ((exp1 '(break)))
`((1 ,exp1) (2 ,exp1) (3 ,exp1)))
(accept-process-output nil 0.2)
(slime-check-top-level)
(slime-eval-async
`(cl:eval (cl:read-from-string
,(prin1-to-string `(dotimes (i ,times)
(unless (= i 0)
(swank::sleep-for 1))
,exp)))))
(dotimes (_i times)
(slime-wait-condition "Debugger visible"
(lambda ()
(and (slime-sldb-level= 1)
(get-buffer-window
(sldb-get-default-buffer))))
3)
(with-current-buffer (sldb-get-default-buffer)
(sldb-continue))
(slime-wait-condition "sldb closed"
(lambda () (not (sldb-get-default-buffer)))
5))
(slime-sync-to-top-level 1))
(def-slime-test (break2 (:fails-for "cmucl" "allegro"))
(times exp)
"Backends should arguably make sure that BREAK does not depend
on *DEBUGGER-HOOK*."
(let ((exp2
'(block outta
(let ((*debugger-hook* (lambda (c h) (return-from outta 42))))
(break)))))
`((1 ,exp2) (2 ,exp2) (3 ,exp2)))
(slime-test-break times exp))
(def-slime-test locally-bound-debugger-hook
()
"Test that binding *DEBUGGER-HOOK* locally works properly."
'(())
(accept-process-output nil 1)
(slime-check-top-level)
(slime-compile-string
(prin1-to-string `(defun cl-user::quux ()
(block outta
(let ((*debugger-hook*
(lambda (c hook)
(declare (ignore c hook))
(return-from outta 42))))
(error "FOO")))))
0)
(slime-sync-to-top-level 2)
(slime-eval-async '(cl-user::quux))
(slime-wait-condition "Checking that Debugger does not popup"
(lambda ()
(not (sldb-get-default-buffer)))
3)
(slime-sync-to-top-level 5))
(def-slime-test end-of-file
(expr)
"Signalling END-OF-FILE should invoke the debugger."
'(((cl:error 'cl:end-of-file))
((cl:read-from-string "")))
(let ((value (slime-eval
`(cl:let ((condition nil))
(cl:with-simple-restart
(cl:continue "continue")
(cl:let ((cl:*debugger-hook*
(cl:lambda (c h)
(cl:setq condition c)
(cl:continue))))
,expr))
(cl:if (cl:typep condition 'cl:end-of-file) t)))))
(slime-test-expect "Debugger invoked" t value)))
(def-slime-test interrupt-at-toplevel
()
"Let's see what happens if we send a user interrupt at toplevel."
'(())
(slime-check-top-level)
(unless (and (eq (slime-communication-style) :spawn)
(not (featurep 'slime-repl)))
(slime-interrupt)
(slime-wait-condition
"Debugger visible"
(lambda ()
(and (slime-sldb-level= 1)
(get-buffer-window (sldb-get-default-buffer))))
5)
(with-current-buffer (sldb-get-default-buffer)
(sldb-quit))
(slime-sync-to-top-level 5)))
(def-slime-test interrupt-in-debugger (interrupts continues)
"Let's see what happens if we interrupt the debugger.
INTERRUPTS ... number of nested interrupts
CONTINUES ... how often the continue restart should be invoked"
'((1 0) (2 1) (4 2))
(slime-check "No debugger" (not (sldb-get-default-buffer)))
(when (and (eq (slime-communication-style) :spawn)
(not (featurep 'slime-repl)))
(slime-eval-async '(swank::without-slime-interrupts
(swank::receive)))
(sit-for 0.2))
(dotimes (i interrupts)
(slime-interrupt)
(let ((level (1+ i)))
(slime-wait-condition (format "Debug level %d reachend" level)
(lambda () (equal (sldb-level) level))
2)))
(dotimes (i continues)
(with-current-buffer (sldb-get-default-buffer)
(sldb-continue))
(let ((level (- interrupts (1+ i))))
(slime-wait-condition (format "Return to debug level %d" level)
(lambda () (equal (sldb-level) level))
2)))
(with-current-buffer (sldb-get-default-buffer)
(sldb-quit))
(slime-sync-to-top-level 1))
(def-slime-test flow-control
(n delay interrupts)
"Let Lisp produce output faster than Emacs can consume it."
`((400 0.03 3))
(when noninteractive
(slime-skip-test "test is currently unstable"))
(slime-check "No debugger" (not (sldb-get-default-buffer)))
(slime-eval-async `(swank:flow-control-test ,n ,delay))
(sleep-for 0.2)
(dotimes (_i interrupts)
(slime-interrupt)
(slime-wait-condition "In debugger" (lambda () (slime-sldb-level= 1)) 5)
(slime-check "In debugger" (slime-sldb-level= 1))
(with-current-buffer (sldb-get-default-buffer)
(sldb-continue))
(slime-wait-condition "No debugger" (lambda () (slime-sldb-level= nil)) 3)
(slime-check "Debugger closed" (slime-sldb-level= nil)))
(slime-sync-to-top-level 8))
(def-slime-test sbcl-world-lock
(n delay)
"Print something from *MACROEXPAND-HOOK*.
In SBCL, the compiler grabs a lock which can be problematic because
no method dispatch code can be generated for other threads.
This test will fail more likely before dispatch caches are warmed up."
'((10 0.03)
)
(slime-test-expect "no error"
123
(slime-eval
`(cl:let ((cl:*macroexpand-hook*
(cl:lambda (fun form env)
(swank:flow-control-test ,n ,delay)
(cl:funcall fun form env))))
(cl:eval '(cl:macrolet ((foo () 123))
(foo)))))))
(def-slime-test (disconnect-one-connection (:style :spawn)) ()
"`slime-disconnect' should disconnect only the current connection"
'(())
(let ((connection-count (length slime-net-processes))
(old-connection slime-default-connection)
(slime-connected-hook nil))
(unwind-protect
(let ((slime-dispatching-connection
(slime-connect "localhost"
(slime-eval `(swank:create-server
:port 0 :style :spawn
:dont-close nil)))))
(slime-sync-to-top-level 3)
(slime-disconnect)
(slime-test-expect "Number of connections must remane the same"
connection-count
(length slime-net-processes)))
(slime-select-connection old-connection))))
(def-slime-test disconnect-and-reconnect
()
"Close the connetion.
Confirm that the subprocess continues gracefully.
Reconnect afterwards."
'(())
(slime-check-top-level)
(let* ((c (slime-connection))
(p (slime-inferior-process c)))
(with-current-buffer (process-buffer p)
(erase-buffer))
(delete-process c)
(cl-assert (equal (process-status c) 'closed) nil "Connection not closed")
(accept-process-output nil 0.1)
(cl-assert (equal (process-status p) 'run) nil "Subprocess not running")
(with-current-buffer (process-buffer p)
(cl-assert (< (buffer-size) 500) nil "Unusual output"))
(slime-inferior-connect p (slime-inferior-lisp-args p))
(let ((hook nil) (p p))
(setq hook (lambda ()
(slime-test-expect
"We are connected again" p (slime-inferior-process))
(remove-hook 'slime-connected-hook hook)))
(add-hook 'slime-connected-hook hook)
(slime-wait-condition "Lisp restarted"
(lambda ()
(not (member hook slime-connected-hook)))
5))))
(cl-defun slime-test-recipe-test-for (&key preflight
takeoff
landing)
(let ((success nil)
(test-file (make-temp-file "slime-recipe-" nil ".el"))
(test-forms
`((require 'cl-lib)
(cl-labels
((die
(reason &optional more)
(princ reason)
(terpri)
(and more (pp more))
(kill-emacs 254)))
(condition-case err
(progn ,@preflight)
(error
(die "Unexpected error running preflight forms"
err)))
(add-hook
'slime-connected-hook
#'(lambda ()
(condition-case err
(progn
,@landing
(kill-emacs 0))
(error
(die "Unexpected error running landing forms"
err))))
t)
(condition-case err
(progn
,@takeoff
,(when (null landing) '(kill-emacs 0)))
(error
(die "Unexpected error running takeoff forms"
err)))
(with-timeout
(60
(die "Timeout waiting for recipe test to finish."
takeoff))
(while t (sit-for 1)))))))
(unwind-protect
(progn
(with-temp-buffer
(mapc #'insert (mapcar #'pp-to-string test-forms))
(write-file test-file))
(with-temp-buffer
(let ((retval
(call-process (concat invocation-directory invocation-name)
nil (list t nil) nil
"-Q" "--batch"
"-l" test-file)))
(unless (= 0 retval)
(ert-fail (buffer-substring
(+ (goto-char (point-min))
(skip-chars-forward " \t\n"))
(+ (goto-char (point-max))
(skip-chars-backward " \t\n")))))))
(setq success t))
(if success (delete-file test-file)
(message "Test failed: keeping %s for inspection" test-file)))))
(define-slime-ert-test readme-recipe ()
"Test the README.md's autoload recipe."
(slime-test-recipe-test-for
:preflight `((add-to-list 'load-path ,slime-path)
(require 'slime-autoloads)
(setq inferior-lisp-program ,inferior-lisp-program)
(setq slime-contribs '(slime-fancy)))
:takeoff `((call-interactively 'slime))
:landing `((unless (and (featurep 'slime-repl)
(cl-find 'swank-repl slime-required-modules))
(die "slime-repl not loaded properly"))
(with-current-buffer (slime-repl-buffer)
(unless (and (string-match "^; +SLIME" (buffer-string))
(string-match "CL-USER> *$" (buffer-string)))
(die "REPL prompt not properly setup"
(buffer-substring-no-properties (point-min)
(point-max))))))))
(define-slime-ert-test traditional-recipe ()
"Test the README.md's traditional recipe."
(slime-test-recipe-test-for
:preflight `((add-to-list 'load-path ,slime-path)
(require 'slime)
(setq inferior-lisp-program ,inferior-lisp-program)
(slime-setup '(slime-fancy)))
:takeoff `((call-interactively 'slime))
:landing `((unless (and (featurep 'slime-repl)
(cl-find 'swank-repl slime-required-modules))
(die "slime-repl not loaded properly"))
(with-current-buffer (slime-repl-buffer)
(unless (and (string-match "^; +SLIME" (buffer-string))
(string-match "CL-USER> *$" (buffer-string)))
(die "REPL prompt not properly setup"
(buffer-substring-no-properties (point-min)
(point-max))))))))
(define-slime-ert-test readme-recipe-autoload-on-lisp-visit ()
"Test more autoload bits in README.md's installation recipe."
(slime-test-recipe-test-for
:preflight `((add-to-list 'load-path ,slime-path)
(require 'slime-autoloads))
:takeoff `((if (featurep 'slime)
(die "Didn't expect SLIME to be loaded so early!"))
(find-file ,(make-temp-file "slime-lisp-source-file" nil
".lisp"))
(unless (featurep 'slime)
(die "Expected SLIME to be fully loaded by now")))))
(defun slime-test-eval-now (string)
(cl-second (slime-eval `(swank:eval-and-grab-output ,string))))
(def-slime-test (slime-recompile-all-xrefs (:fails-for "cmucl")) ()
"Test recompilation of all references within an xref buffer."
'(())
(let* ((cell (cons nil nil))
(hook (slime-curry (lambda (cell &rest _) (setcar cell t)) cell))
(filename (make-temp-file "slime-recompile-all-xrefs" nil ".lisp")))
(add-hook 'slime-compilation-finished-hook hook)
(unwind-protect
(with-temp-file filename
(set-visited-file-name filename)
(slime-test-eval-now "(defparameter swank::*.var.* nil)")
(insert "(in-package :swank)
(defun .fn1. ())
(defun .fn2. () (.fn1.) #.*.var.*)
(defun .fn3. () (.fn1.) #.*.var.*)")
(save-buffer)
(slime-compile-and-load-file)
(slime-wait-condition "Compilation finished"
(lambda () (car cell))
5)
(slime-test-eval-now "(setq *.var.* t)")
(setcar cell nil)
(slime-xref :calls ".fn1."
(lambda (&rest args)
(apply #'slime-show-xrefs args)
(setcar cell t)))
(slime-wait-condition "Xrefs computed and displayed"
(lambda () (car cell))
5)
(setcar cell nil)
(with-current-buffer slime-xref-last-buffer
(slime-recompile-all-xrefs)
(slime-wait-condition "Compilation finished"
(lambda () (car cell))
5))
(should (cl-equalp (list (slime-test-eval-now "(.fn2.)")
(slime-test-eval-now "(.fn3.)"))
'("T" "T"))))
(remove-hook 'slime-compilation-finished-hook hook)
(when slime-xref-last-buffer
(kill-buffer slime-xref-last-buffer)))))
(provide 'slime-tests)