Subject: [PATCH] Add tests for lisp/kmacro.el

From: Gemini Lasswell <[email-masked]>
Date: Sun, 13 Nov 2016 13:23:51 -0800

Hello,
There weren't any tests for kmacro.el, so I have written some.

Two things that may yet need to be done:

1. These tests make extensive use of two macros, called
kmacro-tests-should-call and kmacro-tests-should-not-call. They are
context-creating macros which add advice to named functions for the
duration of a test. I think that these two macros would be a useful
addition to ERT, and I'll submit that idea as a separate patch.

2. I found several minor bugs in the process of writing these, leading
to tests marked as :expected-result :failed. One is a way to create an
empty keyboard macro using the mouse and the rest are ways to get
kmacro-step-edit-macro to behave oddly. I haven't sent them to
bug-gnu-emacs yet. When I do so would it be better to send them
individually or put all the step-edit ones in one report?

My copyright assignment paperwork was finished as of Nov 2.

>From 6dc518ac6f356a55593184ed6e1bafa5cd764623 Mon Sep 17 00:00:00 2001
From: gazally <[email-masked]>
Date: Sun, 13 Nov 2016 08:02:38 -0800
Subject: [PATCH] Add tests for lisp/kmacro.el

* test/lisp/kmacro-tests.el: New file.
---
 test/lisp/kmacro-tests.el | 1251 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 1251 insertions(+)
 create mode 100644 test/lisp/kmacro-tests.el

diff --git a/test/lisp/kmacro-tests.el b/test/lisp/kmacro-tests.el
new file mode 100644
index 0000000..fae9cbc
--- /dev/null
+++ b/test/lisp/kmacro-tests.el
@@ -0,0 +1,1251 @@
+;;; kmacro-tests.el --- Tests for kmacro.el       -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2016 Free Software Foundation, Inc.
+
+;; Author: Gemini Lasswell
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'kmacro)
+(require 'edmacro)
+(require 'ert)
+(require 'ert-x)
+
+;;; Test fixtures:
+
+(defvar kmacro-tests-keymap
+  (let ((map (make-sparse-keymap)))
+
+    (dotimes (i 26)
+      (define-key map (string (+ ?a i)) 'self-insert-command))
+    (dotimes (i 10)
+      (define-key map (string (+ ?0 i)) 'self-insert-command))
+
+    ;; define a few key sequences of different lengths
+    (dolist (item '(("\C-a" . beginning-of-line)
+                    ("\C-e" . end-of-line)
+                    ("\C-r" . isearch-backward)
+                    ("\C-u" . universal-argument)
+                    ("\M-x" . execute-extended-command)
+                    ("\C-cd" . downcase-word)
+                    ("\C-cxu" . upcase-word)
+                    ("\C-cxq" . quoted-insert)))
+      (define-key map (car item) (cdr item)))
+
+    map)
+  "Keymap to use for testing keyboard macros.
+Used to obtain consistent results even if tests are run in an
+environment with rebound keys.")
+
+(defmacro kmacro-tests-with-kmacro-clean-slate (directions &rest body)
+  "Create a clean environment for a kmacro test to run in.
+Save the kmacro global variables and set them to reasonable
+default values.  Temporarily bind the functions defined by
+macros.c and used by kmacro.el to mocked versions as requested by
+DIRECTIONS (see the docstring of `kmacro-tests-make-macros-c-rebindings'
+for details).  Then execute BODY, and restore the variables and
+functions."
+  (declare (debug ((&rest (gate gv-place &optional form)) body)))
+  (let ((var-bindings
+         '(;; Macro customizations
+           (kmacro-execute-before-append t)
+           (kmacro-ring-max 8)
+           (kmacro-repeat-no-prefix t)
+           (kmacro-call-repeat-key nil)
+           (kmacro-call-repeat-with-arg nil)
+
+           ;; Macro recording state
+           (kbd-macro-termination-hook nil)
+           (defining-kbd-macro nil)
+           (executing-kbd-macro nil)
+           (executing-kbd-macro-index 0)
+           (last-kbd-macro nil)
+
+           ;; Macro history
+           (kmacro-ring nil)
+
+           ;; Macro counter
+           (kmacro-counter 0)
+           (kmacro-default-counter-format "%d")
+           (kmacro-counter-format "%d")
+           (kmacro-counter-format-start "%d")
+           (kmacro-counter-value-start 0)
+           (kmacro-last-counter 0)
+           (kmacro-initial-counter-value nil)))
+
+        ;; Rebindings for macros.c functions
+        (func-bindings (kmacro-tests-make-macros-c-rebindings directions)))
+
+    `(cl-letf* ,(append var-bindings func-bindings) ,@body)))
+
+(defvar kmacro-tests-stubs
+  '((start-kbd-macro .     (lambda (_append _noexec)
+                             (setq defining-kbd-macro t)))
+    (end-kbd-macro .       (lambda (&optional _repeat _loopfunc)
+                             (setq defining-kbd-macro nil)))
+    (call-last-kbd-macro . (lambda (_count loopfunc)
+                             (and loopfunc (funcall loopfunc))))
+    (execute-kbd-macro .   (lambda (_mac _count loopfunc)
+                             (and loopfunc (funcall loopfunc)))))
+  "Stubs for functions from macros.c that can be used to test kmacro.el.")
+
+(defun kmacro-tests-make-macros-c-rebindings (directions)
+  "Create rebindings suitable for `cl-letf' for the functions in macros.c.
+DIRECTIONS is a list that may contain :use, :stub and the symbols
+for the four functions in macros.c which are used by
+kmacro.el.  If any of the four functions is not in the list it
+will be rebound to a form which will cause a test failure if it
+is called.  A function preceded by :stub will be bound to a mock
+with a minimal level of functionality, and one preceded by :use
+will be left unbound."
+  (let ((protected (mapcar #'car kmacro-tests-stubs))
+        stub-bindings safety-bindings func directive)
+    (while directions
+      (if (keywordp (car directions))
+          (setq directive (car directions))
+        (setq func (car directions))
+        (cond
+         ;; directions should only contain macros.c symbols or :use or :stub
+         ((or (not (keywordp directive))
+              (not (symbolp func))
+              (not (assoc func kmacro-tests-stubs)))
+          (message "rebindings syntax %s" directions))
+
+         ;; write a binding for a stubbed function
+         ((eq directive :stub)
+          (let ((binding
+                 `((symbol-function #',func)
+                   (lambda (&rest args)
+                     (apply ,(alist-get func kmacro-tests-stubs) args)))))
+            (setq stub-bindings (cons binding stub-bindings))))
+
+         ;; Remove function from list that will be protected from
+         ;; unwanted calls
+         ((eq directive :use) (setq protected (delq func protected)))
+
+         (t (should-not directive))))
+      (setq directions (cdr directions)))
+
+    ;; Write bindings that will cause a test failure if any functions
+    ;; not named by :stub or :use are called.
+    (setq safety-bindings
+          (mapcar (lambda (func)
+                    `((symbol-function #',func)
+                      (lambda (&rest args) (should-not ',func))))
+                  protected))
+    (append safety-bindings stub-bindings)))
+
+;;; Wrapper for ert-deftest to set up everthing a kmacro test needs
+
+(defmacro kmacro-tests-deftest (name _args docstring &rest keys-and-body)
+  "Set up clean context for a kmacro unit test.
+NAME is the name of the test, _ARGS should be nil, and DOCSTRING
+is required.  To avoid having to duplicate ert's keyword parsing
+here, its keywords and values (if any) must be inside a list
+after the docstring, preceding the body, here combined with the
+body in KEYS-AND-BODY.  Directives for rebinding functions from
+macros.c may be placed in another list after ert's keywords and
+before the body.  For example:
+
+    (kmacro-tests-deftest a-kmacro-test ()
+      \"docstring\"
+       (:tags '(:expensive-test) :expected-result :fail)
+       (:stub end-kbd-macro :use execute-kbd-macro)
+       (body-of-test))"
+
+  (declare (debug (name sexp stringp
+                        (&rest keywordp sexp)
+                        (&rest [keywordp sexp])
+                        body))
+           (doc-string 3)
+           (indent 2))
+
+  (let* ((keys (when (and (listp (car keys-and-body))
+                          (keywordp (caar keys-and-body))
+                          (not (eq (caar keys-and-body) :use))
+                          (not (eq (caar keys-and-body) :stub)))
+                 (car keys-and-body)))
+         (directions-and-body (if keys (cdr keys-and-body)
+                                keys-and-body))
+         (directions (when (and (listp (car directions-and-body))
+                                (keywordp (caar directions-and-body)))
+                       (car directions-and-body)))
+         (body (if directions (cdr directions-and-body)
+                 directions-and-body)))
+    `(ert-deftest ,name ()
+       ,docstring ,@keys
+       (kmacro-tests-with-kmacro-clean-slate ,directions
+                                             ,@body))))
+
+(defmacro kmacro-tests-with-current-prefix-arg (form)
+  "Set `current-prefix-arg' temporarily while executing FORM.
+Use the second element of form as the argument.  It will be
+evaluated twice."
+  (declare (debug (form)))
+  `(cl-letf ((current-prefix-arg ,(cadr form)))
+     ,form))
+
+;; Some more powerful expectations
+
+(defun kmacro-tests-arg-recorder ()
+  "Return a closure which will save its arguments for later examination.
+Helper for `kmacro-tests-should-call'.  Create a lambda which
+saves the arguments given to it inside a closure.  If you instead
+pass it :call-args, it will return the list of saved arguments."
+  (let (arglist)
+    (lambda (&rest args)
+      (if (eq (car args) :call-args)
+          arglist
+        (setq arglist (cons args arglist))))))
+
+(defmacro kmacro-tests-should-call (defs &rest body)
+  "Verify that the function(s) in DEFS are called by BODY.
+DEFS should be a list containing elements of the form:
+ (FUNC ARGCHECK WHERE FUNCTION)
+
+where FUNC is a symbol, ARGCHECK is either :once, :times followed
+by a value, or :check-args-with followed by a function value.
+WHERE and FUNCTION are optional and have the same meaning as in
+`advice-add'.
+
+Temporarily add advice to each FUNC in DEFS, including advice
+which records the arguments passed to FUNC (by reference not
+copy, relevant for destructive functions), execute BODY, and then
+depending on the ARGCHECK forms, verify that FUNC was either
+called once, a specified number of times, or that the function
+following :check-args-with returns a non-nil value when passed a
+list of all the arguments passed to FUNC (which will be in
+reverse order)."
+  (declare (debug ((&rest (gate fboundp
+                                [&or ":once"
+                                     [":times" form]
+                                     [":check-args-with" function-form]]
+                                &optional keywordp function-form))
+                   body))
+           (indent 1))
+  (if (null defs)
+      `(progn ,@body)
+    (let* ((g-argrec (cl-gensym))
+           (g-advice (cl-gensym))
+           (def (car defs))
+           (func (car def))
+           (check-type (nth 1 def))
+           (advice-given (> (length def) 3))
+           (advice-keyword (car (last def 2)))
+           (advice-function (car (last def))))
+      ;; Add two pieces of advice to the function: the one provided in
+      ;; the definitions list, and another to record the arguments.
+      `(let ((,g-argrec (kmacro-tests-arg-recorder))
+             (,g-advice ,advice-function)) ; only evaluate lambdas once
+         (advice-add ',func :before ,g-argrec '((depth . -100)))
+         (unwind-protect
+             (progn
+               ,(when advice-given
+                  `(advice-add ',func ,advice-keyword ,g-advice
+                               '((depth . -99))))
+               (unwind-protect
+                   (kmacro-tests-should-call ,(cdr defs) ,@body)
+                 ,(when advice-given
+                    `(advice-remove ',func ,g-advice))))
+           (advice-remove ',func ,g-argrec))
+         ;; Generate the after-execution argument list check.
+         ,(cond
+           ((eq check-type :once)
+            `(should (eql 1 (length (funcall ,g-argrec :call-args)))))
+           ((eq check-type :times)
+            `(should (eql ,(nth 2 def) (length (funcall ,g-argrec
+                                                        :call-args)))))
+           ((eq check-type :check-args-with)
+            `(should (funcall ,(nth 2 def) (funcall ,g-argrec
+                                                    :call-args)))))))))
+
+(defmacro kmacro-tests-should-not-call (func-or-funcs &rest body)
+  "Verify that a function or functions are not called during execution.
+FUNC-OR-FUNCS can either be a single function or a list of them.
+Temporarily override them them during the execution of BODY with advice
+containing `should-not' and a non-nil value.  Use the function symbol
+as the non-nil value to make tracking down what went wrong a bit
+faster."
+  (declare (debug (&or [(&rest fboundp) body]
+                       [fboundp body]))
+           (indent 1))
+  (let* ((funcs (if (consp func-or-funcs)
+                    func-or-funcs
+                  (list func-or-funcs)))
+         (defs (mapcar (lambda (f)
+                         `(,f :times 0 :override
+                              (lambda (&rest args) (should-not ',f))))
+                       funcs)))
+    `(kmacro-tests-should-call ,defs
+       ,@body)))
+
+(defmacro kmacro-tests-should-insert (value &rest body)
+  "Verify that VALUE is inserted by the execution of BODY.
+Execute BODY, then check that the string VALUE was inserted
+into the current buffer at point.  As a side effect captures the
+symbols p_ and bsize_."
+  (declare (debug (stringp body))
+           (indent 1))
+  `(let ((p_ (point))
+         (bsize_ (buffer-size)))
+     ,@body
+     (should (equal (buffer-substring p_ (point)) ,value))
+     (should (equal (- (buffer-size) bsize_) (string-width ,value)))))
+
+(defmacro kmacro-tests-should-match-message (value &rest body)
+  "Verify that a message matching VALUE is issued while executing BODY.
+Execute BODY, then check for a regexp match between
+VALUE and any text written to *Messages* during the execution."
+  (declare (debug (stringp body))
+           (indent 1))
+  `(with-current-buffer (get-buffer-create "*Messages*")
+     (save-restriction
+       (narrow-to-region (point-max) (point-max))
+       ,@body
+       (should (string-match-p ,value (buffer-string))))))
+
+;; Tests begin here
+
+(kmacro-tests-deftest kmacro-tests-test-insert-counter-01-nil ()
+  "Insert counter adds one with nil arg."
+  (with-temp-buffer
+    (kmacro-tests-should-insert "0"
+      (kmacro-tests-with-current-prefix-arg (kmacro-insert-counter nil)))
+    (kmacro-tests-should-insert "1"
+      (kmacro-tests-with-current-prefix-arg (kmacro-insert-counter nil)))))
+
+(kmacro-tests-deftest kmacro-tests-test-insert-counter-02-int ()
+  "Insert counter increments by value of list argument."
+  (with-temp-buffer
+    (kmacro-tests-should-insert "0"
+      (kmacro-tests-with-current-prefix-arg (kmacro-insert-counter 2)))
+    (kmacro-tests-should-insert "2"
+      (kmacro-tests-with-current-prefix-arg (kmacro-insert-counter 3)))
+    (kmacro-tests-should-insert "5"
+      (kmacro-tests-with-current-prefix-arg (kmacro-insert-counter nil)))))
+
+(kmacro-tests-deftest kmacro-tests-test-insert-counter-03-list ()
+  "Insert counter doesn't increment when given universal argument."
+  (with-temp-buffer
+    (kmacro-tests-should-insert "0"
+      (kmacro-tests-with-current-prefix-arg (kmacro-insert-counter '(16))))
+    (kmacro-tests-should-insert "0"
+      (kmacro-tests-with-current-prefix-arg (kmacro-insert-counter '(4))))))
+
+(kmacro-tests-deftest kmacro-tests-test-insert-counter-04-neg ()
+  "Insert counter decrements with '- prefix argument"
+  (ert-with-test-buffer (:name "")
+    (kmacro-tests-should-insert "0"
+      (kmacro-tests-with-current-prefix-arg (kmacro-insert-counter '-))
+      )
+    (kmacro-tests-should-insert "-1"
+      (kmacro-tests-with-current-prefix-arg (kmacro-insert-counter nil)))))
+
+(kmacro-tests-deftest kmacro-tests-test-start-format-counter ()
+  "Insert counter uses start value and format."
+  (ert-with-test-buffer (:name "start-format-counter")
+    (kmacro-tests-with-current-prefix-arg (kmacro-set-counter 10))
+    (kmacro-tests-should-insert "10"
+      (kmacro-tests-with-current-prefix-arg (kmacro-insert-counter nil)))
+    (kmacro-tests-should-insert "11"
+      (kmacro-tests-with-current-prefix-arg (kmacro-insert-counter nil)))
+    (kmacro-set-format "c=%s")
+    (kmacro-tests-with-current-prefix-arg (kmacro-set-counter 50))
+    (kmacro-tests-should-insert "c=50"
+      (kmacro-tests-with-current-prefix-arg (kmacro-insert-counter nil)))))
+
+(kmacro-tests-deftest kmacro-tests-test-start-macro-when-defining-macro ()
+  "Starting a macro while defining a macro does not start a second macro."
+  ;; Verify kmacro-start-macro calls start-kbd-macro
+  (:stub start-kbd-macro)
+  (kmacro-tests-should-call
+      ((start-kbd-macro :once :before (lambda (append noexec)
+                                        (should-not append)
+                                        (should-not noexec))))
+    (kmacro-tests-with-current-prefix-arg (kmacro-start-macro nil)))
+  ;; And leaves macro in being-recorded state
+  (should defining-kbd-macro)
+  ;; And that it doesn't call it again
+  (kmacro-tests-should-not-call start-kbd-macro
+    (kmacro-tests-with-current-prefix-arg (kmacro-start-macro nil))))
+
+
+(kmacro-tests-deftest kmacro-tests-set-macro-counter-while-defining ()
+  "Use of the prefix arg with kmacro-start sets kmacro-counter."
+  (:stub start-kbd-macro)
+  ;; Give kmacro-start-macro an argument
+  (kmacro-tests-should-call
+      ((start-kbd-macro :once :before (lambda (append noexec)
+                                        (should-not append)
+                                        (should-not noexec))))
+    (kmacro-tests-with-current-prefix-arg (kmacro-start-macro 5)))
+  (should defining-kbd-macro)
+  ;; And verify that the counter is set to that value
+  (ert-with-test-buffer (:name "counter-while-defining")
+    (kmacro-tests-should-insert "5"
+      (kmacro-tests-with-current-prefix-arg (kmacro-insert-counter nil)))
+    ;; change it while defining macro
+    (kmacro-tests-with-current-prefix-arg (kmacro-set-counter 1))
+    (kmacro-tests-should-insert "1"
+      (kmacro-tests-with-current-prefix-arg (kmacro-insert-counter nil)))
+    ;; using universal arg to to set counter should reset to starting value
+    (kmacro-tests-with-current-prefix-arg (kmacro-set-counter '(4)))
+    (kmacro-tests-should-insert "5"
+      (kmacro-tests-with-current-prefix-arg (kmacro-insert-counter nil)))))
+
+
+(kmacro-tests-deftest kmacro-tests-start-insert-counter-appends-to-macro ()
+  "Use of the universal arg appends to the previous macro."
+  (:stub start-kbd-macro end-kbd-macro)
+  ;; start recording a macro
+  (kmacro-tests-should-call ((start-kbd-macro :once))
+    (kmacro-tests-with-current-prefix-arg
+     (kmacro-start-macro-or-insert-counter nil)))
+  ;; make sure we are still recording
+  (should defining-kbd-macro)
+  ;; called again should insert the counter
+  (ert-with-test-buffer (:name "start-insert-counter-appends")
+    (kmacro-tests-should-not-call start-kbd-macro
+      (kmacro-tests-should-insert "0"
+        (kmacro-tests-with-current-prefix-arg
+         (kmacro-start-macro-or-insert-counter nil)))))
+  ;; still recording
+  (should defining-kbd-macro)
+  ;; end recording, check that repeat count is passed
+  (kmacro-tests-should-call ((end-kbd-macro
+                              :once :after
+                              (lambda (repeat loopfunc)
+                                (should (eql repeat 3))
+                                (should (functionp loopfunc))
+                                (setq last-kbd-macro
+                                      (string-to-vector "hello\C-a")))))
+    (kmacro-tests-with-current-prefix-arg (kmacro-end-or-call-macro 3)))
+  ;; now not recording
+  (should-not defining-kbd-macro)
+  ;; now append to the previous macro
+  (kmacro-tests-should-call
+      ((start-kbd-macro
+        :once :before (lambda (append noexec)
+                        (should append)
+                        (if kmacro-execute-before-append
+                            (should noexec)
+                          (should-not noexec)))))
+    (kmacro-tests-with-current-prefix-arg
+     (kmacro-start-macro-or-insert-counter '(16))))
+  (should (equal defining-kbd-macro 'append)))
+
+(kmacro-tests-deftest kmacro-tests-end-call-macro-prefix-args ()
+  "kmacro-end-call-macro changes behavior based on prefix arg."
+  (:stub call-last-kbd-macro)
+  ;; "record" two macros
+  (dotimes (i 2)
+    (kmacro-tests-mock-define-macro (make-vector (1+ i) i)))
+
+  ;; with no prefix arg, it should call the second macro
+  (kmacro-tests-should-call ((call-last-kbd-macro
+                              :once :before
+                              (lambda (count loopfunc)
+                                (should (equal last-kbd-macro [1 1])))))
+    (kmacro-tests-with-current-prefix-arg (kmacro-end-or-call-macro nil)))
+
+  ;; universal arg should call the first one
+  (kmacro-tests-should-call ((execute-kbd-macro
+                              :once :override
+                              (lambda (macro count loopfunc)
+                                (should (equal [0] macro))
+                                (should (eql 1 count))
+                                (should (functionp loopfunc)))))
+    (kmacro-tests-with-current-prefix-arg (kmacro-end-or-call-macro '(4)))))
+
+
+(kmacro-tests-deftest kmacro-tests-end-and-call-macro-tests ()
+  "Commands to end and call macro work under various conditions."
+  ;; this is currently failing because kmacro-end-call-mouse leaves an empty
+  ;; macro at the head of the ring.
+  (:expected-result :failed)
+  (:stub start-kbd-macro end-kbd-macro call-last-kbd-macro)
+  ;; test 2 very similar ways to do the same thing
+  (dolist (command '(kmacro-end-and-call-macro
+                     kmacro-end-call-mouse))
+
+    (kmacro-tests-should-call
+        ((start-kbd-macro :once)
+         (end-kbd-macro :once :after
+                        (lambda (repeat loopfunc)
+                          (setq last-kbd-macro "")))
+         (call-last-kbd-macro
+          :once :before
+          (lambda (count loopfunc)
+            (should (eql (if (eq command 'kmacro-end-and-call-macro) 2)
+                         count))))
+         ;; kmacro-end-call-mouse uses this
+         (mouse-set-point :check-args-with (lambda (_args) t)
+                          :override #'ignore))
+      (kmacro-tests-with-current-prefix-arg (kmacro-start-macro nil))
+      ;; try the command while defining a macro
+      (cl-letf ((current-prefix-arg 2))
+        (ert-simulate-command `(,command 2))))
+
+    ;; check that it stopped defining and that no macro was recorded
+    (should-not defining-kbd-macro)
+    (should-not last-kbd-macro)
+
+    ;; now try it while not defining a macro
+    (kmacro-tests-should-call ((call-last-kbd-macro
+                                :once :before
+                                (lambda (count loopfunc)
+                                  (should (eql nil count)))))
+      (cl-letf ((current-prefix-arg nil))
+        (ert-simulate-command `(,command nil))))))
+
+(kmacro-tests-deftest kmacro-tests-call-macro-hint-and-repeat ()
+  "`kmacro-call-macro' gives hint in Messages and sets up repeat keymap.
+\(Bug#11817)"
+  (:stub start-kbd-macro end-kbd-macro call-last-kbd-macro)
+  (kmacro-tests-mock-define-macro [5])
+  (cl-letf ((kmacro-call-repeat-key t)
+            (kmacro-call-repeat-with-arg t)
+            (last-input-event ?e))
+    (let (exitfunc)
+
+      (message "")  ; clear the echo area
+      (kmacro-tests-should-match-message "Type e to repeat macro"
+        (kmacro-tests-should-call
+            ((this-single-command-keys :once
+                                       :override (lambda () [?\C-x ?e]))
+             (call-last-kbd-macro :times 2
+                                  :before (lambda (count loopfunc)
+                                            (should (eql 3 count))))
+             (set-transient-map :times 2
+                                :around (lambda (func &rest args)
+                                          (push (apply func args)
+                                                exitfunc)
+                                          (car exitfunc))))
+
+          (kmacro-call-macro 3)
+          ;; check that it set up for repeat, and run the repeat
+          (funcall (lookup-key overriding-terminal-local-map "e"))))
+      ;; get rid of the transient maps made by `kmacro-call-macro'
+      (while exitfunc
+        (funcall (pop exitfunc))))))
+
+(kmacro-tests-deftest
+    kmacro-tests-run-macro-command-recorded-in-macro ()
+  "No infinite loop if `kmacro-end-and-call-macro' is recorded in the macro.
+\(Bug#15126)"
+  (ert-skip "Skipping due to Bug#24921 (an ERT bug)")
+  (:expected-result :failed)
+  (:stub start-kbd-macro end-kbd-macro :use call-last-kbd-macro)
+  (kmacro-tests-mock-define-macro (vconcat "foo" [return]
+                                           (where-is-internal
+                                            'execute-extended-command nil t)
+                                           "kmacro-end-and-call-macro"))
+  (ert-with-test-buffer (:name "Bug#15126")
+    (switch-to-buffer (current-buffer))
+    (use-local-map kmacro-tests-keymap)
+    (ert-simulate-command '(kmacro-end-and-call-macro nil))))
+
+
+(kmacro-tests-deftest kmacro-tests-test-ring-2nd-commands ()
+  "2nd macro in ring is displayed and executed normally and on repeat."
+  (:stub start-kbd-macro end-kbd-macro execute-kbd-macro)
+  (kmacro-tests-should-call ((start-kbd-macro :once)
+                             (end-kbd-macro
+                              :once :after (lambda (repeat loopfunc)
+                                             (setq last-kbd-macro [1]))))
+    ;; Record one macro, with count
+    (kmacro-tests-with-current-prefix-arg (kmacro-start-macro 1))
+    (kmacro-tests-with-current-prefix-arg (kmacro-end-macro nil)))
+
+  ;; Check that execute and display do nothing with no 2nd macro
+  (kmacro-tests-should-not-call execute-kbd-macro
+    (kmacro-tests-with-current-prefix-arg (kmacro-call-ring-2nd nil)))
+  (kmacro-tests-should-match-message "Only one keyboard macro defined"
+    (kmacro-view-ring-2nd))
+
+  ;; Record another one, with format
+  (kmacro-set-format "=%d=")
+  (kmacro-tests-mock-define-macro [2 2])
+
+  ;; Execute the first one, mocked up to insert counter.
+  ;; Should get default format.
+  (kmacro-tests-should-call
+      ((execute-kbd-macro
+        :once :after
+        (lambda (mac count loopfunc)
+          (should (equal [1] mac))
+          (should (null count))
+          (kmacro-tests-with-current-prefix-arg
+           (kmacro-insert-counter nil))
+          (kmacro-tests-with-current-prefix-arg
+           (kmacro-insert-counter '(4))))))
+    (with-temp-buffer
+      (kmacro-tests-should-insert "11"
+        (kmacro-tests-with-current-prefix-arg (kmacro-call-ring-2nd nil)))))
+
+  ;; Now display and check that the [1] becomes C-a
+  (kmacro-tests-should-match-message "C-a" (kmacro-view-ring-2nd)))
+
+
+(kmacro-tests-deftest kmacro-tests-fill-ring-and-rotate ()
+  "Macro ring can do the hokey pokey, and shake it all about."
+  (:stub call-last-kbd-macro)
+  (cl-letf ((kmacro-ring-max 4))
+    ;; Record enough macros that the first one drops off the history
+    (dotimes (n (1+ kmacro-ring-max))
+      (kmacro-tests-mock-define-macro (make-vector (1+ n) (1+ n))))
+
+    ;; Cycle the ring and check that #2 comes up
+    (kmacro-tests-should-match-message "2*C-b" (kmacro-cycle-ring-next nil))
+
+    ;; Execute the current macro and check arguments
+    (kmacro-tests-should-call ((call-last-kbd-macro
+                                :once :before
+                                (lambda (count loopfunc)
+                                  (should (zerop count)))))
+      (kmacro-call-macro 0 t))
+
+    ;; Cycle the ring the other way; #5 expected
+    (kmacro-tests-should-match-message "5*C-e" (kmacro-cycle-ring-previous nil))
+    ;; Swapping the top two should give #4
+    (kmacro-tests-should-match-message "4*C-d" (kmacro-swap-ring))
+    ;; Delete the top and expect #5
+    (kmacro-tests-should-match-message "5*C-e" (kmacro-delete-ring-head))))
+
+
+(kmacro-tests-deftest kmacro-tests-test-ring-commands-when-no-macros ()
+  "Ring commands give appropriate message when no macros exist."
+  (dolist (cmd '((kmacro-cycle-ring-next nil)
+                 (kmacro-cycle-ring-previous nil)
+                 (kmacro-swap-ring)
+                 (kmacro-delete-ring-head)
+                 (kmacro-view-ring-2nd)
+                 (kmacro-call-ring-2nd nil)
+                 (kmacro-view-macro)))
+    (kmacro-tests-should-match-message "No keyboard macro defined"
+      (apply #'funcall cmd))))
+
+(kmacro-tests-deftest kmacro-tests-repeat-on-last-key ()
+  "Kmacro commands can be run in sequence without prefix keys."
+  (:stub start-kbd-macro end-kbd-macro
+         :use call-last-kbd-macro execute-kbd-macro)
+  ;; I'm attempting to make the test work even if keys have been
+  ;; rebound, but if this is failing try emacs -Q first.
+  (let* ((prefix (where-is-internal 'kmacro-keymap nil t))
+         ;; Make a sequence of events to run
+         ;; comments are expected output of mock macros
+         ;; on the first and second run of the sequence (see below)
+         (events (mapcar #'kmacro-tests-get-kmacro-key
+                         '(kmacro-end-or-call-macro-repeat ;c / b
+                           kmacro-end-or-call-macro-repeat ;c / b
+                           kmacro-call-ring-2nd-repeat     ;b / a
+                           kmacro-cycle-ring-next
+                           kmacro-end-or-call-macro-repeat ;a / a
+                           kmacro-cycle-ring-previous
+                           kmacro-end-or-call-macro-repeat ;c / b
+                           kmacro-delete-ring-head
+                           kmacro-end-or-call-macro-repeat ;b / a
+                           )))
+         ;; What we want kmacro to see as keyboard command sequence
+         (first-event (seq-concatenate
+                       'vector
+                       prefix
+                       (vector (kmacro-tests-get-kmacro-key
+                                'kmacro-end-or-call-macro-repeat)))))
+    (cl-letf
+        ;; standardize repeat options
+        ((kmacro-repeat-no-prefix t)
+         (kmacro-call-repeat-key t)
+         (kmacro-call-repeat-with-arg nil))
+
+      ;; "Record" two macros
+      (dotimes (n 2)
+        (kmacro-tests-mock-define-macro (make-vector 1 (+ ?a n))))
+      ;; Start recording #3
+      (kmacro-tests-should-call
+          ((start-kbd-macro :once))
+        (kmacro-tests-with-current-prefix-arg (kmacro-start-macro nil)))
+      ;; Set up pending keyboard events and a fresh buffer
+      ;; kmacro-set-counter is not one of the repeating kmacro
+      ;; commands so it should end the sequence.
+      (let* ((end-key (kmacro-tests-get-kmacro-key 'kmacro-set-counter))
+             (seq1 (append events (list end-key))))
+        (kmacro-tests-should-call
+            ((end-kbd-macro :once :after
+                            (lambda (repeat loopfunc)
+                              (setq last-kbd-macro [?c])))
+             (call-last-kbd-macro :times 5)
+             (execute-kbd-macro :once)
+             (read-event :times (length seq1)
+                         :around (kmacro-tests-make-mock-read-func seq1))
+             (this-single-command-keys :once :override (lambda ()
+                                                         first-event)))
+          (ert-with-test-buffer (:name "first sequence")
+            (switch-to-buffer (current-buffer))
+            (use-local-map kmacro-tests-keymap)
+            (kmacro-tests-should-insert "ccbacb"
+              ;; end #3 and launch loop to read events
+              (kmacro-end-or-call-macro-repeat nil))
+            (switch-to-prev-buffer nil t))))
+
+      ;; kmacro-edit-macro-repeat should also stop the sequence,
+      ;; so run it again with that at the end.
+      (let* ((end-key (kmacro-tests-get-kmacro-key 'kmacro-edit-macro-repeat))
+             (seq2 (append events (list end-key))))
+        (kmacro-tests-should-call
+            ((call-last-kbd-macro :times 6)
+             (execute-kbd-macro :once)
+             (edit-kbd-macro :once :override #'ignore)
+             (read-event :times (length seq2)
+                         :around (kmacro-tests-make-mock-read-func seq2))
+             (this-single-command-keys :once :override (lambda ()
+                                                         first-event)))
+          (ert-with-test-buffer (:name "second sequence")
+            (switch-to-buffer (current-buffer))
+            (use-local-map kmacro-tests-keymap)
+            (kmacro-tests-should-insert "bbbbbaaba"
+              (kmacro-end-or-call-macro-repeat 3))
+            (switch-to-prev-buffer nil t)))))))
+
+(kmacro-tests-deftest kmacro-tests-repeat-view-and-run ()
+  "Kmacro view cycles through ring and executes macro just viewed."
+  (:use execute-kbd-macro)
+  ;; I'm attempting to make the test work even if keys have been
+  ;; rebound, but if this is failing try emacs -Q first.
+  (let* ((prefix (where-is-internal 'kmacro-keymap nil t))
+         (events (mapcar #'kmacro-tests-get-kmacro-key
+                         (append (make-list 5 'kmacro-view-macro-repeat)
+                                 '(kmacro-end-or-call-macro-repeat
+                                   kmacro-set-counter))))
+         ;; What we want kmacro to see as keyboard command sequence
+         (first-event (seq-concatenate
+                       'vector
+                       prefix
+                       (vector (kmacro-tests-get-kmacro-key
+                                'kmacro-view-macro-repeat))))
+         ;; Results of repeated view-repeats
+         (macros '("d" "c" "b" "a" "d" "c")))
+    (cl-letf ((kmacro-repeat-no-prefix t)
+              (kmacro-call-repeat-key t)
+              (kmacro-call-repeat-with-arg nil))
+      ;; "Record" some macros
+      (dotimes (n 4)
+        (kmacro-tests-mock-define-macro (make-vector 1 (+ ?a n))))
+
+      ;; Set up pending keyboard events and a fresh buffer
+      (ert-with-test-buffer (:name "view sequence")
+        (switch-to-buffer (current-buffer))
+        (use-local-map kmacro-tests-keymap)
+        ;; 6 views (the direct call plus the 5 in events) should
+        ;; cycle through the ring and get to the second-to-last
+        ;; macro defined.
+        (kmacro-tests-should-insert "c"
+          (kmacro-tests-should-call
+              ((execute-kbd-macro :once)
+               (read-event :times (length events)
+                           :around (kmacro-tests-make-mock-read-func events))
+               (message :times 6
+                        :after
+                        (lambda (&rest _args)
+                          (with-current-buffer
+                              (get-buffer-create "*Messages*")
+                            (should (equal
+                                     (car macros)
+                                     (buffer-substring (- (point-max) 2)
+                                                       (- (point-max) 1))))
+                            (setq macros (cdr macros)))))
+               (this-single-command-keys :once :override (lambda ()
+                                                           first-event)))
+
+            (ert-simulate-command '(kmacro-view-macro-repeat nil))))
+        (switch-to-prev-buffer nil t)))))
+
+(kmacro-tests-deftest kmacro-tests-bind-to-key-when-recording ()
+  "Bind to key fails when recording a macro."
+  (:stub start-kbd-macro)
+  (kmacro-tests-should-call
+      ((start-kbd-macro :once))
+    (kmacro-tests-with-current-prefix-arg (kmacro-start-macro 1)))
+  (kmacro-tests-should-not-call read-key-sequence
+    (kmacro-bind-to-key nil)))
+
+(kmacro-tests-deftest kmacro-tests-name-or-bind-to-key-when-no-macro ()
+  "Bind to key, symbol or register fails when when no macro exists."
+  (should-error (kmacro-bind-to-key nil))
+  (should-error (kmacro-name-last-macro 'kmacro-tests-symbol-for-test))
+  (should-error (kmacro-to-register)))
+
+(kmacro-tests-deftest kmacro-tests-bind-to-key-bad-key-sequence ()
+  "Bind to key fails to bind to ^G."
+  (kmacro-tests-mock-define-macro [1])
+  (kmacro-tests-should-call ((read-key-sequence :once
+                                                :override (lambda (_s)
+                                                            "\C-g")))
+    (kmacro-tests-should-not-call define-key (kmacro-bind-to-key nil))))
+
+(kmacro-tests-deftest kmacro-tests-bind-to-key-with-key-sequence-in-use ()
+  "Bind to key respects yes-or-no-p when given already bound key sequence."
+  (kmacro-tests-mock-define-macro [1])
+  (with-temp-buffer
+    (switch-to-buffer (current-buffer))
+    (let ((map (make-sparse-keymap)))
+      (define-key map "\C-hi" 'info)
+      (use-local-map map))
+
+    (kmacro-tests-should-call
+        ((read-key-sequence
+          :times 2
+          :around (kmacro-tests-make-mock-read-func (make-list 2 "\C-hi"))))
+
+      ;; Try the command with yes-or-no-p set up to say no
+      (kmacro-tests-should-call ((yes-or-no-p
+                                  :once
+                                  :override
+                                  (lambda (prompt)
+                                    (should (string-match-p "info" prompt))
+                                    (should (string-match-p "C-h i" prompt))
+                                    nil)))
+        (kmacro-tests-should-not-call define-key
+          (kmacro-bind-to-key nil)))
+
+      ;; Try it again with yes
+      (kmacro-tests-should-call ((yes-or-no-p :once
+                                              :override (lambda (_prompt) t))
+                                 (define-key  :once :override
+                                   (lambda (map keys l-form)
+                                     (should (eq map global-map))
+                                     (should (equal keys "\C-hi"))
+                                     (should (functionp l-form)))))
+        (kmacro-bind-to-key nil)))
+    (switch-to-prev-buffer nil t)))
+
+
+(kmacro-tests-deftest kmacro-tests-kmacro-bind-to-single-key ()
+  "Bind to key uses C-x C-k A when asked to bind to A."
+  (:stub start-kbd-macro end-kbd-macro execute-kbd-macro)
+  ;; record a macro with counter and format set
+  (kmacro-set-format "<%d>")
+  (kmacro-tests-should-call ((start-kbd-macro :once)
+                             (end-kbd-macro :once :after
+                                            (lambda (repeat loopfunc)
+                                              (setq last-kbd-macro [1]))))
+    (kmacro-tests-with-current-prefix-arg
+     (kmacro-start-macro-or-insert-counter 5))
+    (kmacro-tests-with-current-prefix-arg (kmacro-end-macro nil)))
+
+  ;; bind it to a key and save the lambda form it makes
+  (let ((prefix (where-is-internal 'kmacro-keymap nil t))
+        lambda-form)
+    (kmacro-tests-should-call ((read-key-sequence :once
+                                                  :override (lambda (_s) "A"))
+                               (define-key :once :override
+                                 (lambda (map seq lform)
+                                   (should (eq map global-map))
+                                   (should (equal seq (concat prefix "A")))
+                                   (setq lambda-form lform))))
+      (kmacro-bind-to-key nil))
+
+    ;; record a second macro with different counter and format
+    (kmacro-set-format "%d")
+    (kmacro-tests-mock-define-macro [2])
+
+    ;; Check the lambda form and run it and verify correct counter
+    ;; and format
+    (should (equal [1] (car (kmacro-extract-lambda lambda-form))))
+    (ert-with-test-buffer (:name "bind single key")
+      (kmacro-tests-should-insert "<5>"
+        (kmacro-tests-should-call ((execute-kbd-macro
+                                    :once :after
+                                    (lambda (mac count loopfunc)
+                                      (should (equal [1] mac))
+                                      (should-not count)
+                                      (kmacro-tests-with-current-prefix-arg
+                                       (kmacro-insert-counter nil)))))
+          (funcall lambda-form))))))
+
+(kmacro-tests-deftest kmacro-tests-name-last-macro-unable-to-bind ()
+  "Name last macro won't bind to symbol which is already bound."
+  (kmacro-tests-mock-define-macro [1])
+  ;; set up a test symbol which looks like a function
+  (setplist 'kmacro-tests-symbol-for-test nil)
+  (fset 'kmacro-tests-symbol-for-test #'ignore)
+  (should-error (kmacro-name-last-macro 'kmacro-tests-symbol-for-test))
+  ;; the empty string symbol also can't be bound
+  (should-error (kmacro-name-last-macro (make-symbol ""))))
+
+(kmacro-tests-deftest kmacro-tests-name-last-macro-bind-and-rebind ()
+  "Name last macro can rebind a symbol it binds."
+  ;; make sure our symbol is unbound
+  (when (fboundp 'kmacro-tests-symbol-for-test)
+    (fmakunbound 'kmacro-tests-symbol-for-test))
+  (setplist 'kmacro-tests-symbol-for-test nil)
+  ;; make two macros and bind them to the same symbol
+  (dotimes (i 2)
+    (kmacro-tests-mock-define-macro (make-vector (1+ i) (1+ i)))
+    (kmacro-name-last-macro 'kmacro-tests-symbol-for-test)
+    (should (fboundp 'kmacro-tests-symbol-for-test)))
+
+  ;; now run the function bound to the symbol, should be the second macro
+  (kmacro-tests-should-call ((execute-kbd-macro
+                              :once :override
+                              (lambda (mac _count _loopfunc)
+                                (should (equal mac [2 2])))))
+    (funcall #'kmacro-tests-symbol-for-test)))
+
+(kmacro-tests-deftest kmacro-tests-store-in-register ()
+  "Macro can be stored in and retrieved from a register."
+  (kmacro-tests-mock-define-macro [1])
+  ;; save and restore register 200 so we can use it for the test
+  (let ((saved-reg-contents (get-register 200)))
+    (unwind-protect
+        (progn
+          (kmacro-to-register 200)
+          ;; then make a new different macro
+          (kmacro-tests-mock-define-macro [2 2])
+          (should (equal last-kbd-macro [2 2]))
+          ;; call from the register, should get first macro
+          (kmacro-tests-should-call ((call-last-kbd-macro
+                                      :once :override
+                                      (lambda (count _loopfunc)
+                                        (should (equal count 3))
+                                        (should (equal last-kbd-macro [1])))))
+            (cl-letf ((current-prefix-arg 3))
+              (jump-to-register 200 3)))
+          ;; check-that insert-register gives us human-readable macro
+          (ert-with-test-buffer (:name "register")
+            (cl-letf ((current-prefix-arg nil))
+              (insert-register 200 nil))
+            (should (equal (buffer-string) "C-a"))))
+      (set-register 200 saved-reg-contents))))
+
+(kmacro-tests-deftest kmacro-tests-step-edit-01 ()
+  "Step-edit act, skip, insert, quit."
+  (:use call-last-kbd-macro)
+
+  ;; Stepping through macro with 'act key
+  (kmacro-tests-run-step-edit "he\C-u2lo"
+                              :events (make-list 6 'act)
+                              :result "hello"
+                              :macro-result "he\C-u2lo")
+
+  ;; Grouping similar commands using act-repeat
+  (kmacro-tests-run-step-edit "f\C-aoo\C-abar"
+                              :events (make-list 5 'act-repeat)
+                              :states '("" "f" "f" "oof" "oof")
+                              :result "baroof"
+                              :macro-result "f\C-aoo\C-abar")
+
+  ;; skipping parts of macro while editing
+  (kmacro-tests-run-step-edit "ofoofff"
+                              :events '(skip skip-keep skip-keep skip-keep
+                                             skip-rest)
+                              :result ""
+                              :macro-result "foo")
+
+  ;; quitting while step-editing leaves macro unchanged
+  (kmacro-tests-run-step-edit "bar"
+                              :events '(help insert skip help quit)
+                              :sequences '("f" "o" "o" "\C-j")
+                              :result "foo"
+                              :macro-result "bar")
+
+  ;; insert and insert-1 add to macro
+  (kmacro-tests-run-step-edit "fbazbop"
+                              :events '(insert act insert-1 act-repeat)
+                              :sequences '("o" "o" "\C-a" "\C-j" "\C-e")
+                              :result "foobazbop"
+                              :macro-result "oo\C-af\C-ebazbop"))
+
+(kmacro-tests-deftest kmacro-tests-step-edit-replace-digit-argument ()
+  "Step-edit replace can replace numeric argument in macro."
+  (:expected-result :failed)
+  (:use call-last-kbd-macro)
+  ;; Replace numeric argument in macro
+  (kmacro-tests-run-step-edit "\C-u3b\C-a\C-cxu"
+                              :events '(act replace automatic)
+                              :sequences '("8" "x" "\C-j")
+                              :result "XXXXXXXX"
+                              :macro-result "\C-u8x\C-a\C-cxu"))
+
+(kmacro-tests-deftest kmacro-tests-step-edit-replace ()
+  "Step-edit replace and replace-1"
+  (:use call-last-kbd-macro)
+  (kmacro-tests-run-step-edit "a\C-a\C-cxu"
+                              :events '(act act replace)
+                              :sequences '("b" "c" "\C-j")
+                              :result "bca"
+                              :macro-result "a\C-abc")
+  (kmacro-tests-run-step-edit "a\C-a\C-cxucd"
+                              :events '(act replace-1 automatic)
+                              :sequences '("b")
+                              :result "abcd"
+                              :macro-result "ab\C-cxucd")
+  (kmacro-tests-run-step-edit "by"
+                              :events '(act replace)
+                              :sequences '("a" "r" "\C-j")
+                              :result "bar"
+                              :macro-result "bar"))
+
+(kmacro-tests-deftest kmacro-tests-step-edit-append ()
+  "Step edit append inserts after point, and append-end inserts at end."
+  (:use call-last-kbd-macro)
+
+  (kmacro-tests-run-step-edit "f-b"
+                              :events '(append append-end)
+                              :sequences '("o" "o" "\C-j" "a" "r" "\C-j")
+                              :result "foo-bar"
+                              :macro-result "foo-bar")
+  (kmacro-tests-run-step-edit "x"
+                              :events '(append)
+                              :sequences '("\C-a" "\C-cxu" "\C-e" "y" "\C-j")
+                              :result "Xy"
+                              :macro-result "x\C-a\C-cxu\C-ey"))
+
+(kmacro-tests-deftest kmacro-tests-append-end-at-end-appends ()
+  "Append-end when already at end of macro appends to end of macro."
+  (:expected-result :failed)
+  (:use call-last-kbd-macro)
+
+  (kmacro-tests-run-step-edit "x"
+                              :events '(append-end)
+                              :sequences '("\C-a" "\C-cxu" "\C-e" "y" "\C-j")
+                              :result "Xy"
+                              :macro-result "x\C-a\C-cxu\C-ey"))
+
+
+(kmacro-tests-deftest kmacro-tests-step-edit-skip-entire ()
+  "Skipping whole macro in step-edit leaves macro unchanged."
+  (:expected-result :failed)
+  (:use call-last-kbd-macro)
+
+  (kmacro-tests-run-step-edit "xyzzy"
+                              :events '(skip-rest)
+                              :result ""
+                              :macro-result "xyzzy"))
+
+(kmacro-tests-deftest kmacro-tests-step-edit-with-quoted-insert ()
+  "Stepping through a macro that uses quoted insert leaves macro unchanged."
+  (:expected-result :failed)
+  (:use call-last-kbd-macro)
+  (cl-letf ((read-quoted-char-radix 8))
+    (kmacro-tests-run-step-edit "\C-cxq17051i there"
+                                :events '(act automatic)
+                                :result "ḩi there"
+                                :macro-result "\C-cxq17051i there")))
+
+(kmacro-tests-deftest kmacro-tests-step-edit-step-through-negative-argument ()
+  "Step edit works on macros using negative universal argument."
+  (:expected-result :failed)
+  (:use call-last-kbd-macro)
+  (kmacro-tests-run-step-edit "boo\C-u-\C-cu"
+                              :events '(act-repeat automatic )
+                              :result "BOO"
+                              :macro-result "boo\C-u-\C-cd"))
+
+(kmacro-tests-deftest kmacro-tests-step-edit-can-insert-before-quoted-insert ()
+  "Step edit can insert  before a quoted-insert in a macro."
+  (:expected-result :failed)
+  (:use call-last-kbd-macro)
+  (cl-letf ((read-quoted-char-radix 8))
+    (kmacro-tests-run-step-edit "g\C-cxq17051i"
+                                :events '(act insert-1 automatic)
+                                :sequences '("-")
+                                :result "g-ḩi"
+                                :macro-result "g-\C-cxq17051i")))
+
+(kmacro-tests-deftest kmacro-tests-step-edit-ignores-qr-map-commands ()
+  "Unimplemented commands from query-replace map are ignored."
+  (:use call-last-kbd-macro)
+  (kmacro-tests-run-step-edit "yep"
+                              :events '(edit-replacement
+                                        act-and-show act-and-exit
+                                        delete-and-edit
+                                        recenter backup
+                                        scroll-up scroll-down
+                                        scroll-other-window
+                                        scroll-other-window-down
+                                        exit-prefix
+                                        act act act)
+                              :result "yep"
+                              :macro-result "yep"))
+
+(kmacro-tests-deftest
+    kmacro-tests-step-edit-edits-macro-with-extended-command ()
+  "Step-editing a macro which uses the minibuffer can change the macro."
+  (:use call-last-kbd-macro)
+  (let ((mac (vconcat [?\M-x] "eval-expression" '[return]
+                      "(insert-char (+ ?a \C-e" [?1] "))" '[return]))
+        (mac-after (vconcat [?\M-x] "eval-expression" '[return]
+                            "(insert-char (+ ?a \C-e" [?2] "))" '[return])))
+
+    (kmacro-tests-run-step-edit mac
+                                :events '(act act-repeat
+                                              act act-repeat act
+                                              replace-1 act-repeat act)
+                                :sequences '("2")
+                                :result "c"
+                                :macro-result mac-after)))
+
+(kmacro-tests-deftest kmacro-tests-step-edit-step-through-isearch ()
+  "Step-editing can edit a macro which uses isearch-backward (Bug#22488)."
+  (:expected-result :failed)
+  (:use call-last-kbd-macro)
+  (let ((mac (vconcat "test Input" '[return]
+                      [?\C-r] "inp" '[return] "\C-cxu"))
+        (mac-after (vconcat "test input" '[return]
+                            [?\C-r] "inp" '[return] "\C-cd")))
+
+    (kmacro-tests-run-step-edit mac
+                                :events '(act-repeat act act
+                                                     act-repeat act
+                                                     replace-1)
+                                :sequences '("\C-cd")
+                                :result "test input\n"
+                                :macro-result mac-after)))
+
+(kmacro-tests-deftest kmacro-tests-step-edit-cleans-up-hook ()
+  "Step-editing properly cleans up post-command-hook. (Bug #18708)"
+  (:expected-result :failed)
+  (:use call-last-kbd-macro)
+  (let* ((mac "x")
+         (mac-after "x"))
+    (kmacro-tests-run-step-edit mac
+                                :setup (lambda ()
+                                         (setq-local post-command-hook '(t)))
+                                :teardown (lambda ()
+                                            (ert-simulate-command
+                                             '(beginning-of-line)))
+                                :events '(act)
+                                :result "x"
+                                :macro-result mac-after)))
+
+
+(cl-defun kmacro-tests-run-step-edit
+    (macro &key events sequences setup states teardown result macro-result)
+  "Implement a sort of macro to test editing other macros.
+
+Run kmacro-step-edit-macro with MACRO defined as a keyboard macro
+and mock functions attached to read-event and read-key-sequence
+which will return items from EVENTS and SEQUENCES
+respectively.  SEQUENCES may be nil, but EVENTS should not be.
+EVENTS should be a list of symbols bound in kmacro-step-edit-map
+or query-replace map, and this function will do the keymap lookup
+for you. SEQUENCES should contain return values for
+read-key-sequence.
+
+During execution an ert test buffer will be current.  SETUP, if
+provided, should be a function taking no arguments which will be
+called after the buffer for the test is created and before the
+macro runs.  STATES may be nil or a list of strings, and if it is
+the latter, before each call to read-event, the contents of the
+buffer will be expected to match the strings in the
+list. TEARDOWN, if non-nil, should be a function taking no
+arguments which will be called after the macro is run and before
+the buffer is deleted.
+
+RESULT is the string that should be inserted in (the empty test)
+buffer during the step-editing process, and MACRO-RESULT is the
+expected value of last-kbd-macro after the editing is
+complete."
+
+  (let* ((event-keys (mapcar #'kmacro-tests-get-kmacro-step-edit-key events))
+         (mock-read-event (kmacro-tests-make-mock-read-func event-keys))
+         (current-state states))
+
+    (kmacro-tests-mock-define-macro (string-to-vector macro))
+    (ert-with-test-buffer (:name "run-step-edit")
+      (when setup
+        (funcall setup))
+      (switch-to-buffer (current-buffer))
+      (use-local-map kmacro-tests-keymap)
+      (kmacro-tests-should-call
+          ((call-last-kbd-macro :once)
+           (read-event :check-args-with
+                       (lambda (arglists)
+                         ;; extra calls may come from sit-for
+                         (>= (length arglists) (length event-keys)))
+                       :around
+                       (lambda (&rest args)
+                         (when states
+                           (should (equal (car current-state) (buffer-string)))
+                           (setq current-state (cdr current-state)))
+                         (apply mock-read-event args)))
+           (read-key-sequence
+            :times (length sequences)
+            :around (kmacro-tests-make-mock-read-func sequences)))
+        (kmacro-step-edit-macro))
+      (when result
+        (should (equal result (buffer-string))))
+      (when macro-result
+        (should (equal last-kbd-macro (string-to-vector macro-result))))
+      (when teardown
+        (funcall teardown))
+      (switch-to-prev-buffer nil t))))
+
+;;; Mocks
+
+(defun kmacro-tests-mock-define-macro (mac)
+  "Mock-define a macro.
+Call into kmacro.el to start and end macro recording with the
+macros.c calls mocked to set `last-kbd-macro' to MAC.  Does its own
+mocking of `start-kbd-macro' and `end-kbd-macro' so they don't have to
+be stubbed by the calling test."
+  (kmacro-tests-should-call
+      ((start-kbd-macro :once :override
+                        (alist-get 'start-kbd-macro kmacro-tests-stubs))
+       (end-kbd-macro :once :override
+                      (lambda (&rest args)
+                        (apply
+                         (alist-get 'end-kbd-macro kmacro-tests-stubs) args)
+                        (setq last-kbd-macro mac))))
+    (kmacro-tests-with-current-prefix-arg (kmacro-start-macro nil))
+    (kmacro-tests-with-current-prefix-arg (kmacro-end-macro nil))))
+
+(defun kmacro-tests-get-kmacro-key (sym)
+  "Look up kmacro command SYM in kmacro's keymap.
+Return the integer key value found."
+  (aref (where-is-internal sym kmacro-keymap t) 0))
+
+(defun kmacro-tests-get-kmacro-step-edit-key (sym)
+  "Return the first key bound to SYM in kmacro-step-edit-map."
+  (let ((where (aref (where-is-internal sym kmacro-step-edit-map t) 0)))
+    (cond
+     ((consp where) (car where))
+     (t where))))
+
+
+(defun kmacro-tests-make-mock-read-func (events)
+  "Create around advice for `read-event' or `read-sequence'.
+If a keyboard macro is being executed, call the real function.
+Otherwise return the items in EVENTS one by one until
+they are exhausted, at which point start calling the real
+read function."
+  (lambda (orig-func &rest _args)
+    (if (or executing-kbd-macro (null events))
+        (funcall orig-func)
+      (prog1
+          (car events)
+        (setq events (cdr events))))))
+
+
+
+(provide 'kmacro-tests)
+
+;;; kmacro-tests.el ends here
-- 
2.10.1