                                   (* (float-time duration) 1000))
           (or expected (cl-incf unexpected))
           (when (ert-test-result-with-condition-p result)
-            (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)
-              (let ((message (buffer-substring-no-properties (point-min)
-                                                             (point-max))))
-                (message "%s" message)
-                (push `(message . ,message) report))))
+            (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 ""))
-        ;; 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 (nreverse report)))))
-          (unwind-protect
-              (with-temp-buffer
-                (unless (eq 0 (call-process (file-name-unquote converter) nil t
-                                            nil "--" json-file xml-output-file))
-                  (message "%s" (buffer-string))
-                  (error "Writing XML output file %s failed" xml-output-file)))
-            (delete-file json-file))))
+        (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
-          (dolist (buffer load-buffers)
-            (let ((file-name (elisp/ert/sanitize--string
-                              (file-relative-name (buffer-file-name buffer)
-                                                  runfiles-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)))))
-            (kill-buffer buffer))
-          (write-region nil nil (expand-file-name "emacs-lisp.dat" coverage-dir)
-                        nil nil nil 'excl)))
-      (kill-emacs (min unexpected 1)))))
+          (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."

         (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"))

         (load-source-file-function
          (if coverage-enabled
              (lambda (fullname file noerror _nomessage)
                (cond
                 ((file-readable-p fullname)
                  (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)))
