ert.el: Use handler-bind to record backtraces

* lisp/emacs-lisp/ert.el (ert--should-signal-hook): Delete function.
(ert--expand-should-1): Don't bind `signal-hook-function`.
(ert--test-execution-info): Remove `next-debugger` slot.
(ert--run-test-debugger): Adjust to new calling convention.
Pass the `:backtrace-base` info to the debugger.
(ert--run-test-internal): Use `handler-bind` rather than let-binding
`debugger` and `debug-on-error`.

* lisp/emacs-lisp/ert-x.el (ert-remote-temporary-file-directory): Don't
use `defconst` if it's not meant to stay constant (e.g. we let-bind it
in tramp-tests.el).
This commit is contained in:
Stefan Monnier 2023-12-18 23:57:45 -05:00
parent 7959a63ce2
commit fe0f15dbc9
2 changed files with 55 additions and 86 deletions

View file

@ -543,7 +543,7 @@ The same keyword arguments are supported as in
;; If this defconst is used in a test file, `tramp' shall be loaded
;; prior `ert-x'. There is no default value on w32 systems, which
;; could work out of the box.
(defconst ert-remote-temporary-file-directory
(defvar ert-remote-temporary-file-directory
(when (featurep 'tramp)
(cond
((getenv "REMOTE_TEMPORARY_FILE_DIRECTORY"))

View file

@ -278,14 +278,6 @@ DATA is displayed to the user and should state the reason for skipping."
(when ert--should-execution-observer
(funcall ert--should-execution-observer form-description)))
;; See Bug#24402 for why this exists
(defun ert--should-signal-hook (error-symbol data)
"Stupid hack to stop `condition-case' from catching ert signals.
It should only be stopped when ran from inside `ert--run-test-internal'."
(when (and (not (symbolp debugger)) ; only run on anonymous debugger
(memq error-symbol '(ert-test-failed ert-test-skipped)))
(funcall debugger 'error (cons error-symbol data))))
(defun ert--special-operator-p (thing)
"Return non-nil if THING is a symbol naming a special operator."
(and (symbolp thing)
@ -324,8 +316,7 @@ It should only be stopped when ran from inside `ert--run-test-internal'."
(default-value (gensym "ert-form-evaluation-aborted-")))
`(let* ((,fn (function ,fn-name))
(,args (condition-case err
(let ((signal-hook-function #'ert--should-signal-hook))
(list ,@arg-forms))
(list ,@arg-forms)
(error (progn (setq ,fn #'signal)
(list (car err)
(cdr err)))))))
@ -728,78 +719,68 @@ in front of the value of MESSAGE-FORM."
;; value and test execution should be terminated. Should not
;; return.
(exit-continuation (cl-assert nil))
;; The binding of `debugger' outside of the execution of the test.
next-debugger
;; The binding of `ert-debug-on-error' that is in effect for the
;; execution of the current test. We store it to avoid being
;; affected by any new bindings the test itself may establish. (I
;; don't remember whether this feature is important.)
ert-debug-on-error)
(defun ert--run-test-debugger (info args)
"During a test run, `debugger' is bound to a closure that calls this function.
(defun ert--run-test-debugger (info condition debugfun)
"Error handler used during the test run.
This function records failures and errors and either terminates
the test silently or calls the interactive debugger, as
appropriate.
INFO is the ert--test-execution-info corresponding to this test
run. ARGS are the arguments to `debugger'."
(cl-destructuring-bind (first-debugger-arg &rest more-debugger-args)
args
(cl-ecase first-debugger-arg
((lambda debug t exit nil)
(apply (ert--test-execution-info-next-debugger info) args))
(error
(let* ((condition (car more-debugger-args))
(type (cl-case (car condition)
((quit) 'quit)
((ert-test-skipped) 'skipped)
(otherwise 'failed)))
;; We store the backtrace in the result object for
;; `ert-results-pop-to-backtrace-for-test-at-point'.
;; This means we have to limit `print-level' and
;; `print-length' when printing result objects. That
;; might not be worth while when we can also use
;; `ert-results-rerun-test-at-point-debugging-errors',
;; (i.e., when running interactively) but having the
;; backtrace ready for printing is important for batch
;; use.
;;
;; Grab the frames above the debugger.
(backtrace (cdr (backtrace-get-frames debugger)))
(infos (reverse ert--infos)))
(setf (ert--test-execution-info-result info)
(cl-ecase type
(quit
(make-ert-test-quit :condition condition
:backtrace backtrace
:infos infos))
(skipped
(make-ert-test-skipped :condition condition
:backtrace backtrace
:infos infos))
(failed
(make-ert-test-failed :condition condition
:backtrace backtrace
:infos infos))))
;; Work around Emacs's heuristic (in eval.c) for detecting
;; errors in the debugger.
(cl-incf num-nonmacro-input-events)
;; FIXME: We should probably implement more fine-grained
;; control a la non-t `debug-on-error' here.
(cond
((ert--test-execution-info-ert-debug-on-error info)
(apply (ert--test-execution-info-next-debugger info) args))
(t))
(funcall (ert--test-execution-info-exit-continuation info)))))))
INFO is the `ert--test-execution-info' corresponding to this test run.
ERR is the error object."
(let* ((type (cl-case (car condition)
((quit) 'quit)
((ert-test-skipped) 'skipped)
(otherwise 'failed)))
;; We store the backtrace in the result object for
;; `ert-results-pop-to-backtrace-for-test-at-point'.
;; This means we have to limit `print-level' and
;; `print-length' when printing result objects. That
;; might not be worth while when we can also use
;; `ert-results-rerun-test-at-point-debugging-errors',
;; (i.e., when running interactively) but having the
;; backtrace ready for printing is important for batch
;; use.
;;
;; Grab the frames above ourselves.
(backtrace (cdr (backtrace-get-frames debugfun)))
(infos (reverse ert--infos)))
(setf (ert--test-execution-info-result info)
(cl-ecase type
(quit
(make-ert-test-quit :condition condition
:backtrace backtrace
:infos infos))
(skipped
(make-ert-test-skipped :condition condition
:backtrace backtrace
:infos infos))
(failed
(make-ert-test-failed :condition condition
:backtrace backtrace
:infos infos))))
;; FIXME: We should probably implement more fine-grained
;; control a la non-t `debug-on-error' here.
(cond
((ert--test-execution-info-ert-debug-on-error info)
;; The `debugfun' arg tells `debug' which backtrace frame starts
;; the "entering the debugger" code so it can hide those frames
;; from the backtrace.
(funcall debugger 'error condition :backtrace-base debugfun))
(t))
(funcall (ert--test-execution-info-exit-continuation info))))
(defun ert--run-test-internal (test-execution-info)
"Low-level function to run a test according to TEST-EXECUTION-INFO.
This mainly sets up debugger-related bindings."
(setf (ert--test-execution-info-next-debugger test-execution-info) debugger
(ert--test-execution-info-ert-debug-on-error test-execution-info)
(setf (ert--test-execution-info-ert-debug-on-error test-execution-info)
ert-debug-on-error)
(catch 'ert--pass
;; For now, each test gets its own temp buffer and its own
@ -807,26 +788,14 @@ This mainly sets up debugger-related bindings."
;; too expensive, we can remove it.
(with-temp-buffer
(save-window-excursion
;; FIXME: Use `signal-hook-function' instead of `debugger' to
;; handle ert errors. Once that's done, remove
;; `ert--should-signal-hook'. See Bug#24402 and Bug#11218 for
;; details.
(let ((lexical-binding t)
(debugger (lambda (&rest args)
(ert--run-test-debugger test-execution-info
args)))
(debug-on-error t)
;; Don't infloop if the error being called is erroring
;; out, and we have `debug-on-error' bound to nil inside
;; the test.
(backtrace-on-error-noninteractive nil)
(debug-on-quit t)
;; FIXME: Do we need to store the old binding of this
;; and consider it in `ert--run-test-debugger'?
(debug-ignored-errors nil)
(let ((lexical-binding t) ;;FIXME: Why?
(ert--infos '()))
(funcall (ert-test-body (ert--test-execution-info-test
test-execution-info))))))
(letrec ((debugfun (lambda (err)
(ert--run-test-debugger test-execution-info
err debugfun))))
(handler-bind (((error quit) debugfun))
(funcall (ert-test-body (ert--test-execution-info-test
test-execution-info))))))))
(ert-pass))
(setf (ert--test-execution-info-result test-execution-info)
(make-ert-test-passed))