branch: elpa/flycheck
commit 51efdbcf43e1b97a52b828be7906c1abd413d693
Author: Bozhidar Batsov <[email protected]>
Commit: Bozhidar Batsov <[email protected]>
Interrupt a running check when a new one starts
New checks used to queue behind a running one, so with slow checkers
(cargo, mypy and friends) the displayed diagnostics lagged one full
check cycle behind the buffer. Interrupt the running check instead and
start fresh, like Flymake does; the results then always reflect the
latest buffer contents. Manual checks also restart a running check now
instead of silently doing nothing.
The old queueing behavior remains available by setting the new option
flycheck-interrupt-running-checks to nil, for checkers too slow to ever
complete between edits.
---
CHANGES.rst | 15 +++
doc/user/syntax-checks.rst | 19 ++++
flycheck.el | 217 +++++++++++++++++++++++++++++++------
test/specs/test-command-checker.el | 201 ++++++++++++++++++++++++++++++++++
test/specs/test-helpers.el | 18 +++
5 files changed, 435 insertions(+), 35 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index 96a7a3a754..ae2df861e2 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -2,6 +2,21 @@
=======================
- Drop support for Emacs 27; Flycheck now requires Emacs 28.1 or newer.
+- A new syntax check now interrupts a still-running one and starts
+ immediately, instead of waiting for it to finish and displaying its
+ stale results in the meantime. This makes slow checkers (cargo, mypy
+ and friends) feel much more responsive. The new option
+ ``flycheck-interrupt-running-checks`` controls this: with the default
+ value of ``10``, only checks younger than ten seconds are interrupted,
+ so long-running checks still complete and publish their results
+ instead of being restarted forever; ``t`` always interrupts and
+ ``nil`` (also usable file- or directory-locally) restores the old
+ queueing behavior. Checks triggered on every keystroke (the
+ ``new-line`` condition) always coalesce behind a running check, and
+ checkers without an ``:interrupt`` function are never interrupted.
+ Interactive checks (``C-c ! c``) now also restart a running check
+ instead of doing nothing, without the age limit; Lisp calls to
+ ``flycheck-buffer`` keep the old do-nothing-while-running behavior.
- The error list pops up in a bottom side window by default, a quarter of
the frame tall. Customize the new option
``flycheck-error-list-display-buffer-action`` to change that;
diff --git a/doc/user/syntax-checks.rst b/doc/user/syntax-checks.rst
index f63b8f56ff..3e2b147f94 100644
--- a/doc/user/syntax-checks.rst
+++ b/doc/user/syntax-checks.rst
@@ -133,6 +133,25 @@ You can customise this behaviour with
`flycheck-check-syntax-automatically`:
(setq flycheck-check-syntax-automatically '(mode-enabled save))
+When a change-driven syntax check (``idle-change`` or ``save``) is triggered
+while a recently started check is still running, the running check is
+interrupted and the new one starts immediately, since its results would no
+longer match the buffer. Checks triggered on every keystroke (the
+``new-line`` condition) or by buffer switches coalesce behind the running
+check instead, and syntax checkers that cannot be interrupted or that have
+already made substantial progress are left to finish:
+
+.. defcustom:: flycheck-interrupt-running-checks
+
+ Whether a new syntax check interrupts a running one. With the default
+ value of ``10``, only running checks younger than ten seconds are
+ interrupted; checks that have made more progress are left to complete, and
+ the new check is deferred until they finish, so slow syntax checkers still
+ publish their results. Set to ``t`` to always interrupt, or ``nil`` to
+ never interrupt and always defer, like older Flycheck versions did.
+ Setting ``nil`` file- or directory-locally is handy for projects whose
+ syntax checkers you never want interrupted.
+
.. _flycheck-manual-checks:
Check manually
diff --git a/flycheck.el b/flycheck.el
index 4f670c826d..9cfed98509 100644
--- a/flycheck.el
+++ b/flycheck.el
@@ -364,6 +364,43 @@ to the excessive errors."
:risky t
:package-version '(flycheck . "0.22"))
+(defcustom flycheck-interrupt-running-checks 10
+ "Whether a new syntax check interrupts a running one.
+
+When a syntax check is triggered while one is already running in
+the buffer, the running check can either be interrupted, so that
+the new check starts immediately and its results reflect the
+latest buffer contents, or the new check can be deferred until
+the running one finishes, like older Flycheck versions did.
+
+If the value is a number, interrupt running checks that are
+younger than that many seconds, and defer behind older ones.
+This is the default: fast syntax checkers restart immediately on
+every change, while slow checkers (think cargo or mypy on a large
+project) are left alone once they have made real progress, so
+they complete and publish their results instead of being
+restarted forever.
+
+If t, always interrupt running checks. If nil, never interrupt,
+and always defer new checks, as older Flycheck versions did.
+
+Regardless of the value, checks triggered on every keystroke (the
+`new-line' condition) coalesce behind a running check, and
+checkers without an `:interrupt' function are never interrupted.
+Interactive checks (\\[flycheck-buffer]) are exempt from the age
+limit -- you asked for fresh results explicitly -- but nil
+disables interruption even for them.
+
+This variable is buffer-local friendly: set it to nil via file or
+directory local variables in projects whose syntax checkers you
+never want interrupted."
+ :group 'flycheck
+ :type '(choice (number :tag "Interrupt checks younger than N seconds")
+ (const :tag "Always interrupt" t)
+ (const :tag "Never interrupt; defer new checks" nil))
+ :safe (lambda (value) (or (booleanp value) (numberp value)))
+ :package-version '(flycheck . "37"))
+
(defcustom flycheck-checker-error-threshold-action 'truncate
"What to do when a checker exceeds `flycheck-checker-error-threshold'.
@@ -3039,8 +3076,12 @@ Slots:
`working-directory'
Working directory for the syntax checker. Serves as a value for
- `default-directory' for a checker."
- buffer checker context working-directory)
+ `default-directory' for a checker.
+
+`start-time'
+ The time the syntax check was started, as a float."
+ buffer checker context working-directory
+ (start-time (float-time)))
(defun flycheck-syntax-check-start (syntax-check callback)
"Start a SYNTAX-CHECK with CALLBACK."
@@ -3313,7 +3354,14 @@ Set `flycheck-current-syntax-check' accordingly."
:buffer (current-buffer)
:checker checker
:context nil
- :working-directory (flycheck-compute-working-directory checker)))
+ :working-directory (flycheck-compute-working-directory checker)
+ ;; Chained checkers continue the same logical check: keep the
+ ;; original start time, so the age limit of
+ ;; `flycheck-interrupt-running-checks' covers the whole chain
+ :start-time (if flycheck-current-syntax-check
+ (flycheck-syntax-check-start-time
+ flycheck-current-syntax-check)
+ (float-time))))
(callback (flycheck-buffer-status-callback check)))
(setq flycheck-current-syntax-check check)
(flycheck-report-status 'running)
@@ -3323,15 +3371,72 @@ Set `flycheck-current-syntax-check' accordingly."
"Determine whether a syntax check is running in the current buffer."
(not (null flycheck-current-syntax-check)))
+(defun flycheck--interrupt-current-syntax-check ()
+ "Interrupt the running syntax check, without reporting a status."
+ (when (flycheck-running-p)
+ (let ((syntax-check flycheck-current-syntax-check))
+ ;; Remove the current syntax check FIRST, to reset Flycheck into a
+ ;; non-running state, and to make
+ ;; `flycheck-report-buffer-checker-status' ignore any status reports
+ ;; from the interrupted syntax check. Interrupting below may run the
+ ;; process sentinel synchronously, so this must happen before.
+ (setq flycheck-current-syntax-check nil)
+ ;; A signaling interrupt function must not leave Flycheck in a
+ ;; half-stopped state; the interrupted check's reports are already
+ ;; ignored at this point
+ (with-demoted-errors "Error interrupting syntax check: %S"
+ (flycheck-syntax-check-interrupt syntax-check)))))
+
(defun flycheck-stop ()
"Stop any ongoing syntax check in the current buffer."
(when (flycheck-running-p)
- (flycheck-syntax-check-interrupt flycheck-current-syntax-check)
- ;; Remove the current syntax check, to reset Flycheck into a non-running
- ;; state, and to make `flycheck-report-buffer-checker-status' ignore any
- ;; status reports from the current syntax check.
- (setq flycheck-current-syntax-check nil)
- (flycheck-report-status 'interrupted)))
+ (flycheck--interrupt-current-syntax-check)
+ ;; Match the observable behavior of older Flycheck versions, where
+ ;; the interrupted process's own status report cleared the buffer
+ ;; state and ran `flycheck-syntax-check-failed-hook' -- but do it
+ ;; deterministically, instead of relying on the process sentinel
+ ;; running inline. Only after the check was interrupted: a
+ ;; signaling hook must not leave the checker process running.
+ (flycheck-report-failed-syntax-check 'interrupted)))
+
+(defun flycheck--interruptible-check-p ()
+ "Whether the running syntax check can be interrupted.
+
+Only syntax checkers with an `:interrupt' function can be
+interrupted; for others the check would keep running in the
+background and pile up with its replacement."
+ (and flycheck-current-syntax-check
+ (flycheck-checker-get
+ (flycheck-syntax-check-checker flycheck-current-syntax-check)
+ 'interrupt)
+ t))
+
+(defconst flycheck--interrupting-conditions '(idle-change save)
+ "Trigger conditions that may interrupt a running syntax check.
+
+Only conditions that imply changed buffer contents qualify: the
+running check's results are stale then, so restarting has value.
+Buffer switches and the deferred-check drain don't change the
+contents, and `new-line' fires on every keystroke; interrupting
+for these would kill and restart the checker without gaining
+anything.")
+
+(defun flycheck--may-interrupt-at-condition-p (condition)
+ "Whether checks triggered at CONDITION may interrupt a running check.
+
+Consults `flycheck-interrupt-running-checks'. Only conditions in
+`flycheck--interrupting-conditions' interrupt, and with a numeric
+option value only checks younger than that many seconds are
+interrupted, so that slow checkers eventually complete."
+ (let ((allowed flycheck-interrupt-running-checks))
+ (and allowed
+ (seq-intersection (if (listp condition) condition (list condition))
+ flycheck--interrupting-conditions)
+ (or (eq allowed t)
+ (< (- (float-time)
+ (flycheck-syntax-check-start-time
+ flycheck-current-syntax-check))
+ allowed)))))
(defun flycheck-buffer-status-callback (syntax-check)
"Create a status callback for SYNTAX-CHECK in the current buffer."
@@ -3343,28 +3448,44 @@ Set `flycheck-current-syntax-check' accordingly."
"Start checking syntax in the current buffer.
Get a syntax checker for the current buffer with
-`flycheck-get-checker-for-buffer', and start it."
+`flycheck-get-checker-for-buffer', and start it.
+
+Interactively, a running syntax check is interrupted first, per
+`flycheck-interrupt-running-checks' (without its age limit, since
+you asked for fresh results explicitly). When called from Lisp
+while a check is running, do nothing; automatic re-checks handle
+interruption in `flycheck-buffer-automatically' instead."
(interactive)
(flycheck-clean-deferred-check)
(if flycheck-mode
- (unless (flycheck-running-p)
- ;; Clear error list and mark all overlays for deletion. We do not
- ;; delete all overlays immediately to avoid excessive re-displays and
- ;; flickering, if the same errors gets highlighted again after the
check
- ;; completed.
- (run-hooks 'flycheck-before-syntax-check-hook)
- (flycheck-clear-errors)
- (setq flycheck--suppressed-error-count 0)
- (flycheck-mark-all-overlays-for-deletion)
- (condition-case err
- (let* ((checker (flycheck-get-checker-for-buffer)))
- (if checker
- (flycheck-start-current-syntax-check checker)
- (flycheck-clear)
- (flycheck-report-status 'no-checker)))
- (error
- (flycheck-report-failed-syntax-check)
- (signal (car err) (cdr err)))))
+ (progn
+ (when (and (called-interactively-p 'any)
+ (flycheck-running-p)
+ flycheck-interrupt-running-checks
+ (flycheck--interruptible-check-p)
+ ;; Don't kill a useful running check when no new one
+ ;; could start in its place; resolution errors surface
+ ;; in the check below
+ (ignore-errors (flycheck-get-checker-for-buffer)))
+ (flycheck--interrupt-current-syntax-check))
+ (unless (flycheck-running-p)
+ ;; Clear error list and mark all overlays for deletion. We do not
+ ;; delete all overlays immediately to avoid excessive re-displays
+ ;; and flickering, if the same errors gets highlighted again after
+ ;; the check completed.
+ (run-hooks 'flycheck-before-syntax-check-hook)
+ (flycheck-clear-errors)
+ (setq flycheck--suppressed-error-count 0)
+ (flycheck-mark-all-overlays-for-deletion)
+ (condition-case err
+ (let* ((checker (flycheck-get-checker-for-buffer)))
+ (if checker
+ (flycheck-start-current-syntax-check checker)
+ (flycheck-clear)
+ (flycheck-report-status 'no-checker)))
+ (error
+ (flycheck-report-failed-syntax-check)
+ (signal (car err) (cdr err))))))
(user-error "Flycheck mode disabled")))
(defun flycheck-report-buffer-checker-status
@@ -3643,17 +3764,33 @@ returns nil for CONDITION. (CONDITION may be a single
condition
or a list of them.)
The syntax check is deferred if FORCE-DEFERRED is non-nil, or if
-`flycheck-must-defer-check' returns t."
+the buffer is not visible or being reverted. When a syntax check
+is already running, it is interrupted per
+`flycheck-interrupt-running-checks'; if it is kept instead, the
+new check is deferred until it finishes."
(when (and flycheck-mode (if (listp condition)
(apply #'flycheck-may-check-automatically
condition)
(flycheck-may-check-automatically condition)))
(flycheck--clear-idle-trigger-timer)
(setq flycheck--idle-trigger-conditions nil)
- (if (or force-deferred (flycheck-must-defer-check))
+ (if (or force-deferred (flycheck--must-defer-regardless-of-running-p))
(flycheck-buffer-deferred)
- (with-demoted-errors "Error while checking syntax automatically: %S"
- (flycheck-buffer)))))
+ ;; A new check supersedes a running one: its results would be
+ ;; outdated by the time it finishes anyway. But don't kill a useful
+ ;; running check when no new one could start in its place.
+ (when (and (flycheck-running-p)
+ (flycheck--may-interrupt-at-condition-p condition)
+ (flycheck--interruptible-check-p)
+ (ignore-errors (flycheck-get-checker-for-buffer)))
+ (flycheck--interrupt-current-syntax-check))
+ ;; When the running check was kept -- it cannot be interrupted, or
+ ;; has made too much progress to throw away -- queue the new check
+ ;; behind it, like older Flycheck versions did
+ (if (flycheck-must-defer-check)
+ (flycheck-buffer-deferred)
+ (with-demoted-errors "Error while checking syntax automatically: %S"
+ (flycheck-buffer))))))
(defun flycheck--clear-idle-trigger-timer ()
"Clear the idle trigger timer."
@@ -3760,12 +3897,22 @@ A check has to be deferred if the buffer is not
visible, or if the buffer is
currently being reverted.
Return t if the check is to be deferred, or nil otherwise."
- (or (not (get-buffer-window))
+ (or (flycheck--must-defer-regardless-of-running-p)
;; We defer the syntax check if Flycheck is already running, to
;; immediately start a new syntax check after the current one finished,
;; because the result of the current check will most likely be outdated
by
- ;; the time it is finished.
- (flycheck-running-p)
+ ;; the time it is finished. `flycheck-buffer-automatically' may
+ ;; interrupt the running check instead; see
+ ;; `flycheck-interrupt-running-checks'.
+ (flycheck-running-p)))
+
+(defun flycheck--must-defer-regardless-of-running-p ()
+ "Whether a new syntax check must be deferred, running check or not.
+
+Like `flycheck-must-defer-check', but without considering a
+running syntax check, which may be interrupted to make room; the
+conditions here cannot be resolved by interruption."
+ (or (not (get-buffer-window))
;; We must defer checks while a buffer is being reverted, to avoid race
;; conditions while the buffer contents are being restored.
revert-buffer-in-progress-p))
diff --git a/test/specs/test-command-checker.el
b/test/specs/test-command-checker.el
index b201fd0969..3ace0e88ba 100644
--- a/test/specs/test-command-checker.el
+++ b/test/specs/test-command-checker.el
@@ -6,6 +6,30 @@
(require 'test-helpers)
(require 'shut-up)
+(defmacro flycheck-test--with-fake-running-check (checker properties &rest
body)
+ "Run BODY with a fake running check by CHECKER defined with PROPERTIES.
+
+Define CHECKER as a generic syntax checker with PROPERTIES (a
+quoted plist), install a fake syntax check for it as
+`flycheck-current-syntax-check', and clean everything up
+afterwards."
+ (declare (indent 2))
+ `(flycheck-buttercup-with-temp-buffer
+ (apply #'flycheck-define-generic-checker ,checker
+ "A fake checker for interruption specs." ,properties)
+ (unwind-protect
+ (progn
+ (setq flycheck-current-syntax-check
+ (flycheck-syntax-check-new
+ :buffer (current-buffer)
+ :checker ,checker
+ :context nil
+ :working-directory default-directory))
+ ,@body)
+ (setq flycheck-current-syntax-check nil)
+ (flycheck-clean-deferred-check)
+ (setf (symbol-plist ,checker) nil))))
+
(describe "Command checker"
(describe "flycheck-command-argument-p"
@@ -247,6 +271,183 @@
(expect (flycheck-error-list-mode-line-suppressed-indicator)
:to-equal "")))))
+ (it "interrupts a running check when a new one starts"
+ (assume (or (executable-find "python3") (executable-find "python")))
+ (cl-letf* ((flycheck-checker 'slow-checker)
+ ((symbol-plist 'slow-checker) flycheck-test--slow-checker))
+ (flycheck-buttercup-with-temp-buffer
+ (slow-checker-mode)
+ (insert "hello\n")
+ (flycheck-mode)
+ (unwind-protect
+ (progn
+ (flycheck-buffer)
+ (expect (flycheck-running-p) :to-be-truthy)
+ (let* ((first-check flycheck-current-syntax-check)
+ (first-process
+ (flycheck-syntax-check-context first-check)))
+ ;; Only interactive invocations interrupt
+ (funcall-interactively #'flycheck-buffer)
+ ;; The second check interrupted the first and took over,
+ ;; and the first check's process is dead
+ (expect (flycheck-running-p) :to-be-truthy)
+ (expect flycheck-current-syntax-check
+ :not :to-be first-check)
+ (expect (process-live-p first-process)
+ :not :to-be-truthy)))
+ (flycheck-stop)))))
+
+ (it "does not interrupt a running check when configured not to"
+ (assume (or (executable-find "python3") (executable-find "python")))
+ (cl-letf* ((flycheck-checker 'slow-checker)
+ ((symbol-plist 'slow-checker) flycheck-test--slow-checker)
+ (flycheck-interrupt-running-checks nil))
+ (flycheck-buttercup-with-temp-buffer
+ (slow-checker-mode)
+ (insert "hello\n")
+ (flycheck-mode)
+ (unwind-protect
+ (progn
+ (flycheck-buffer)
+ (let ((first-check flycheck-current-syntax-check))
+ ;; The running check keeps running even for an
+ ;; interactive invocation; the new check is a no-op
+ (funcall-interactively #'flycheck-buffer)
+ (expect flycheck-current-syntax-check :to-be first-check)
+ ;; Lisp calls never interrupt to begin with
+ (let ((flycheck-interrupt-running-checks t))
+ (flycheck-buffer)
+ (expect flycheck-current-syntax-check
+ :to-be first-check))))
+ (flycheck-stop)))))
+
+ (it "ignores reports from an interrupt that fires synchronously"
+ ;; `delete-process' can run the process sentinel inline, so the
+ ;; interrupted check's status report must not clear the buffer state
+ (flycheck-buttercup-with-temp-buffer
+ (let (captured-callback)
+ (flycheck-define-generic-checker 'sync-interrupt
+ "Capture the callback; report synchronously on interruption."
+ :start (lambda (_checker callback)
+ (setq captured-callback callback))
+ :interrupt (lambda (_checker _context)
+ (funcall captured-callback 'errored "interrupted"))
+ :modes '(prog-mode))
+ (unwind-protect
+ (progn
+ (setq flycheck-current-syntax-check
+ (flycheck-syntax-check-new
+ :buffer (current-buffer)
+ :checker 'sync-interrupt
+ :context nil
+ :working-directory default-directory))
+ (setq captured-callback
+ (flycheck-buffer-status-callback
+ flycheck-current-syntax-check))
+ (setq flycheck-current-errors
+ (list (flycheck-error-new-at 1 1 'error "keep me")))
+ ;; The restart path interrupts via the internal function:
+ ;; the synchronous 'errored report must be ignored, and
+ ;; the displayed errors survive for the replacement check
+ (flycheck--interrupt-current-syntax-check)
+ (expect flycheck-current-errors :to-be-truthy)
+ (expect flycheck-last-status-change :not :to-be 'errored)
+ ;; A genuine stop clears deterministically, like older
+ ;; Flycheck versions did via the inline sentinel report
+ (setq flycheck-current-syntax-check
+ (flycheck-syntax-check-new
+ :buffer (current-buffer)
+ :checker 'sync-interrupt
+ :context nil
+ :working-directory default-directory))
+ (setq captured-callback
+ (flycheck-buffer-status-callback
+ flycheck-current-syntax-check))
+ (flycheck-stop)
+ (expect flycheck-current-errors :not :to-be-truthy)
+ (expect flycheck-last-status-change :to-be 'interrupted))
+ (setf (symbol-plist 'sync-interrupt) nil)))))
+
+ (it "does not interrupt checkers without an interrupt function"
+ (flycheck-test--with-fake-running-check 'no-interrupt
+ '(:start ignore :modes (prog-mode))
+ (expect (flycheck--interruptible-check-p) :not :to-be-truthy)
+ (let ((first-check flycheck-current-syntax-check)
+ (flycheck-mode t))
+ ;; Even an interactive check must not stop it and pile up a
+ ;; second concurrent one
+ (funcall-interactively #'flycheck-buffer)
+ (expect flycheck-current-syntax-check :to-be first-check))))
+
+ (it "only interrupts at content-changing trigger conditions"
+ (flycheck-test--with-fake-running-check 'gate-checker
+ '(:start ignore :interrupt ignore :modes (prog-mode))
+ (cl-flet ((may-p (condition &optional option)
+ (let ((flycheck-interrupt-running-checks
+ (or option t)))
+ (flycheck--may-interrupt-at-condition-p condition))))
+ (expect (may-p 'idle-change) :to-be-truthy)
+ (expect (may-p 'save) :to-be-truthy)
+ (expect (may-p '(idle-buffer-switch idle-change)) :to-be-truthy)
+ ;; Per-keystroke triggers coalesce behind the running check
+ (expect (may-p 'new-line) :not :to-be-truthy)
+ ;; Buffer switches don't change the contents, so the running
+ ;; check's results are not stale
+ (expect (may-p 'idle-buffer-switch) :not :to-be-truthy)
+ ;; The deferred-check drain passes no condition
+ (expect (may-p nil) :not :to-be-truthy)
+ ;; Numeric option: young checks are interruptible...
+ (expect (may-p 'idle-change 10) :to-be-truthy)
+ ;; ...checks that made real progress are protected
+ (setf (flycheck-syntax-check-start-time
+ flycheck-current-syntax-check)
+ (- (float-time) 60))
+ (expect (may-p 'idle-change 10) :not :to-be-truthy))
+ (let ((flycheck-interrupt-running-checks nil))
+ (expect (flycheck--may-interrupt-at-condition-p 'idle-change)
+ :not :to-be-truthy))))
+
+ (it "defers the new check when the running one is kept"
+ (flycheck-test--with-fake-running-check 'keeper
+ '(:start ignore :modes (prog-mode))
+ (rename-buffer "flycheck-keeper-test")
+ (cl-letf (((symbol-function 'get-buffer-window)
+ (lambda (&rest _) (selected-window))))
+ (let ((first-check flycheck-current-syntax-check)
+ (flycheck-mode t))
+ (flycheck-buffer-automatically 'idle-change)
+ ;; No :interrupt function: the running check is kept and the
+ ;; new one queued behind it
+ (expect flycheck-current-syntax-check :to-be first-check)
+ (expect (flycheck-deferred-check-p) :to-be-truthy)))))
+
+ (it "keeps the chain's start time across chained checkers"
+ (flycheck-test--with-fake-running-check 'chain-link
+ '(:start ignore :modes (prog-mode))
+ (setf (flycheck-syntax-check-start-time flycheck-current-syntax-check)
+ 1000.0)
+ ;; Starting the next chain link inherits the start time of the
+ ;; running check, so the interruption age limit covers the whole
+ ;; chain
+ (flycheck-start-current-syntax-check 'chain-link)
+ (expect (flycheck-syntax-check-start-time
+ flycheck-current-syntax-check)
+ :to-equal 1000.0)))
+
+ (it "keeps the running check when the buffer is not visible"
+ (flycheck-test--with-fake-running-check 'invisible-checker
+ '(:start ignore :interrupt ignore :modes (prog-mode))
+ (rename-buffer "flycheck-invisible-test")
+ (cl-letf (((symbol-function 'get-buffer-window)
+ (lambda (&rest _) nil)))
+ (let ((first-check flycheck-current-syntax-check)
+ (flycheck-mode t))
+ ;; The replacement could only be deferred, so the running
+ ;; check must not be killed
+ (flycheck-buffer-automatically 'save)
+ (expect flycheck-current-syntax-check :to-be first-check)
+ (expect (flycheck-deferred-check-p) :to-be-truthy)))))
+
(it "forces English messages without overriding the character set"
;; LC_MESSAGES=C gives English output, while LC_ALL is left alone so
;; that the subprocess keeps the user's LC_CTYPE (see #2170).
diff --git a/test/specs/test-helpers.el b/test/specs/test-helpers.el
index 33123a9800..d47488908a 100644
--- a/test/specs/test-helpers.el
+++ b/test/specs/test-helpers.el
@@ -219,6 +219,24 @@ The manifest path is relative to
(setf (symbol-plist 'many-errors) nil)
+;;; Slow-checker test helpers (for check-interruption tests)
+
+(define-derived-mode slow-checker-mode prog-mode "slow")
+
+(flycheck-define-command-checker 'slow-checker
+ "Sleep for a minute before reporting an error."
+ :command `(,(or (executable-find "python3") "python")
+ "-c" "import time; time.sleep(60); print('slow:1:1:too late')")
+ :error-patterns '((error bol "slow:" line ":" column ":" (message)))
+ :modes '(slow-checker-mode))
+
+(defconst flycheck-test--slow-checker
+ (symbol-plist 'slow-checker)
+ "Saved plist for the slow-checker checker.")
+
+(setf (symbol-plist 'slow-checker) nil)
+
+
;;; Custom error level for error-level tests
(flycheck-define-error-level 'test-level