]> code.delx.au - gnu-emacs-elpa/blobdiff - test/context-coloring-test.el
Refactor js test definitions.
[gnu-emacs-elpa] / test / context-coloring-test.el
index dcad8e323bae8dd0925e38c28be41c0ff0d34b52..743aee46eeee668d11d6bf55d3419f6daa8a4553 100644 (file)
@@ -1,4 +1,4 @@
-;;; test/context-coloring-test.el --- Tests for context coloring. -*- lexical-binding: t; -*-
+;;; context-coloring-test.el --- Tests for context coloring  -*- lexical-binding: t; -*-
 
 ;; Copyright (C) 2014-2015  Free Software Foundation, Inc.
 
 ;; You should have received a copy of the GNU General Public License
 ;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+;;; Commentary:
+
+;; Tests for context coloring.
+
+;; Use with `make test'.
+
 ;;; Code:
 
+(require 'context-coloring)
 (require 'ert-async)
+(require 'js2-mode)
 
 
 ;;; Test running utilities
   "This file's directory.")
 
 (defun context-coloring-test-read-file (path)
-  "Read a file's contents into a string."
+  "Return the file's contents from PATH as a string."
   (with-temp-buffer
     (insert-file-contents (expand-file-name path context-coloring-test-path))
     (buffer-string)))
 
-(defun context-coloring-test-setup ()
-  "Preparation code to run before all tests."
-  (setq context-coloring-comments-and-strings nil))
+(defun context-coloring-test-before-all ()
+  "Prepare before all tests."
+  (setq context-coloring-syntactic-comments nil)
+  (setq context-coloring-syntactic-strings nil))
 
-(defun context-coloring-test-cleanup ()
-  "Cleanup code to run after all tests."
-  (setq context-coloring-comments-and-strings t)
-  (setq context-coloring-after-colorize-hook nil)
-  (setq context-coloring-js-block-scopes nil)
-  (context-coloring-set-colors-default))
+(defun context-coloring-test-after-all ()
+  "Cleanup after all tests."
+  (setq context-coloring-colorize-hook nil)
+  (setq context-coloring-check-scopifier-version-hook nil)
+  (setq context-coloring-maximum-face 7)
+  (setq context-coloring-original-maximum-face
+        context-coloring-maximum-face))
 
 (defmacro context-coloring-test-with-fixture (fixture &rest body)
-  "Evaluate BODY in a temporary buffer with the relative
-FIXTURE."
+  "With the relative FIXTURE, evaluate BODY in a temporary
+buffer."
   `(with-temp-buffer
      (unwind-protect
          (progn
-           (context-coloring-test-setup)
+           (context-coloring-test-before-all)
            (insert (context-coloring-test-read-file ,fixture))
            ,@body)
-       (context-coloring-test-cleanup))))
+       (context-coloring-test-after-all))))
 
 (defun context-coloring-test-with-temp-buffer-async (callback)
   "Create a temporary buffer, and evaluate CALLBACK there.  A
 teardown callback is passed to CALLBACK for it to invoke when it
 is done."
-  (let ((temp-buffer (make-symbol "temp-buffer")))
-    (let ((previous-buffer (current-buffer))
-          (temp-buffer (generate-new-buffer " *temp*")))
-      (set-buffer temp-buffer)
-      (funcall
-       callback
-       (lambda ()
-         (and (buffer-name temp-buffer)
-              (kill-buffer temp-buffer))
-         (set-buffer previous-buffer))))))
-
-(defun context-coloring-test-with-fixture-async
-    (fixture callback &optional setup)
-  "Evaluate CALLBACK in a temporary buffer with the relative
-FIXTURE.  A teardown callback is passed to CALLBACK for it to
-invoke when it is done.  An optional SETUP callback can be passed
-to run arbitrary code before the mode is invoked."
+  (let ((previous-buffer (current-buffer))
+        (temp-buffer (generate-new-buffer " *temp*")))
+    (set-buffer temp-buffer)
+    (funcall
+     callback
+     (lambda ()
+       (and (buffer-name temp-buffer)
+            (kill-buffer temp-buffer))
+       (set-buffer previous-buffer)))))
+
+(defun context-coloring-test-with-fixture-async (fixture callback)
+  "With the relative FIXTURE, evaluate CALLBACK in a temporary
+buffer.  A teardown callback is passed to CALLBACK for it to
+invoke when it is done."
   (context-coloring-test-with-temp-buffer-async
    (lambda (done-with-temp-buffer)
-     (context-coloring-test-setup)
-     (if setup (funcall setup))
+     (context-coloring-test-before-all)
      (insert (context-coloring-test-read-file fixture))
      (funcall
       callback
       (lambda ()
-        (context-coloring-test-cleanup)
+        (context-coloring-test-after-all)
         (funcall done-with-temp-buffer))))))
 
 
 ;;; Test defining utilities
 
-(defun context-coloring-test-js-mode (fixture callback &optional setup)
-  "Use FIXTURE as the subject matter for test logic in CALLBACK.
-Optionally, provide setup code to run before the mode is
-instantiated in SETUP."
-  (context-coloring-test-with-fixture-async
-   fixture
-   (lambda (done-with-test)
-     (js-mode)
-     (context-coloring-mode)
-     (context-coloring-colorize
-      (lambda ()
-        (funcall callback done-with-test))))
-   setup))
-
-(defmacro context-coloring-test-js2-mode (fixture &rest body)
-  "Use FIXTURE as the subject matter for test logic in BODY."
-  `(context-coloring-test-with-fixture
-    ,fixture
-    (require 'js2-mode)
-    (setq js2-mode-show-parse-errors nil)
-    (setq js2-mode-show-strict-warnings nil)
-    (js2-mode)
-    (context-coloring-mode)
-    ,@body))
-
-(defmacro context-coloring-test-deftest-js-mode (name)
-  "Define an asynchronous test for `js-mode' in the typical
-format."
-  (let ((test-name (intern (format "context-coloring-test-js-mode-%s" name)))
-        (fixture (format "./fixtures/%s.js" name))
-        (function-name (intern-soft
-                        (format "context-coloring-test-js-%s" name))))
-    `(ert-deftest-async ,test-name (done)
-                        (context-coloring-test-js-mode
-                         ,fixture
-                         (lambda (teardown)
-                           (unwind-protect
-                               (,function-name)
-                             (funcall teardown))
-                           (funcall done))))))
-
-(defmacro context-coloring-test-deftest-js2-mode (name)
-    "Define a test for `js2-mode' in the typical format."
-  (let ((test-name (intern (format "context-coloring-test-js2-mode-%s" name)))
-        (fixture (format "./fixtures/%s.js" name))
-        (function-name (intern-soft
-                        (format "context-coloring-test-js-%s" name))))
-    `(ert-deftest ,test-name ()
-       (context-coloring-test-js2-mode
-        ,fixture
-        (,function-name)))))
+(cl-defmacro context-coloring-test-define-deftest (name
+                                                   &key mode
+                                                   &key extension
+                                                   &key async)
+  "Define a deftest defmacro for tests prefixed with NAME. MODE
+is called to set up the test's environment.  EXTENSION denotes
+the suffix for tests' fixture files."
+  (declare (indent defun))
+  (let ((macro-name (intern (format "context-coloring-test-deftest-%s" name))))
+    `(cl-defmacro ,macro-name (name
+                               body
+                               &key fixture
+                               &key before
+                               &key after)
+       ,(format "Define a test for `%s' suffixed with NAME.
+Function BODY makes assertions.  The default fixture has a
+filename matching NAME (plus the filetype extension, \"%s\"),
+unless FIXTURE is specified to override it.  Functions BEFORE
+and AFTER run before and after the test, even if an error is
+signaled.
+
+BODY is run after `context-coloring-mode' is activated, or after
+initial colorization if colorization should occur."
+                (cadr mode) extension)
+       (declare (indent defun))
+       ;; Commas in nested backquotes are not evaluated.  Binding the mode here
+       ;; is probably the cleanest workaround.
+       (let ((mode ,mode)
+             (test-name (intern (format ,(format "%s-%%s" name) name)))
+             (fixture (cond
+                       (fixture (format "./fixtures/%s" fixture))
+                       (t (format ,(format "./fixtures/%%s.%s" extension) name)))))
+         ,@(cond
+            (async
+             `(`(ert-deftest-async ,test-name (done)
+                  (context-coloring-test-with-fixture-async
+                   ,fixture
+                   (lambda (done-with-fixture)
+                     (,mode)
+                     (when ,before (funcall ,before))
+                     (context-coloring-mode)
+                     ;; TODO: Rigid expectations, should be looser.
+                     (context-coloring-colorize
+                      (lambda ()
+                        (unwind-protect
+                            (progn
+                              (funcall ,body))
+                          (when ,after (funcall ,after))
+                          (funcall done-with-fixture))
+                        (funcall done))))))))
+            (t
+             `(`(ert-deftest ,test-name ()
+                  (context-coloring-test-with-fixture
+                   ,fixture
+                   (,mode)
+                   (when ,before (funcall ,before))
+                   (context-coloring-mode)
+                   (unwind-protect
+                       (progn
+                         (funcall ,body))
+                     (when ,after (funcall ,after))))))))))))
+
+(context-coloring-test-define-deftest js
+  :mode 'js-mode
+  :extension "js"
+  :async t)
+
+;; TODO: Do we need some way to do
+;;   (setq js2-mode-show-parse-errors nil)
+;;   (setq js2-mode-show-strict-warnings nil)
+;; ?
+(context-coloring-test-define-deftest js2
+  :mode 'js2-mode
+  :extension "js")
+
+(context-coloring-test-define-deftest emacs-lisp
+  :mode 'emacs-lisp-mode
+  :extension "el")
 
 
 ;;; Assertion functions
 
+(defun context-coloring-test-assert-position-level (position level)
+  "Assert that POSITION has LEVEL."
+  (let ((face (get-text-property position 'face))
+        actual-level)
+    (when (not (and face
+                    (let* ((face-string (symbol-name face))
+                           (matches (string-match
+                                     context-coloring-level-face-regexp
+                                     face-string)))
+                      (when matches
+                        (setq actual-level (string-to-number
+                                            (substring face-string
+                                                       (match-beginning 1)
+                                                       (match-end 1))))
+                        (= level actual-level)))))
+      (ert-fail (format (concat "Expected level at position %s, "
+                                "which is \"%s\", to be %s; "
+                                "but it was %s")
+                        position
+                        (buffer-substring-no-properties position (1+ position)) level
+                        actual-level)))))
+
+(defun context-coloring-test-assert-position-face (position face-regexp)
+  "Assert that the face at POSITION satisfies FACE-REGEXP."
+  (let ((face (get-text-property position 'face)))
+    (when (or
+           ;; Pass a non-string to do an `equal' check (against a symbol or nil).
+           (unless (stringp face-regexp)
+             (not (equal face-regexp face)))
+           ;; Otherwise do the matching.
+           (when (stringp face-regexp)
+             (not (string-match-p face-regexp (symbol-name face)))))
+      (ert-fail (format (concat "Expected face at position %s, "
+                                "which is \"%s\", to be %s; "
+                                "but it was %s")
+                        position
+                        (buffer-substring-no-properties position (1+ position)) face-regexp
+                        face)))))
+
+(defun context-coloring-test-assert-position-comment (position)
+  (context-coloring-test-assert-position-face
+   position "\\`font-lock-comment\\(-delimiter\\)?-face\\'"))
+
+(defun context-coloring-test-assert-position-constant-comment (position)
+  (context-coloring-test-assert-position-face position '(font-lock-constant-face
+                                                         font-lock-comment-face)))
+
+(defun context-coloring-test-assert-position-string (position)
+  (context-coloring-test-assert-position-face position 'font-lock-string-face))
+
+(defun context-coloring-test-assert-position-nil (position)
+  (context-coloring-test-assert-position-face position nil))
+
+(defun context-coloring-test-assert-coloring (map)
+  "Assert that the current buffer's coloring matches MAP."
+  ;; Omit the superfluous, formatting-related leading newline.  Can't use
+  ;; `save-excursion' here because if an assertion fails it will cause future
+  ;; tests to get messed up.
+  (goto-char (point-min))
+  (let* ((map (substring map 1))
+         (index 0)
+         char-string
+         char)
+    (while (< index (length map))
+      (setq char-string (substring map index (1+ index)))
+      (setq char (string-to-char char-string))
+      (cond
+       ;; Newline
+       ((= char 10)
+        (next-logical-line)
+        (beginning-of-line))
+       ;; Number
+       ((and (>= char 48)
+             (<= char 57))
+        (context-coloring-test-assert-position-level
+         (point) (string-to-number char-string))
+        (forward-char))
+       ;; ';' = Comment
+       ((= char 59)
+        (context-coloring-test-assert-position-comment (point))
+        (forward-char))
+       ;; 'c' = Constant comment
+       ((= char 99)
+        (context-coloring-test-assert-position-constant-comment (point))
+        (forward-char))
+       ;; 'n' = nil
+       ((= char 110)
+        (context-coloring-test-assert-position-nil (point))
+        (forward-char))
+       ;; 's' = String
+       ((= char 115)
+        (context-coloring-test-assert-position-string (point))
+        (forward-char))
+       (t
+        (forward-char)))
+      (setq index (1+ index)))))
+
 (defmacro context-coloring-test-assert-region (&rest body)
-  "Skeleton for asserting something about the face of points in a
-region.  Provides the free variables `i', `length', `point',
-`face' and `actual-level'."
+  "Assert something about the face of points in a region.
+Provides the free variables `i', `length', `point', `face' and
+`actual-level' to the code in BODY."
   `(let ((i 0)
          (length (- end start)))
      (while (< i length)
        (let* ((point (+ i start))
-              (face (get-text-property point 'face))
-              actual-level)
+              (face (get-text-property point 'face)))
          ,@body)
        (setq i (+ i 1)))))
 
@@ -163,23 +292,24 @@ region.  Provides the free variables `i', `length', `point',
   "Assert that all points in the range [START, END) are of level
 LEVEL."
   (context-coloring-test-assert-region
-   (when (not (when face
-                (let* ((face-string (symbol-name face))
-                       (matches (string-match
-                                 context-coloring-level-face-regexp
-                                 face-string)))
-                  (when matches
-                    (setq actual-level (string-to-number
-                                        (substring face-string
-                                                   (match-beginning 1)
-                                                   (match-end 1))))
-                    (= level actual-level)))))
-     (ert-fail (format (concat "Expected level in region [%s, %s), "
-                               "which is \"%s\", to be %s; "
-                               "but at point %s, it was %s")
-                       start end
-                       (buffer-substring-no-properties start end) level
-                       point actual-level)))))
+   (let (actual-level)
+     (when (not (when face
+                  (let* ((face-string (symbol-name face))
+                         (matches (string-match
+                                   context-coloring-level-face-regexp
+                                   face-string)))
+                    (when matches
+                      (setq actual-level (string-to-number
+                                          (substring face-string
+                                                     (match-beginning 1)
+                                                     (match-end 1))))
+                      (= level actual-level)))))
+       (ert-fail (format (concat "Expected level in region [%s, %s), "
+                                 "which is \"%s\", to be %s; "
+                                 "but at point %s, it was %s")
+                         start end
+                         (buffer-substring-no-properties start end) level
+                         point actual-level))))))
 
 (defun context-coloring-test-assert-region-face (start end expected-face)
   "Assert that all points in the range [START, END) have the face
@@ -200,19 +330,27 @@ EXPECTED-FACE."
    start end 'font-lock-comment-delimiter-face))
 
 (defun context-coloring-test-assert-region-comment (start end)
-    "Assert that all points in the range [START, END) have
+  "Assert that all points in the range [START, END) have
 `font-lock-comment-face'."
   (context-coloring-test-assert-region-face
    start end 'font-lock-comment-face))
 
 (defun context-coloring-test-assert-region-string (start end)
-    "Assert that all points in the range [START, END) have
+  "Assert that all points in the range [START, END) have
 `font-lock-string-face'."
   (context-coloring-test-assert-region-face
    start end 'font-lock-string-face))
 
+(defun context-coloring-test-get-last-message ()
+  (let ((messages (split-string
+                   (buffer-substring-no-properties
+                    (point-min)
+                    (point-max))
+                   "\n")))
+    (car (nthcdr (- (length messages) 2) messages))))
+
 (defun context-coloring-test-assert-message (expected buffer)
-  "Assert that BUFFER has message EXPECTED."
+  "Assert that message EXPECTED is at the end of BUFFER."
   (when (null (get-buffer buffer))
     (ert-fail
      (format
@@ -221,20 +359,28 @@ EXPECTED-FACE."
        "but the buffer did not have any messages.")
       buffer expected)))
   (with-current-buffer buffer
-    (let ((messages (split-string
-                     (buffer-substring-no-properties
-                      (point-min)
-                      (point-max))
-                     "\n")))
-      (let ((message (car (nthcdr (- (length messages) 2) messages))))
-        (when (not (equal message expected))
+    (let ((message (context-coloring-test-get-last-message)))
+      (when (not (equal message expected))
+        (ert-fail
+         (format
+          (concat
+           "Expected buffer `%s' to have message \"%s\", "
+           "but instead it was \"%s\"")
+          buffer expected
+          message))))))
+
+(defun context-coloring-test-assert-not-message (expected buffer)
+  "Assert that message EXPECTED is not at the end of BUFFER."
+  (when (get-buffer buffer)
+    (with-current-buffer buffer
+      (let ((message (context-coloring-test-get-last-message)))
+        (when (equal message expected)
           (ert-fail
            (format
             (concat
-             "Expected buffer `%s' to have message \"%s\", "
-             "but instead it was \"%s\"")
-            buffer expected
-            message)))))))
+             "Expected buffer `%s' not to have message \"%s\", "
+             "but it did")
+            buffer expected)))))))
 
 (defun context-coloring-test-assert-no-message (buffer)
   "Assert that BUFFER has no message."
@@ -247,12 +393,12 @@ EXPECTED-FACE."
 
 (defun context-coloring-test-kill-buffer (buffer)
   "Kill BUFFER if it exists."
-  (if (get-buffer buffer) (kill-buffer buffer)))
+  (when (get-buffer buffer) (kill-buffer buffer)))
 
 (defun context-coloring-test-assert-face (level foreground &optional negate)
   "Assert that a face for LEVEL exists and that its `:foreground'
-is FOREGROUND."
-  (let* ((face (context-coloring-face-symbol level))
+is FOREGROUND, or the inverse if NEGATE is non-nil."
+  (let* ((face (context-coloring-level-face level))
          actual-foreground)
     (when (not (or negate
                    face))
@@ -267,50 +413,231 @@ is FOREGROUND."
                                 "but it %s.")
                         level
                         (if negate "not " "") foreground
-                        (if negate "did" (format "was `%s'" actual-foreground)))))))
+                        (if negate
+                            "did" (format "was `%s'" actual-foreground)))))))
 
 (defun context-coloring-test-assert-not-face (&rest arguments)
   "Assert that LEVEL does not have a face with `:foreground'
-FOREGROUND."
+FOREGROUND.  Apply ARGUMENTS to
+`context-coloring-test-assert-face', see that function."
   (apply 'context-coloring-test-assert-face
          (append arguments '(t))))
 
+(defun context-coloring-test-assert-error (body error-message)
+  "Assert that BODY signals ERROR-MESSAGE."
+  (let ((error-signaled-p nil))
+    (condition-case err
+        (progn
+          (funcall body))
+      (error
+       (setq error-signaled-p t)
+       (when (not (string-equal (cadr err) error-message))
+         (ert-fail (format (concat "Expected the error \"%s\" to be thrown, "
+                                   "but instead it was \"%s\".")
+                           error-message
+                           (cadr err))))))
+    (when (not error-signaled-p)
+      (ert-fail "Expected an error to be thrown, but there wasn't."))))
+
+(defun context-coloring-test-assert-trimmed (result expected)
+  (when (not (string-equal result expected))
+    (ert-fail "Expected string to be trimmed, but it wasn't.")))
+
 
 ;;; The tests
 
+(ert-deftest context-coloring-test-trim ()
+  (context-coloring-test-assert-trimmed (context-coloring-trim "") "")
+  (context-coloring-test-assert-trimmed (context-coloring-trim " ") "")
+  (context-coloring-test-assert-trimmed (context-coloring-trim "a") "a")
+  (context-coloring-test-assert-trimmed (context-coloring-trim " a") "a")
+  (context-coloring-test-assert-trimmed (context-coloring-trim "a ") "a")
+  (context-coloring-test-assert-trimmed (context-coloring-trim " a ") "a"))
+
+(ert-deftest-async context-coloring-test-async-mode-startup (done)
+  (context-coloring-test-with-fixture-async
+   "./fixtures/empty"
+   (lambda (teardown)
+     (js-mode)
+     (add-hook
+      'context-coloring-colorize-hook
+      (lambda ()
+        ;; If this runs we are implicitly successful; this test only confirms
+        ;; that colorization occurs on mode startup.
+        (funcall teardown)
+        (funcall done)))
+     (context-coloring-mode))))
+
+(define-derived-mode
+  context-coloring-change-detection-mode
+  fundamental-mode
+  "Testing"
+  "Prevent `context-coloring-test-change-detection' from
+  having any unintentional side-effects on mode support.")
+
+;; Simply cannot figure out how to trigger an idle timer; would much rather test
+;; that.  But (current-idle-time) always returns nil in these tests.
+(ert-deftest-async context-coloring-test-change-detection (done)
+  (context-coloring-define-dispatch
+     'idle-change
+     :modes '(context-coloring-change-detection-mode)
+     :executable "node"
+     :command "node test/binaries/noop")
+  (context-coloring-test-with-fixture-async
+   "./fixtures/empty"
+   (lambda (teardown)
+     (context-coloring-change-detection-mode)
+     (add-hook
+      'context-coloring-colorize-hook
+      (lambda ()
+        (setq context-coloring-colorize-hook nil)
+        (add-hook
+         'context-coloring-colorize-hook
+         (lambda ()
+           (funcall teardown)
+           (funcall done)))
+        (insert " ")
+        (set-window-buffer (selected-window) (current-buffer))
+        (context-coloring-maybe-colorize (current-buffer))))
+     (context-coloring-mode))))
+
+(ert-deftest context-coloring-test-check-version ()
+  (when (not (context-coloring-check-version "2.1.3" "3.0.1"))
+    (ert-fail "Expected version 3.0.1 to satisfy 2.1.3, but it didn't."))
+  (when (context-coloring-check-version "3.0.1" "2.1.3")
+    (ert-fail "Expected version 2.1.3 not to satisfy 3.0.1, but it did.")))
+
 (ert-deftest context-coloring-test-unsupported-mode ()
   (context-coloring-test-with-fixture
-   "./fixtures/function-scopes.js"
+   "./fixtures/empty"
    (context-coloring-mode)
    (context-coloring-test-assert-message
     "Context coloring is not available for this major mode"
     "*Messages*")))
 
-(ert-deftest context-coloring-test-set-colors ()
-  ;; This test has an irreversible side-effect in that it defines faces beyond
-  ;; 7.  Faces 0 through 7 are reset to their default states, so it might not
-  ;; matter, but be aware anyway.
-  (context-coloring-set-colors
-   "#000000"
-   "#111111"
-   "#222222"
-   "#333333"
-   "#444444"
-   "#555555"
-   "#666666"
-   "#777777"
-   "#888888"
-   "#999999")
-  (context-coloring-test-assert-face 0 "#000000")
-  (context-coloring-test-assert-face 1 "#111111")
-  (context-coloring-test-assert-face 2 "#222222")
-  (context-coloring-test-assert-face 3 "#333333")
-  (context-coloring-test-assert-face 4 "#444444")
-  (context-coloring-test-assert-face 5 "#555555")
-  (context-coloring-test-assert-face 6 "#666666")
-  (context-coloring-test-assert-face 7 "#777777")
-  (context-coloring-test-assert-face 8 "#888888")
-  (context-coloring-test-assert-face 9 "#999999"))
+(ert-deftest context-coloring-test-derived-mode ()
+  (context-coloring-test-with-fixture
+   "./fixtures/empty"
+   (lisp-interaction-mode)
+   (context-coloring-mode)
+   (context-coloring-test-assert-not-message
+    "Context coloring is not available for this major mode"
+    "*Messages*")))
+
+(define-derived-mode
+  context-coloring-test-define-dispatch-error-mode
+  fundamental-mode
+  "Testing"
+  "Prevent `context-coloring-test-define-dispatch-error' from
+  having any unintentional side-effects on mode support.")
+
+(ert-deftest context-coloring-test-define-dispatch-error ()
+  (context-coloring-test-assert-error
+   (lambda ()
+     (context-coloring-define-dispatch
+      'define-dispatch-no-modes))
+   "No mode defined for dispatch")
+  (context-coloring-test-assert-error
+   (lambda ()
+     (context-coloring-define-dispatch
+      'define-dispatch-no-strategy
+      :modes '(context-coloring-test-define-dispatch-error-mode)))
+   "No colorizer, scopifier or command defined for dispatch"))
+
+(define-derived-mode
+  context-coloring-test-define-dispatch-scopifier-mode
+  fundamental-mode
+  "Testing"
+  "Prevent `context-coloring-test-define-dispatch-scopifier' from
+  having any unintentional side-effects on mode support.")
+
+(ert-deftest context-coloring-test-define-dispatch-scopifier ()
+  (context-coloring-define-dispatch
+   'define-dispatch-scopifier
+   :modes '(context-coloring-test-define-dispatch-scopifier-mode)
+   :scopifier (lambda () (vector)))
+  (with-temp-buffer
+    (context-coloring-test-define-dispatch-scopifier-mode)
+    (context-coloring-mode)
+    (context-coloring-colorize)))
+
+(define-derived-mode
+  context-coloring-test-missing-executable-mode
+  fundamental-mode
+  "Testing"
+  "Prevent `context-coloring-test-define-dispatch-scopifier' from
+  having any unintentional side-effects on mode support.")
+
+(ert-deftest context-coloring-test-missing-executable ()
+  (context-coloring-define-dispatch
+   'scopifier
+   :modes '(context-coloring-test-missing-executable-mode)
+   :command ""
+   :executable "__should_not_exist__")
+  (with-temp-buffer
+    (context-coloring-test-missing-executable-mode)
+    (context-coloring-mode)))
+
+(define-derived-mode
+  context-coloring-test-unsupported-version-mode
+  fundamental-mode
+  "Testing"
+  "Prevent `context-coloring-test-unsupported-version' from
+  having any unintentional side-effects on mode support.")
+
+(ert-deftest-async context-coloring-test-unsupported-version (done)
+  (context-coloring-define-dispatch
+   'outta-date
+   :modes '(context-coloring-test-unsupported-version-mode)
+   :executable "node"
+   :command "node test/binaries/outta-date"
+   :version "v2.1.3")
+  (context-coloring-test-with-fixture-async
+   "./fixtures/empty"
+   (lambda (teardown)
+     (context-coloring-test-unsupported-version-mode)
+     (add-hook
+      'context-coloring-check-scopifier-version-hook
+      (lambda ()
+        (unwind-protect
+            (progn
+              ;; Normally the executable would be something like "outta-date"
+              ;; rather than "node".
+              (context-coloring-test-assert-message
+               "Update to the minimum version of \"node\" (v2.1.3)"
+               "*Messages*"))
+          (funcall teardown))
+        (funcall done)))
+     (context-coloring-mode))))
+
+(define-derived-mode
+  context-coloring-test-disable-mode-mode
+  fundamental-mode
+  "Testing"
+  "Prevent `context-coloring-test-disable-mode' from having any
+  unintentional side-effects on mode support.")
+
+(ert-deftest-async context-coloring-test-disable-mode (done)
+  (let (torn-down)
+    (context-coloring-define-dispatch
+     'disable-mode
+     :modes '(context-coloring-test-disable-mode-mode)
+     :executable "node"
+     :command "node test/binaries/noop"
+     :teardown (lambda ()
+                 (setq torn-down t)))
+    (context-coloring-test-with-fixture-async
+     "./fixtures/empty"
+     (lambda (teardown)
+       (unwind-protect
+           (progn
+             (context-coloring-test-disable-mode-mode)
+             (context-coloring-mode)
+             (context-coloring-mode -1)
+             (when (not torn-down)
+               (ert-fail "Expected teardown function to have been called, but it wasn't.")))
+         (funcall teardown))
+       (funcall done)))))
 
 (defvar context-coloring-test-theme-index 0
   "Unique index for unique theme names.")
@@ -326,8 +653,8 @@ FOREGROUND."
 (defun context-coloring-test-assert-theme-originally-set-p
     (settings &optional negate)
   "Assert that `context-coloring-theme-originally-set-p' returns
-t for a theme with SETTINGS (or the inverse if NEGATE is
-non-nil)."
+t for a theme with SETTINGSor the inverse if NEGATE is
+non-nil."
   (let ((theme (context-coloring-test-get-next-theme)))
     (put theme 'theme-settings settings)
     (when (funcall (if negate 'identity 'not)
@@ -341,7 +668,9 @@ non-nil)."
 
 (defun context-coloring-test-assert-not-theme-originally-set-p (&rest arguments)
   "Assert that `context-coloring-theme-originally-set-p' does not
-return t for a theme with SETTINGS."
+return t for a theme with SETTINGS.  Apply ARGUMENTS to
+`context-coloring-test-assert-theme-originally-set-p', see that
+function."
   (apply 'context-coloring-test-assert-theme-originally-set-p
          (append arguments '(t))))
 
@@ -360,12 +689,16 @@ return t for a theme with SETTINGS."
 
 (defun context-coloring-test-assert-theme-settings-highest-level
     (settings expected-level)
+  "Assert that a theme with SETTINGS has the highest level
+EXPECTED-LEVEL."
   (let ((theme (context-coloring-test-get-next-theme)))
     (put theme 'theme-settings settings)
     (context-coloring-test-assert-theme-highest-level theme expected-level)))
 
 (defun context-coloring-test-assert-theme-highest-level
     (theme expected-level &optional negate)
+  "Assert that THEME has the highest level EXPECTED-LEVEL, or the
+inverse if NEGATE is non-nil."
   (let ((highest-level (context-coloring-theme-highest-level theme)))
     (when (funcall (if negate 'identity 'not) (eq highest-level expected-level))
       (ert-fail (format (concat "Expected theme with settings `%s' "
@@ -376,6 +709,10 @@ return t for a theme with SETTINGS."
                         (if negate "did" (format "was %s" highest-level)))))))
 
 (defun context-coloring-test-assert-theme-not-highest-level (&rest arguments)
+  "Assert that THEME's highest level is not EXPECTED-LEVEL.
+Apply ARGUMENTS to
+`context-coloring-test-assert-theme-highest-level', see that
+function."
   (apply 'context-coloring-test-assert-theme-highest-level
          (append arguments '(t))))
 
@@ -400,20 +737,26 @@ return t for a theme with SETTINGS."
   )
 
 (defmacro context-coloring-test-deftest-define-theme (name &rest body)
+  "Define a test with name NAME and an automatically-generated
+theme symbol available as a free variable `theme'.  Side-effects
+from enabling themes are reversed after BODY is executed and the
+test completes."
   (declare (indent defun))
   (let ((deftest-name (intern
                        (format "context-coloring-test-define-theme-%s" name))))
     `(ert-deftest ,deftest-name ()
        (context-coloring-test-kill-buffer "*Warnings*")
+       (context-coloring-test-before-all)
        (let ((theme (context-coloring-test-get-next-theme)))
          (unwind-protect
              (progn
                ,@body)
            ;; Always cleanup.
            (disable-theme theme)
-           (context-coloring-set-colors-default))))))
+           (context-coloring-test-after-all))))))
 
 (defun context-coloring-test-deftheme (theme)
+  "Dynamically define theme THEME."
   (eval (macroexpand `(deftheme ,theme))))
 
 (context-coloring-test-deftest-define-theme additive
@@ -429,6 +772,8 @@ return t for a theme with SETTINGS."
   (context-coloring-test-assert-face 1 "#bbbbbb"))
 
 (defun context-coloring-test-assert-defined-warning (theme)
+  "Assert that a warning about colors already being defined for
+theme THEME is signaled."
   (context-coloring-test-assert-message
    (format (concat "Warning (emacs): Context coloring colors for theme "
                    "`%s' are already defined")
@@ -485,6 +830,18 @@ return t for a theme with SETTINGS."
   (context-coloring-test-assert-face 0 "#cccccc")
   (context-coloring-test-assert-face 1 "#dddddd"))
 
+(context-coloring-test-deftest-define-theme pre-recede-delayed-application
+  (context-coloring-define-theme
+   theme
+   :recede t
+   :colors '("#aaaaaa"
+             "#bbbbbb"))
+  (context-coloring-test-deftheme theme)
+  (enable-theme theme)
+  (context-coloring-test-assert-no-message "*Warnings*")
+  (context-coloring-test-assert-face 0 "#aaaaaa")
+  (context-coloring-test-assert-face 1 "#bbbbbb"))
+
 (context-coloring-test-deftest-define-theme post-recede
   (context-coloring-test-deftheme theme)
   (custom-theme-set-faces
@@ -553,43 +910,66 @@ return t for a theme with SETTINGS."
   (context-coloring-test-assert-face 0 "#aaaaaa")
   (context-coloring-test-assert-face 1 "#bbbbbb"))
 
-(defun context-coloring-test-assert-face-count (count &optional negate)
+(defun context-coloring-test-assert-maximum-face (maximum &optional negate)
+  "Assert that `context-coloring-maximum-face' is MAXIMUM, or the
+inverse if NEGATE is non-nil."
   (when (funcall (if negate 'identity 'not)
-                 (eq context-coloring-face-count count))
-    (ert-fail (format (concat "Expected `context-coloring-face-count' "
+                 (eq context-coloring-maximum-face maximum))
+    (ert-fail (format (concat "Expected `context-coloring-maximum-face' "
                               "%sto be `%s', "
                               "but it %s.")
-                      (if negate "not " "") count
+                      (if negate "not " "") maximum
                       (if negate
                           "was"
-                        (format "was `%s'" context-coloring-face-count))))))
+                        (format "was `%s'" context-coloring-maximum-face))))))
 
-(defun context-coloring-test-assert-not-face-count (&rest arguments)
-  (apply 'context-coloring-test-assert-face-count
+(defun context-coloring-test-assert-not-maximum-face (&rest arguments)
+  "Assert that `context-coloring-maximum-face' is not MAXIMUM.
+Apply ARGUMENTS to `context-coloring-test-assert-maximum-face',
+see that function."
+  (apply 'context-coloring-test-assert-maximum-face
          (append arguments '(t))))
 
-(context-coloring-test-deftest-define-theme disable
-  (context-coloring-test-deftheme theme)
-  (context-coloring-define-theme
-   theme
-   :colors '("#aaaaaa"
-             "#bbbbbb"))
-  (let ((other-theme (context-coloring-test-get-next-theme)))
-    (context-coloring-test-deftheme other-theme)
+(context-coloring-test-deftest-define-theme disable-cascade
+  (let ((maximum-face-value 9999))
+    (setq context-coloring-maximum-face maximum-face-value)
+    (context-coloring-test-deftheme theme)
     (context-coloring-define-theme
-     other-theme
-     :colors '("#cccccc"
-               "#dddddd"))
-    (enable-theme theme)
-    (enable-theme other-theme)
-    (disable-theme other-theme)
-    (context-coloring-test-assert-face 0 "#aaaaaa")
-    (context-coloring-test-assert-face 1 "#bbbbbb")
-    (context-coloring-test-assert-face-count 2))
-  (disable-theme theme)
-  (context-coloring-test-assert-not-face 0 "#aaaaaa")
-  (context-coloring-test-assert-not-face 1 "#bbbbbb")
-  (context-coloring-test-assert-not-face-count 2))
+     theme
+     :colors '("#aaaaaa"
+               "#bbbbbb"))
+    (let ((second-theme (context-coloring-test-get-next-theme)))
+      (context-coloring-test-deftheme second-theme)
+      (context-coloring-define-theme
+       second-theme
+       :colors '("#cccccc"
+                 "#dddddd"
+                 "#eeeeee"))
+      (let ((third-theme (context-coloring-test-get-next-theme)))
+        (context-coloring-test-deftheme third-theme)
+        (context-coloring-define-theme
+         third-theme
+         :colors '("#111111"
+                   "#222222"
+                   "#333333"
+                   "#444444"))
+        (enable-theme theme)
+        (enable-theme second-theme)
+        (enable-theme third-theme)
+        (disable-theme third-theme)
+        (context-coloring-test-assert-face 0 "#cccccc")
+        (context-coloring-test-assert-face 1 "#dddddd")
+        (context-coloring-test-assert-face 2 "#eeeeee")
+        (context-coloring-test-assert-maximum-face 2))
+      (disable-theme second-theme)
+      (context-coloring-test-assert-face 0 "#aaaaaa")
+      (context-coloring-test-assert-face 1 "#bbbbbb")
+      (context-coloring-test-assert-maximum-face 1))
+    (disable-theme theme)
+    (context-coloring-test-assert-not-face 0 "#aaaaaa")
+    (context-coloring-test-assert-not-face 1 "#bbbbbb")
+    (context-coloring-test-assert-maximum-face
+     maximum-face-value)))
 
 (defun context-coloring-test-js-function-scopes ()
   (context-coloring-test-assert-region-level 1 9 0)
@@ -605,27 +985,34 @@ return t for a theme with SETTINGS."
   (context-coloring-test-assert-region-level 82 87 2)
   (context-coloring-test-assert-region-level 87 89 1))
 
-(context-coloring-test-deftest-js-mode function-scopes)
-(context-coloring-test-deftest-js2-mode function-scopes)
+(context-coloring-test-deftest-js function-scopes
+  'context-coloring-test-js-function-scopes)
+(context-coloring-test-deftest-js2 function-scopes
+  'context-coloring-test-js-function-scopes)
 
 (defun context-coloring-test-js-global ()
   (context-coloring-test-assert-region-level 20 28 1)
   (context-coloring-test-assert-region-level 28 35 0)
   (context-coloring-test-assert-region-level 35 41 1))
 
-(context-coloring-test-deftest-js-mode global)
-(context-coloring-test-deftest-js2-mode global)
+(context-coloring-test-deftest-js global
+  'context-coloring-test-js-global)
+(context-coloring-test-deftest-js2 global
+  'context-coloring-test-js-global)
 
 (defun context-coloring-test-js-block-scopes ()
-  (context-coloring-test-assert-region-level 20 64 1)
-   (setq context-coloring-js-block-scopes t)
-   (context-coloring-colorize)
-   (context-coloring-test-assert-region-level 20 27 1)
-   (context-coloring-test-assert-region-level 27 41 2)
-   (context-coloring-test-assert-region-level 41 42 1)
-   (context-coloring-test-assert-region-level 42 64 2))
+  (context-coloring-colorize)
+  (context-coloring-test-assert-region-level 20 27 1)
+  (context-coloring-test-assert-region-level 27 41 2)
+  (context-coloring-test-assert-region-level 41 42 1)
+  (context-coloring-test-assert-region-level 42 64 2))
 
-(context-coloring-test-deftest-js2-mode block-scopes)
+(context-coloring-test-deftest-js2 block-scopes
+  'context-coloring-test-js-block-scopes
+  :before (lambda ()
+            (setq context-coloring-js-block-scopes t))
+  :after (lambda ()
+           (setq context-coloring-js-block-scopes nil)))
 
 (defun context-coloring-test-js-catch ()
   (context-coloring-test-assert-region-level 20 27 1)
@@ -637,14 +1024,18 @@ return t for a theme with SETTINGS."
   (context-coloring-test-assert-region-level 102 117 3)
   (context-coloring-test-assert-region-level 117 123 2))
 
-(context-coloring-test-deftest-js-mode catch)
-(context-coloring-test-deftest-js2-mode catch)
+(context-coloring-test-deftest-js catch
+  'context-coloring-test-js-catch)
+(context-coloring-test-deftest-js2 catch
+  'context-coloring-test-js-catch)
 
 (defun context-coloring-test-js-key-names ()
   (context-coloring-test-assert-region-level 20 63 1))
 
-(context-coloring-test-deftest-js-mode key-names)
-(context-coloring-test-deftest-js2-mode key-names)
+(context-coloring-test-deftest-js key-names
+  'context-coloring-test-js-key-names)
+(context-coloring-test-deftest-js2 key-names
+  'context-coloring-test-js-key-names)
 
 (defun context-coloring-test-js-property-lookup ()
   (context-coloring-test-assert-region-level 20 26 0)
@@ -654,40 +1045,184 @@ return t for a theme with SETTINGS."
   (context-coloring-test-assert-region-level 57 63 0)
   (context-coloring-test-assert-region-level 63 74 1))
 
-(context-coloring-test-deftest-js-mode property-lookup)
-(context-coloring-test-deftest-js2-mode property-lookup)
+(context-coloring-test-deftest-js property-lookup
+  'context-coloring-test-js-property-lookup)
+(context-coloring-test-deftest-js2 property-lookup
+  'context-coloring-test-js-property-lookup)
 
 (defun context-coloring-test-js-key-values ()
   (context-coloring-test-assert-region-level 78 79 1))
 
-(context-coloring-test-deftest-js-mode key-values)
-(context-coloring-test-deftest-js2-mode key-values)
+(context-coloring-test-deftest-js key-values
+  'context-coloring-test-js-key-values)
+(context-coloring-test-deftest-js2 key-values
+  'context-coloring-test-js-key-values)
 
-(defun context-coloring-test-js-comments-and-strings ()
-  (context-coloring-test-assert-region-comment-delimiter 1 4)
-  (context-coloring-test-assert-region-comment 4 8)
+(defun context-coloring-test-js-syntactic-comments-and-strings ()
+  (context-coloring-test-assert-region-level 1 8 0)
   (context-coloring-test-assert-region-comment-delimiter 9 12)
-  (context-coloring-test-assert-region-comment 12 19)
-  (context-coloring-test-assert-region-string 20 32)
-  (context-coloring-test-assert-region-level 32 33 0))
-
-(ert-deftest-async context-coloring-test-js-mode-comments-and-strings (done)
-  (context-coloring-test-js-mode
-   "./fixtures/comments-and-strings.js"
-   (lambda (teardown)
-     (unwind-protect
-         (context-coloring-test-js-comments-and-strings)
-       (funcall teardown))
-     (funcall done))
-   (lambda ()
-     (setq context-coloring-comments-and-strings t))))
-
-(ert-deftest context-coloring-test-js2-mode-comments-and-strings ()
-  (context-coloring-test-js2-mode
-   "./fixtures/comments-and-strings.js"
-   (setq context-coloring-comments-and-strings t)
-   (context-coloring-colorize)
-   (context-coloring-test-js-comments-and-strings)))
+  (context-coloring-test-assert-region-comment 12 16)
+  (context-coloring-test-assert-region-comment-delimiter 17 20)
+  (context-coloring-test-assert-region-comment 20 27)
+  (context-coloring-test-assert-region-string 28 40)
+  (context-coloring-test-assert-region-level 40 41 0))
+
+(defun context-coloring-test-js-syntactic-comments-and-strings-setup ()
+  (setq context-coloring-syntactic-comments t)
+  (setq context-coloring-syntactic-strings t))
+
+(context-coloring-test-deftest-js syntactic-comments-and-strings
+  'context-coloring-test-js-syntactic-comments-and-strings
+  :fixture "comments-and-strings.js"
+  :before 'context-coloring-test-js-syntactic-comments-and-strings-setup)
+(context-coloring-test-deftest-js2 syntactic-comments-and-strings
+  'context-coloring-test-js-syntactic-comments-and-strings
+  :fixture "comments-and-strings.js"
+  :before 'context-coloring-test-js-syntactic-comments-and-strings-setup)
+
+(defun context-coloring-test-js-syntactic-comments ()
+  (context-coloring-test-assert-region-level 1 8 0)
+  (context-coloring-test-assert-region-comment-delimiter 9 12)
+  (context-coloring-test-assert-region-comment 12 16)
+  (context-coloring-test-assert-region-comment-delimiter 17 20)
+  (context-coloring-test-assert-region-comment 20 27)
+  (context-coloring-test-assert-region-level 28 41 0))
+
+(defun context-coloring-test-js-syntactic-comments-setup ()
+  (setq context-coloring-syntactic-comments t))
+
+(context-coloring-test-deftest-js syntactic-comments
+  'context-coloring-test-js-syntactic-comments
+  :fixture "comments-and-strings.js"
+  :before 'context-coloring-test-js-syntactic-comments-setup)
+(context-coloring-test-deftest-js2 syntactic-comments
+  'context-coloring-test-js-syntactic-comments
+  :fixture "comments-and-strings.js"
+  :before 'context-coloring-test-js-syntactic-comments-setup)
+
+(defun context-coloring-test-js-syntactic-strings ()
+  (context-coloring-test-assert-region-level 1 28 0)
+  (context-coloring-test-assert-region-string 28 40)
+  (context-coloring-test-assert-region-level 40 41 0))
+
+(defun context-coloring-test-js-syntactic-strings-setup ()
+  (setq context-coloring-syntactic-strings t))
+
+(context-coloring-test-deftest-js syntactic-strings
+  'context-coloring-test-js-syntactic-strings
+  :fixture "comments-and-strings.js"
+  :before 'context-coloring-test-js-syntactic-strings-setup)
+(context-coloring-test-deftest-js2 syntactic-strings
+  'context-coloring-test-js-syntactic-strings
+  :fixture "comments-and-strings.js"
+  :before 'context-coloring-test-js-syntactic-strings-setup)
+
+(context-coloring-test-deftest-js2 unterminated-comment
+  ;; As long as `add-text-properties' doesn't signal an error, this test passes.
+  (lambda ()))
+
+(context-coloring-test-deftest-emacs-lisp defun
+  (lambda ()
+    (context-coloring-test-assert-coloring "
+111111 000 1111 111 111111111 1111
+  11 111 111 111 000011
+
+0000 0 0 00
+
+111111 01
+111111 111")))
+
+(context-coloring-test-deftest-emacs-lisp lambda
+  (lambda ()
+    (context-coloring-test-assert-coloring "
+00000000 1111111 1111
+           11111111 11 2222222 2222
+                         222 22 12 2221 111 0 00")))
+
+(context-coloring-test-deftest-emacs-lisp quote
+  (lambda ()
+    (context-coloring-test-assert-coloring "
+(xxxxx x (x)
+  (xx (xx x 111
+      111111 1 111 111
+      111111 1 1111111111 11 111 1 111 1 00001 10000 11 00001 1 100001111")))
+
+(context-coloring-test-deftest-emacs-lisp comment
+  (lambda ()
+    ;; Just check that the comment isn't parsed syntactically.
+    (context-coloring-test-assert-coloring "
+(xxxxx x ()
+  (xx (x xxxxx-xxxx xx)   ;;;;;;;;;;
+      11 00000-0000 11))) ;;;;;;;;;;"))
+  :before (lambda ()
+            (setq context-coloring-syntactic-comments t)))
+
+(context-coloring-test-deftest-emacs-lisp string
+  (lambda ()
+    (context-coloring-test-assert-coloring "
+(xxxxx x (x)
+  (xxxxxx x x sss 1 0 sssss 0 1 sssssss11"))
+  :before (lambda ()
+            (setq context-coloring-syntactic-strings t)))
+
+(context-coloring-test-deftest-emacs-lisp ignored
+  (lambda ()
+    (context-coloring-test-assert-coloring "
+(xxxxx x ()
+  (x x 1 11 11 111 11 1 111 (1 1 1)))")))
+
+(context-coloring-test-deftest-emacs-lisp let
+  (lambda ()
+    (context-coloring-test-assert-coloring "
+1111 11
+      11 01
+      11 00001
+      11 2222 22
+               22 02
+               22 000022
+           2222 2 2 2 00002211
+  1111 1 1 1 000011")))
+
+(context-coloring-test-deftest-emacs-lisp let*
+  (lambda ()
+    (context-coloring-test-assert-coloring "
+11111 11
+       11 11
+       11 000011
+  1111 1 1 1 0 0 00001
+  22222 22
+         22 12
+         22 00002
+         22 02
+         22 222
+    2222 1 1 2 2 2 000022
+  1111 1 1 1 0 0 000011")))
+
+(defun context-coloring-test-insert-unread-space ()
+  (setq unread-command-events (cons '(t . 32)
+                                    unread-command-events)))
+
+(defun context-coloring-test-remove-faces ()
+  (remove-text-properties (point-min) (point-max) '(face nil)))
+
+(context-coloring-test-deftest-emacs-lisp iteration
+  (lambda ()
+    (let ((context-coloring-emacs-lisp-iterations-per-pause 1))
+      (context-coloring-colorize)
+      (context-coloring-test-assert-coloring "
+;; `cc' `cc'
+(xxxxx x ())")
+      (context-coloring-test-remove-faces)
+      (context-coloring-test-insert-unread-space)
+      (context-coloring-colorize)
+      ;; The first iteration will color the first part of the comment, but
+      ;; that's it.  Then it will be interrupted.
+      (context-coloring-test-assert-coloring "
+;; nnnn nnnn
+nnnnnn n nnn")))
+  :before (lambda ()
+            (setq context-coloring-syntactic-comments t)
+            (setq context-coloring-syntactic-strings t)))
 
 (provide 'context-coloring-test)