          (load-source-file-function
           (if coverage-enabled
               (lambda (fullname file noerror _nomessage)
-                ;; Similar to testcover.el, we use Edebug to collect coverage
-                ;; information.  The rest of this function is similar to
-                ;; ‘load-with-code-conversion’, but we ignore some edge cases.
                 (cond
                  ((file-readable-p fullname)
-                  (let ((buffer (generate-new-buffer (format "*%s*" file)))
-                        (reporter
-                         (make-progress-reporter
-                          (format-message "Loading and instrumenting %s..."
-                                          file)))
-                        (load-in-progress t)
-                        (load-file-name fullname)
-                        (set-auto-coding-for-load t)
-                        (inhibit-file-name-operation nil)
-                        (edebug-all-defs t))
-                    (with-current-buffer buffer
-                      (insert-file-contents fullname :visit)
-                      ;; The file buffer needs to be current for Edebug
-                      ;; instrumentation to work.
-                      (eval-buffer buffer nil fullname nil :do-allow-print)
-                      ;; Yuck!  We have to mess with internal Edebug data here.
-                      ;; Byte-compile all functions to be a bit more realistic.
-                      (dolist (data edebug-form-data)
-                        (byte-compile (edebug--form-data-name data))))
-                    (do-after-load-evaluation fullname)
-                    (push buffer load-buffers)
-                    (progress-reporter-done reporter)
-                    t))
+                  (push (elisp/ert/load--instrument fullname file)
+                        load-buffers))
                  (noerror nil)
                  (t (signal 'file-error (list "Cannot open load file" file)))))
             load-source-file-function)))

         (print-level 8)
         (print-length 50)
         (edebug-initial-mode 'Go-nonstop)  ; ‘step’ doesn’t work in batch mode
         (runfiles-root (getenv "TEST_SRCDIR"))
         (test-filter (getenv "TESTBRIDGE_TEST_ONLY"))
         (random-seed (or (getenv "TEST_RANDOM_SEED") ""))
         (xml-output-file (getenv "XML_OUTPUT_FILE"))

                                  (* (float-time duration) 1000))
          (or expected (cl-incf unexpected))
          (when (ert-test-result-with-condition-p result)
            (let ((message (elisp/ert/failure--message name result)))
              (message "%s" message)
              (push `(message . ,message) report)))
          (push report test-reports)))
      (push `(elapsed . ,(float-time (time-subtract nil start-time))) report)
      (push `(tests . ,(nreverse test-reports)) report)
      (cl-callf nreverse report)
      (elisp/ert/log--message "Running %d tests finished, %d results unexpected"
                              (length tests) unexpected)
      (unless (member xml-output-file '(nil ""))
        (elisp/ert/write--xml-report report xml-output-file))
      (when load-buffers
        (elisp/ert/write--coverage-report coverage-dir load-buffers))
      (kill-emacs (min unexpected 1)))))

(defun elisp/ert/failure--message (name result)
  "Return a failure message for the RESULT of a failing test.
NAME is the name of the test."
  (cl-check-type name symbol)
  (cl-check-type result ert-test-result-with-condition)
  (with-temp-buffer
    (debugger-insert-backtrace
     (ert-test-result-with-condition-backtrace result) nil)
    (goto-char (point-min))
    (while (not (eobp))
      (message "    %s"
               (buffer-substring-no-properties
                (point) (min (line-end-position) (+ 120 (point)))))
      (forward-line))
    (goto-char (point-min))
    (insert (format-message "  Test %s backtrace:\n" name))
    (goto-char (point-max))
    (dolist (info (ert-test-result-with-condition-infos result))
      (insert "  " (car info) (cdr info) ?\n))
    (insert (format-message "  Test %s condition:\n" name))
    (insert "    ")
    (pp (ert-test-result-with-condition-condition result)
        (current-buffer))
    (insert ?\n)
    (buffer-substring-no-properties (point-min) (point-max))))

(defun elisp/ert/write--xml-report (report output-file)
  "Write an XML report into OUTPUT-FILE.
REPORT must be an alist that is used for an intermediary JSON
representation of the report."
  (cl-check-type report cons)
  (cl-check-type output-file string)
  ;; Rather than trying to write a well-formed XML file in Emacs Lisp,
  ;; write the report as a JSON file and let an external binary deal with
  ;; the conversion to XML.
  (let ((process-environment
         (append (elisp/runfiles/env-vars) process-environment))
        (converter (elisp/runfiles/rlocation
                    "phst_rules_elisp/elisp/ert/write_xml_report"))
        (json-file (make-temp-file "ert-report-" nil ".json"
                                   (json-encode report))))
    (unwind-protect
        (with-temp-buffer
          (unless (eq 0 (call-process (file-name-unquote converter) nil t
                                      nil "--" json-file output-file))
            (message "%s" (buffer-string))
            (error "Writing XML output file %s failed" output-file)))
      (delete-file json-file))))

(defun elisp/ert/load--instrument (fullname file)
  "Load and instrument the Emacs Lisp file FULLNAME.
FILE is an abbreviated name as described in
‘load-source-file-function’, which see.  Return a live buffer
visiting the file."
  (cl-check-type fullname string)
  (cl-check-type file string)
  ;; Similar to testcover.el, we use Edebug to collect coverage
  ;; information.  The rest of this function is similar to
  ;; ‘load-with-code-conversion’, but we ignore some edge cases.
  (let ((buffer (generate-new-buffer (format "*%s*" file)))
        (reporter (make-progress-reporter
                   (format-message "Loading and instrumenting %s..." file)))
        (load-in-progress t)
        (load-file-name fullname)
        (set-auto-coding-for-load t)
        (inhibit-file-name-operation nil)
        (edebug-all-defs t))
    (with-current-buffer buffer
      (insert-file-contents fullname :visit)
      ;; The file buffer needs to be current for Edebug
      ;; instrumentation to work.
      (eval-buffer buffer nil fullname nil :do-allow-print)
      ;; Yuck!  We have to mess with internal Edebug data here.
      ;; Byte-compile all functions to be a bit more realistic.
      (dolist (data edebug-form-data)
        (byte-compile (edebug--form-data-name data))))
    (do-after-load-evaluation fullname)
    (progress-reporter-done reporter)
    buffer))

(defun elisp/ert/write--coverage-report (coverage-dir buffers)
  "Write a coverage report to a file in COVERAGE-DIR.
BUFFERS is a list of buffers containing Emacs Lisp sources
instrumented using Edebug."
  (cl-check-type coverage-dir string)
  (cl-check-type buffers cons)
  (with-temp-buffer
    (let ((root (getenv "TEST_SRCDIR")))
      (dolist (buffer buffers)
        (elisp/ert/insert--coverage-report buffer root)
        (kill-buffer buffer)))
    (write-region nil nil (expand-file-name "emacs-lisp.dat" coverage-dir)
                  nil nil nil 'excl)))

(defun elisp/ert/insert--coverage-report (buffer root)
  "Insert a coverage report into the current buffer.
BUFFER must be a different buffer visiting an Emacs Lisp source
file that has been instrumented with Edebug.  ROOT is a directory
to be used as root."
  (cl-check-type buffer buffer-live)
  (cl-check-type root string)
  (let ((file-name (elisp/ert/sanitize--string
                    (file-relative-name (buffer-file-name buffer) root)))
        (functions ())
        (functions-hit 0)
        (lines (make-hash-table :test #'eql)))
    (with-current-buffer buffer
      (widen)
      ;; Yuck!  More messing around with Edebug internals.
      (dolist (data edebug-form-data)
        (let* ((name (edebug--form-data-name data))
               (frequencies (get name 'edebug-freq-count))
               ;; We don’t really know the number of function calls,
               ;; so assume it’s the same as the hit count of the
               ;; first breakpoint.
               (calls (or (cl-find 0 frequencies :test-not #'eq) 0))
               (stuff (get name 'edebug))
               (begin (car stuff))
               (offsets (caddr stuff)))
          (cl-incf functions-hit (min calls 1))
          (cl-assert (eq (marker-buffer begin) buffer))
          (cl-assert (eql (length frequencies) (length offsets)))
          (cl-loop for offset across offsets
                   and freq across frequencies
                   for position = (+ begin offset)
                   for line = (line-number-at-pos position)
                   do (cl-callf max (gethash line lines 0) freq))
          (push (list (elisp/ert/sanitize--string (symbol-name name))
                      (line-number-at-pos begin) calls)
                functions))))
    (cl-callf nreverse functions)
    ;; The expected format is described to some extend in the
    ;; geninfo(1) man page.
    (insert (format "SF:%s\n" file-name))
    (dolist (func functions)
      (insert (format "FN:%d,%s\n" (cadr func) (car func))))
    (dolist (func functions)
      (insert (format "FNDA:%d,%s\n" (caddr func) (car func))))
    (insert (format "FNF:%d\n" (length functions)))
    (insert (format "FNH:%d\n" functions-hit))
    (let ((list ())
          (lines-hit 0))
      (maphash (lambda (line freq) (push (cons line freq) list)) lines)
      (cl-callf sort list #'car-less-than-car)
      (dolist (line list)
        (cl-incf lines-hit (min (cdr line) 1))
        (insert (format "DA:%d,%d\n" (car line) (cdr line))))
      (insert (format "LH:%d\nLF:%d\nend_of_record\n"
                      lines-hit (hash-table-count lines))))))

(defun elisp/ert/log--message (format &rest args)
  "Like ‘(message FORMAT ARGS…)’, but also print a timestamp."
