summaryrefslogtreecommitdiff
path: root/lisp/emacs-lisp/ert.el
diff options
context:
space:
mode:
Diffstat (limited to 'lisp/emacs-lisp/ert.el')
-rw-r--r--lisp/emacs-lisp/ert.el244
1 files changed, 195 insertions, 49 deletions
diff --git a/lisp/emacs-lisp/ert.el b/lisp/emacs-lisp/ert.el
index 02551bad31f..c57bd0a69e2 100644
--- a/lisp/emacs-lisp/ert.el
+++ b/lisp/emacs-lisp/ert.el
@@ -328,8 +328,8 @@ DATA is displayed to the user and should state the reason for skipping."
(unless (eql ,value ',default-value)
(list :value ,value))
(unless (eql ,value ',default-value)
- (when-let ((-explainer-
- (ert--get-explainer ',fn-name)))
+ (when-let* ((-explainer-
+ (ert--get-explainer ',fn-name)))
(list :explanation
(apply -explainer- ,args)))))
value)
@@ -576,7 +576,7 @@ Return nil if they are."
(defun ert--significant-plist-keys (plist)
"Return the keys of PLIST that have non-null values, in order."
- (cl-assert (zerop (mod (length plist) 2)) t)
+ (cl-assert (evenp (length plist)) t)
(cl-loop for (key value . rest) on plist by #'cddr
unless (or (null value) (memq key accu)) collect key into accu
finally (cl-return accu)))
@@ -587,8 +587,8 @@ Return nil if they are."
Returns nil if they are equivalent, i.e., have the same value for
each key, where absent values are treated as nil. The order of
key/value pairs in each list does not matter."
- (cl-assert (zerop (mod (length a) 2)) t)
- (cl-assert (zerop (mod (length b) 2)) t)
+ (cl-assert (evenp (length a)) t)
+ (cl-assert (evenp (length b)) t)
;; Normalizing the plists would be another way to do this but it
;; requires a total ordering on all lisp objects (since any object
;; is valid as a text property key). Perhaps defining such an
@@ -1159,21 +1159,21 @@ Also changes the counters in STATS to match."
(aref results pos))
(cl-etypecase (aref results pos)
(ert-test-passed
- (cl-incf (ert--stats-passed-expected stats) d))
+ (incf (ert--stats-passed-expected stats) d))
(ert-test-failed
- (cl-incf (ert--stats-failed-expected stats) d))
+ (incf (ert--stats-failed-expected stats) d))
(ert-test-skipped
- (cl-incf (ert--stats-skipped stats) d))
+ (incf (ert--stats-skipped stats) d))
(null)
(ert-test-aborted-with-non-local-exit)
(ert-test-quit))
(cl-etypecase (aref results pos)
(ert-test-passed
- (cl-incf (ert--stats-passed-unexpected stats) d))
+ (incf (ert--stats-passed-unexpected stats) d))
(ert-test-failed
- (cl-incf (ert--stats-failed-unexpected stats) d))
+ (incf (ert--stats-failed-unexpected stats) d))
(ert-test-skipped
- (cl-incf (ert--stats-skipped stats) d))
+ (incf (ert--stats-skipped stats) d))
(null)
(ert-test-aborted-with-non-local-exit)
(ert-test-quit)))))
@@ -1316,13 +1316,9 @@ empty string."
(defun ert--pp-with-indentation-and-newline (object)
"Pretty-print OBJECT, indenting it to the current column of point.
Ensures a final newline is inserted."
- (let ((begin (point))
- (cols (current-column))
- (pp-escape-newlines t)
+ (let ((pp-escape-newlines t)
(print-escape-control-characters t))
- (pp object (current-buffer))
- (unless (bolp) (insert "\n"))
- (indent-rigidly begin (point) cols)))
+ (pp object (current-buffer))))
(defun ert--insert-infos (result)
"Insert `ert-info' infos from RESULT into current buffer.
@@ -1356,10 +1352,10 @@ RESULT must be an `ert-test-result-with-condition'."
(defun ert-test-location (test)
"Return a string description the source location of TEST."
- (when-let ((loc
- (ignore-errors
- (find-function-search-for-symbol
- (ert-test-name test) 'ert-deftest (ert-test-file-name test)))))
+ (when-let* ((loc
+ (ignore-errors
+ (find-function-search-for-symbol
+ (ert-test-name test) 'ert--test (ert-test-file-name test)))))
(let* ((buffer (car loc))
(point (cdr loc))
(file (file-relative-name (buffer-file-name buffer)))
@@ -1423,7 +1419,7 @@ Returns the stats object."
(message "%9s %S%s"
(ert-string-for-test-result result nil)
(ert-test-name test)
- (if (cl-plusp
+ (if (plusp
(length (getenv "EMACS_TEST_VERBOSE")))
(ert-reason-for-test-result result)
""))))
@@ -1436,7 +1432,7 @@ Returns the stats object."
(message "%9s %S%s"
(ert-string-for-test-result result nil)
(ert-test-name test)
- (if (cl-plusp
+ (if (plusp
(length (getenv "EMACS_TEST_VERBOSE")))
(ert-reason-for-test-result result)
""))))
@@ -1552,11 +1548,11 @@ test packages depend on each other, it might be helpful.")
"Write a JUnit test report, generated from STATS."
;; https://www.ibm.com/docs/en/developer-for-zos/14.1.0?topic=formats-junit-xml-format
;; https://llg.cubic.org/docs/junit/
- (when-let ((symbol (car (apropos-internal "" #'ert-test-boundp)))
- (test-file (symbol-file symbol 'ert--test))
- (test-report
- (file-name-with-extension
- (or ert-load-file-name test-file) "xml")))
+ (when-let* ((symbol (car (apropos-internal "" #'ert-test-boundp)))
+ (test-file (symbol-file symbol 'ert--test))
+ (test-report
+ (file-name-with-extension
+ (or ert-load-file-name test-file) "xml")))
(with-temp-file test-report
(insert "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n")
(insert (format "<testsuites name=\"%s\" tests=\"%s\" errors=\"%s\" failures=\"%s\" skipped=\"%s\" time=\"%s\">\n"
@@ -1688,8 +1684,8 @@ test packages depend on each other, it might be helpful.")
(insert " </error>\n"
" </testcase>\n"
" </testsuite>\n")
- (cl-incf errors 1)
- (cl-incf id 1)))
+ (incf errors 1)
+ (incf id 1)))
(insert-file-contents-literally test-report)
(when (looking-at-p
@@ -1697,15 +1693,15 @@ test packages depend on each other, it might be helpful.")
(delete-region (point) (line-beginning-position 2)))
(when (looking-at
"<testsuites name=\".+\" tests=\"\\(.+\\)\" errors=\"\\(.+\\)\" failures=\"\\(.+\\)\" skipped=\"\\(.+\\)\" time=\"\\(.+\\)\">")
- (cl-incf tests (string-to-number (match-string 1)))
- (cl-incf errors (string-to-number (match-string 2)))
- (cl-incf failures (string-to-number (match-string 3)))
- (cl-incf skipped (string-to-number (match-string 4)))
- (cl-incf time (string-to-number (match-string 5)))
+ (incf tests (string-to-number (match-string 1)))
+ (incf errors (string-to-number (match-string 2)))
+ (incf failures (string-to-number (match-string 3)))
+ (incf skipped (string-to-number (match-string 4)))
+ (incf time (string-to-number (match-string 5)))
(delete-region (point) (line-beginning-position 2)))
(when (looking-at " <testsuite id=\"\\(0\\)\"")
(replace-match (number-to-string id) nil nil nil 1)
- (cl-incf id 1))
+ (incf id 1))
(goto-char (point-max))
(beginning-of-line 0)
(when (looking-at-p "</testsuites>")
@@ -2127,7 +2123,7 @@ non-nil, returns the face for expected results.."
(defun ert-face-for-stats (stats)
"Return a face that represents STATS."
(cond ((ert--stats-aborted-p stats) 'nil)
- ((cl-plusp (ert-stats-completed-unexpected stats))
+ ((plusp (ert-stats-completed-unexpected stats))
(ert-face-for-test-result nil))
((eql (ert-stats-completed-expected stats) (ert-stats-total stats))
(ert-face-for-test-result t))
@@ -2471,7 +2467,9 @@ To be used in the ERT results buffer."
(defun ert--test-name-button-action (button)
"Find the definition of the test BUTTON belongs to, in another window."
- (let ((name (button-get button 'ert-test-name)))
+ ;; work with either ert-insert-test-name-button or help-xref-button
+ (let ((name (or (button-get button 'ert-test-name)
+ (car (button-get button 'help-args)))))
(ert-find-test-other-window name)))
(defun ert--ewoc-position (ewoc node)
@@ -2818,7 +2816,8 @@ To be used in the ERT results buffer."
(file-name-nondirectory file-name)))
(save-excursion
(re-search-backward (substitute-command-keys "`\\([^`']+\\)'"))
- (help-xref-button 1 'help-function-def test-name file-name)))
+ (help-xref-button 1 'ert--test-name-button
+ test-name file-name)))
(insert ".")
(fill-region-as-paragraph (point-min) (point))
(insert "\n\n")
@@ -2857,14 +2856,16 @@ To be used in the ERT results buffer."
(ert--tests-running-mode-line-indicator))))
(add-hook 'emacs-lisp-mode-hook #'ert--activate-font-lock-keywords)
-(defun ert--unload-function ()
+(defun ert-unload-function ()
"Unload function to undo the side-effects of loading ert.el."
- (ert--remove-from-list 'find-function-regexp-alist 'ert-deftest :key #'car)
+ (ert--remove-from-list 'find-function-regexp-alist 'ert--test :key #'car)
(ert--remove-from-list 'minor-mode-alist 'ert--current-run-stats :key #'car)
(ert--remove-from-list 'emacs-lisp-mode-hook
'ert--activate-font-lock-keywords)
nil)
+;;; erts files.
+
(defun ert-test-erts-file (file &optional transform)
"Parse FILE as a file containing before/after parts (an erts file).
@@ -2910,10 +2911,10 @@ write erts files."
(setq end-before end-after
start-after start-before))
;; Update persistent specs.
- (when-let ((point-char (assq 'point-char specs)))
+ (when-let* ((point-char (assq 'point-char specs)))
(setq gen-specs
(map-insert gen-specs 'point-char (cdr point-char))))
- (when-let ((code (cdr (assq 'code specs))))
+ (when-let* ((code (cdr (assq 'code specs))))
(setq gen-specs
(map-insert gen-specs 'code (car (read-from-string code)))))
;; Get the "after" strings.
@@ -2921,12 +2922,12 @@ write erts files."
(insert-buffer-substring file-buffer start-after end-after)
(ert--erts-unquote)
;; Remove the newline at the end of the buffer.
- (when-let ((no-newline (cdr (assq 'no-after-newline specs))))
+ (when-let* ((no-newline (cdr (assq 'no-after-newline specs))))
(goto-char (point-min))
(when (re-search-forward "\n\\'" nil t)
(delete-region (match-beginning 0) (match-end 0))))
;; Get the expected "after" point.
- (when-let ((point-char (cdr (assq 'point-char gen-specs))))
+ (when-let* ((point-char (cdr (assq 'point-char gen-specs))))
(goto-char (point-min))
(when (search-forward point-char nil t)
(delete-region (match-beginning 0) (match-end 0))
@@ -2937,13 +2938,13 @@ write erts files."
(insert-buffer-substring file-buffer start-before end-before)
(ert--erts-unquote)
;; Remove the newline at the end of the buffer.
- (when-let ((no-newline (cdr (assq 'no-before-newline specs))))
+ (when-let* ((no-newline (cdr (assq 'no-before-newline specs))))
(goto-char (point-min))
(when (re-search-forward "\n\\'" nil t)
(delete-region (match-beginning 0) (match-end 0))))
(goto-char (point-min))
;; Place point in the specified place.
- (when-let ((point-char (cdr (assq 'point-char gen-specs))))
+ (when-let* ((point-char (cdr (assq 'point-char gen-specs))))
(when (search-forward point-char nil t)
(delete-region (match-beginning 0) (match-end 0))))
(let ((code (cdr (assq 'code gen-specs))))
@@ -2991,8 +2992,151 @@ write erts files."
(forward-line 1)))
(nreverse specs))))
-(defvar ert-unload-hook ())
-(add-hook 'ert-unload-hook #'ert--unload-function)
+
+;;; Buffer related helpers
+
+(defun ert--text-button (string &rest properties)
+ "Return a string containing STRING as a text button with PROPERTIES.
+
+See `make-text-button'."
+ (with-temp-buffer
+ (insert string)
+ (apply #'make-text-button (point-min) (point-max) properties)
+ (buffer-string)))
+
+(defun ert--format-test-buffer-name (base-name)
+ "Compute a test buffer name based on BASE-NAME.
+
+Helper function for `ert--test-buffers'."
+ (format "*Test buffer (%s)%s*"
+ (or (and (ert-running-test)
+ (ert-test-name (ert-running-test)))
+ "<anonymous test>")
+ (if base-name
+ (format ": %s" base-name)
+ "")))
+
+(defvar ert--test-buffers (make-hash-table :weakness t)
+ "Table of all test buffers. Keys are the buffer objects, values are t.
+
+The main use of this table is for `ert-kill-all-test-buffers'.
+Not all buffers in this table are necessarily live, but all live
+test buffers are in this table.")
+
+(define-button-type 'ert--test-buffer-button
+ 'action #'ert--test-buffer-button-action
+ 'help-echo "mouse-2, RET: Pop to test buffer")
+
+(defun ert--test-buffer-button-action (button)
+ "Pop to the test buffer that BUTTON is associated with."
+ (pop-to-buffer (button-get button 'ert--test-buffer)))
+
+(defun ert--call-with-test-buffer (ert--base-name ert--thunk)
+ "Helper function for `ert-with-test-buffer'.
+
+Create a test buffer with a name based on ERT--BASE-NAME and run
+ERT--THUNK with that buffer as current."
+ (let* ((ert--buffer (generate-new-buffer
+ (ert--format-test-buffer-name ert--base-name)))
+ (ert--button (ert--text-button (buffer-name ert--buffer)
+ :type 'ert--test-buffer-button
+ 'ert--test-buffer ert--buffer)))
+ (puthash ert--buffer 't ert--test-buffers)
+ ;; We don't use `unwind-protect' here since we want to kill the
+ ;; buffer only on success.
+ (prog1 (with-current-buffer ert--buffer
+ (ert-info (ert--button :prefix "Buffer: ")
+ (funcall ert--thunk)))
+ (kill-buffer ert--buffer)
+ (remhash ert--buffer ert--test-buffers))))
+
+(cl-defmacro ert-with-test-buffer ((&key ((:name name-form))
+ ((:selected select-form)))
+ &body body)
+ "Create a test buffer and run BODY in that buffer.
+
+To be used in ERT tests. If BODY finishes successfully, the test buffer
+is killed; if there is an error, the test buffer is kept around for
+further inspection. The name of the buffer is derived from the name of
+the test and the result of NAME-FORM.
+
+If SELECT-FORM is non-nil, switch to the test buffer before running
+BODY, as if body was in `ert-with-buffer-selected'.
+
+The return value is the last form in BODY."
+ (declare (debug ((":name" form) (":selected" form) def-body))
+ (indent 1))
+ `(ert--call-with-test-buffer
+ ,name-form
+ ,(if select-form
+ `(lambda () (ert-with-buffer-selected (current-buffer)
+ ,@body))
+ `(lambda () ,@body))))
+
+(defun ert-kill-all-test-buffers ()
+ "Kill all test buffers that are still live."
+ (interactive)
+ (let ((count 0))
+ (maphash (lambda (buffer _dummy)
+ (when (or (not (buffer-live-p buffer))
+ (kill-buffer buffer))
+ (incf count)))
+ ert--test-buffers)
+ (message "%s out of %s test buffers killed"
+ count (hash-table-count ert--test-buffers)))
+ ;; It could be that some test buffers were actually kept alive
+ ;; (e.g., due to `kill-buffer-query-functions'). I'm not sure what
+ ;; to do about this. For now, let's just forget them.
+ (clrhash ert--test-buffers)
+ nil)
+
+(cl-defmacro ert-with-buffer-selected (buffer-or-name &body body)
+ "Display a buffer in a temporary selected window and run BODY.
+
+If BUFFER-OR-NAME is nil, the current buffer is used.
+
+The buffer is made the current buffer, and the temporary window
+becomes the `selected-window', before BODY is evaluated. The
+modification hooks `before-change-functions' and
+`after-change-functions' are not inhibited during the evaluation
+of BODY, which makes it easier to use `execute-kbd-macro' to
+simulate user interaction. The window configuration is restored
+before returning, even if BODY exits nonlocally. The return
+value is the last form in BODY."
+ (declare (debug (form body)) (indent 1))
+ `(save-window-excursion
+ (with-current-buffer (or ,buffer-or-name (current-buffer))
+ (with-selected-window (display-buffer (current-buffer))
+ ,@body))))
+
+(defun ert-call-with-buffer-renamed (buffer-name thunk)
+ "Protect the buffer named BUFFER-NAME from side-effects and run THUNK.
+
+Renames the buffer BUFFER-NAME to a new temporary name, creates a
+new buffer named BUFFER-NAME, executes THUNK, kills the new
+buffer, and renames the original buffer back to BUFFER-NAME.
+
+This is useful if THUNK has undesirable side-effects on an Emacs
+buffer with a fixed name such as *Messages*."
+ (let ((new-buffer-name (generate-new-buffer-name
+ (format "%s orig buffer" buffer-name))))
+ (with-current-buffer (get-buffer-create buffer-name)
+ (rename-buffer new-buffer-name))
+ (unwind-protect
+ (progn
+ (get-buffer-create buffer-name)
+ (funcall thunk))
+ (when (get-buffer buffer-name)
+ (kill-buffer buffer-name))
+ (with-current-buffer new-buffer-name
+ (rename-buffer buffer-name)))))
+
+(cl-defmacro ert-with-buffer-renamed ((buffer-name-form) &body body)
+ "Protect the buffer named BUFFER-NAME from side-effects and run BODY.
+
+See `ert-call-with-buffer-renamed' for details."
+ (declare (indent 1))
+ `(ert-call-with-buffer-renamed ,buffer-name-form (lambda () ,@body)))
;;; Obsolete
@@ -3001,6 +3145,8 @@ write erts files."
(put 'ert-equal-including-properties 'ert-explainer
'ert--explain-equal-including-properties)
+(define-obsolete-function-alias 'ert--unload-function 'ert-unload-function "31.1")
+
(provide 'ert)
;;; ert.el ends here