From f674e607cc4e3520b0da3281d36d344a6b24b0a2 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sun, 3 May 2026 23:43:42 -0500 Subject: fix: scope test-runner state by project `test-runner.el` stored `cj/test-focused-files` and `cj/test-mode` in single global variables. ERT tests loaded by `cj/test-load-all` accumulated in the same global registry across projects. Switching projects inherited the previous project's focused files and mode. `cj/test-run-all` then ran every loaded ERT test from every project visited this session. I introduced a per-project state hash, `cj/test-project-states`, keyed by Projectile project root (or `default-directory` when not in a project). New helpers `cj/test--state-get` and `cj/test--state-put` route each read and write through that hash, so the focused-files list and the all/focused mode now live per project. The legacy public variables `cj/test-focused-files` and `cj/test-mode` are kept. They mirror the active project's state via `cj/test--sync-legacy-state` so existing modeline indicators and external code keep working. I also tracked which project roots had loaded tests (`cj/test-loaded-project-roots`) and added two ERT-isolation helpers. `cj/test--current-project-test-names` filters ERT's full registry to tests whose source file lives under the current project root. `cj/ert-clear-tests` deletes ERT tests loaded from other known project roots, so a fresh project starts with only its own tests. `cj/test-run-all` now uses the filtered name list, and a `projectile-after-switch-project-hook` clears foreign tests automatically when you switch projects. I added four regression tests to `tests/test-test-runner.el`: focus state isolated per project, mode isolated per project, `cj/ert-clear-tests` keeps the current project's tests and removes others, and `cj/test--current-project-test-names` returns only the current project's tests. Each test creates throwaway projects under the test temp dir and stubs `projectile-project-root` to switch contexts. 33 test-runner tests pass together. --- tests/test-test-runner.el | 123 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) (limited to 'tests') diff --git a/tests/test-test-runner.el b/tests/test-test-runner.el index 0edc0d65..0ff66f7f 100644 --- a/tests/test-test-runner.el +++ b/tests/test-test-runner.el @@ -56,6 +56,18 @@ (insert content)) filepath)) +(defun test-testrunner-create-project (name files) + "Create temp project NAME with test FILES. +FILES is an alist of relative test filenames to file contents." + (let* ((root (expand-file-name name test-testrunner--temp-dir)) + (tests-dir (expand-file-name "tests" root))) + (make-directory tests-dir t) + (dolist (file files) + (let ((path (expand-file-name (car file) tests-dir))) + (with-temp-file path + (insert (cdr file))))) + root)) + ;;; Normal Cases - Load Files (ert-deftest test-testrunner-load-files-success () @@ -355,5 +367,116 @@ (should (member "test-real" names))) (test-testrunner-teardown)) +;;; Project-Scoped State + +(ert-deftest test-testrunner-focus-state-is-project-scoped () + "Focused test files should not bleed between projects." + (test-testrunner-setup) + (let ((project-a (test-testrunner-create-project + "project-a" + '(("test-a.el" . "(ert-deftest test-project-a () t)")))) + (project-b (test-testrunner-create-project + "project-b" + '(("test-b.el" . "(ert-deftest test-project-b () t)")))) + (cj/test-project-states (make-hash-table :test #'equal))) + (cl-letf (((symbol-function 'projectile-project-root) + (lambda () project-a)) + ((symbol-function 'completing-read) + (lambda (&rest _args) "test-a.el"))) + (cj/test-focus-add) + (should (equal (cj/test--current-focused-files) '("test-a.el")))) + (cl-letf (((symbol-function 'projectile-project-root) + (lambda () project-b))) + (should (null (cj/test--current-focused-files)))) + (cl-letf (((symbol-function 'projectile-project-root) + (lambda () project-b)) + ((symbol-function 'completing-read) + (lambda (&rest _args) "test-b.el"))) + (cj/test-focus-add) + (should (equal (cj/test--current-focused-files) '("test-b.el")))) + (cl-letf (((symbol-function 'projectile-project-root) + (lambda () project-a))) + (should (equal (cj/test--current-focused-files) '("test-a.el"))))) + (test-testrunner-teardown)) + +(ert-deftest test-testrunner-mode-is-project-scoped () + "Focused/all mode should be tracked independently per project." + (test-testrunner-setup) + (let ((project-a (test-testrunner-create-project "mode-a" nil)) + (project-b (test-testrunner-create-project "mode-b" nil)) + (cj/test-project-states (make-hash-table :test #'equal))) + (cl-letf (((symbol-function 'projectile-project-root) + (lambda () project-a))) + (should (eq (cj/test--current-mode) 'all)) + (cj/test-toggle-mode) + (should (eq (cj/test--current-mode) 'focused))) + (cl-letf (((symbol-function 'projectile-project-root) + (lambda () project-b))) + (should (eq (cj/test--current-mode) 'all))) + (cl-letf (((symbol-function 'projectile-project-root) + (lambda () project-a))) + (should (eq (cj/test--current-mode) 'focused)))) + (test-testrunner-teardown)) + +(ert-deftest test-testrunner-ert-clear-tests-keeps-current-project-tests () + "Clearing ERT tests for a project switch should remove other project tests." + (test-testrunner-setup) + (let* ((project-a (test-testrunner-create-project + "ert-a" + '(("test-a.el" . "(ert-deftest test-testrunner-project-a-sentinel () t)")))) + (project-b (test-testrunner-create-project + "ert-b" + '(("test-b.el" . "(ert-deftest test-testrunner-project-b-sentinel () t)")))) + (file-a (expand-file-name "tests/test-a.el" project-a)) + (file-b (expand-file-name "tests/test-b.el" project-b))) + (unwind-protect + (progn + (cl-letf (((symbol-function 'projectile-project-root) + (lambda () project-a))) + (cj/test-load-all)) + (cl-letf (((symbol-function 'projectile-project-root) + (lambda () project-b))) + (cj/test-load-all)) + (should (ert-test-boundp 'test-testrunner-project-a-sentinel)) + (should (ert-test-boundp 'test-testrunner-project-b-sentinel)) + (cl-letf (((symbol-function 'projectile-project-root) + (lambda () project-a))) + (should (= (cj/ert-clear-tests) 1))) + (should (ert-test-boundp 'test-testrunner-project-a-sentinel)) + (should-not (ert-test-boundp 'test-testrunner-project-b-sentinel))) + (when (ert-test-boundp 'test-testrunner-project-a-sentinel) + (ert-delete-test 'test-testrunner-project-a-sentinel)) + (when (ert-test-boundp 'test-testrunner-project-b-sentinel) + (ert-delete-test 'test-testrunner-project-b-sentinel)))) + (test-testrunner-teardown)) + +(ert-deftest test-testrunner-current-project-test-names-ignore-other-projects () + "Current project ERT selection should ignore loaded tests from other projects." + (test-testrunner-setup) + (let* ((project-a (test-testrunner-create-project + "names-a" + '(("test-a.el" . "(ert-deftest test-testrunner-project-names-a () t)")))) + (project-b (test-testrunner-create-project + "names-b" + '(("test-b.el" . "(ert-deftest test-testrunner-project-names-b () t)"))))) + (unwind-protect + (progn + (cl-letf (((symbol-function 'projectile-project-root) + (lambda () project-a))) + (cj/test-load-all)) + (cl-letf (((symbol-function 'projectile-project-root) + (lambda () project-b))) + (cj/test-load-all)) + (cl-letf (((symbol-function 'projectile-project-root) + (lambda () project-a))) + (let ((names (cj/test--current-project-test-names))) + (should (member 'test-testrunner-project-names-a names)) + (should-not (member 'test-testrunner-project-names-b names))))) + (when (ert-test-boundp 'test-testrunner-project-names-a) + (ert-delete-test 'test-testrunner-project-names-a)) + (when (ert-test-boundp 'test-testrunner-project-names-b) + (ert-delete-test 'test-testrunner-project-names-b)))) + (test-testrunner-teardown)) + (provide 'test-test-runner) ;;; test-test-runner.el ends here -- cgit v1.2.3