From aa1247f6b4296ffbd2a9d2f939ba21a45d8f8b11 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Tue, 25 Aug 2026 11:12:15 +0300 Subject: [PATCH] Cover the rest of the commands worth driving Sixteen more functions the suite never entered, all reached through a command: find-dir, find-file-dwim, recentf, edit-dir-locals, run-command-in-root, version, previous-project-buffer, the impl/test toggle, the three remaining lifecycle commands and repeat-last-command. Writing them cost two corrections worth keeping. `recentf' gates on `(boundp 'recentf-list)', and a `let' cannot make an unbound non-special symbol bound, so the spec has to load recentf rather than bind the variable. And `repeat-last-command' reads a ring through `projectile--get-command-history' rather than any "last command" accessor, so the first version of that spec was testing a function that does not exist. Function coverage 90.0% -> 91.9%, 84 functions never entered -> 68. What is left is mostly code that spawns a terminal or a process, the Mercurial paths, and the dashboard and doctor renderers. --- test/projectile-buffers-test.el | 13 +++++ test/projectile-commands-test.el | 89 ++++++++++++++++++++++++++++++++ test/projectile-relation-test.el | 20 +++++++ test/projectile-tasks-test.el | 55 ++++++++++++++++++++ 4 files changed, 177 insertions(+) diff --git a/test/projectile-buffers-test.el b/test/projectile-buffers-test.el index f590fa91a..0b55b85fe 100644 --- a/test/projectile-buffers-test.el +++ b/test/projectile-buffers-test.el @@ -296,4 +296,17 @@ projectile-process-current-project-buffers-current to have similar behaviour" (projectile-next-project-buffer) (expect 'next-buffer :to-have-been-called))) +(describe "projectile-previous-project-buffer" + (it "walks the other way, through the same repeat-until helper" + (spy-on 'projectile--repeat-until-project-buffer) + (projectile-previous-project-buffer) + (expect 'projectile--repeat-until-project-buffer + :to-have-been-called-with #'previous-buffer)) + + (it "falls back to plain previous-buffer outside a project" + (spy-on 'projectile-project-root :and-return-value nil) + (spy-on 'previous-buffer) + (projectile-previous-project-buffer) + (expect 'previous-buffer :to-have-been-called))) + ;;; projectile-buffers-test.el ends here diff --git a/test/projectile-commands-test.el b/test/projectile-commands-test.el index 6eb34268a..f7560f6b8 100644 --- a/test/projectile-commands-test.el +++ b/test/projectile-commands-test.el @@ -1301,4 +1301,93 @@ (spy-on 'projectile-relevant-open-projects :and-return-value nil) (expect (projectile-switch-open-project) :to-throw 'user-error))) +(describe "projectile-find-dir" + (it "opens the chosen directory in dired, resolved against the root" + (projectile-test-with-project + (("src/nested/a.txt" . "x")) + (let ((root default-directory)) + (spy-on 'projectile-completing-read :and-return-value "src/") + (spy-on 'dired) + (projectile-find-dir) + (expect 'dired :to-have-been-called-with + (expand-file-name "src/" root)))))) + +(describe "projectile-find-file-dwim" + (it "opens the single file the name at point resolves to, without prompting" + (projectile-test-with-project + (("src/unique-name.txt" . "x") ("other.txt" . "y")) + (let ((root default-directory)) + (spy-on 'projectile-select-files :and-return-value '("src/unique-name.txt")) + (spy-on 'find-file) + (spy-on 'projectile-completing-read) + (projectile-find-file-dwim) + (expect 'find-file :to-have-been-called-with + (expand-file-name "src/unique-name.txt" root)) + ;; one candidate means no completion is needed + (expect 'projectile-completing-read :not :to-have-been-called)))) + + (it "prompts when the name at point matches more than one file" + (projectile-test-with-project + (("a-one.txt" . "x") ("a-two.txt" . "y")) + (spy-on 'projectile-select-files :and-return-value '("a-one.txt" "a-two.txt")) + (spy-on 'projectile-completing-read :and-return-value "a-two.txt") + (spy-on 'find-file) + (projectile-find-file-dwim) + (expect 'projectile-completing-read :to-have-been-called)))) + +(describe "projectile-recentf" + (it "opens a recent file of the project, resolved against the root" + (projectile-test-with-project + (("a.txt" . "x")) + ;; the command gates on `(boundp 'recentf-list)', and a `let' cannot + ;; make an unbound, non-special symbol bound - load recentf instead + (require 'recentf) + (let ((root default-directory)) + (spy-on 'projectile-recentf-files :and-return-value '("a.txt")) + (spy-on 'projectile-completing-read :and-return-value "a.txt") + (spy-on 'find-file) + (projectile-recentf) + (expect 'find-file :to-have-been-called-with + (expand-file-name "a.txt" root))))) + + (it "says so when recentf is not enabled" + (projectile-test-with-project + (("a.txt" . "x")) + (spy-on 'message) + (spy-on 'find-file) + ;; `recentf-list' being unbound is how the command detects it + (cl-letf (((symbol-function 'boundp) + (lambda (sym) (not (eq sym 'recentf-list))))) + (projectile-recentf)) + (expect 'find-file :not :to-have-been-called) + (expect (car (spy-calls-args-for 'message 0)) :to-match "recentf")))) + +(describe "projectile-version" + (it "returns a version string" + (expect (projectile-version) :to-be-truthy) + (expect (stringp (projectile-version)) :to-be-truthy))) + +(describe "projectile-run-command-in-root" + (it "runs the next command from the project root" + (projectile-test-with-project + (("a.txt" . "x")) + (let ((root default-directory) + (seen nil)) + (spy-on 'execute-extended-command :and-call-fake + (lambda (&rest _) (setq seen default-directory))) + (cl-letf (((symbol-function 'call-interactively) + (lambda (cmd &rest _) (funcall cmd)))) + (projectile-run-command-in-root)) + (expect (file-truename seen) :to-equal (file-truename root)))))) + +(describe "projectile-edit-dir-locals" + (it "opens the project's .dir-locals.el" + (projectile-test-with-project + ((".dir-locals.el" . "((nil . ((a . 1))))\n")) + (let ((root default-directory)) + (spy-on 'find-file) + (projectile-edit-dir-locals) + (expect 'find-file :to-have-been-called-with + (expand-file-name ".dir-locals.el" root)))))) + ;;; projectile-commands-test.el ends here diff --git a/test/projectile-relation-test.el b/test/projectile-relation-test.el index 887f67ff3..f184c9357 100644 --- a/test/projectile-relation-test.el +++ b/test/projectile-relation-test.el @@ -764,4 +764,24 @@ (projectile-update-project-type 'ext-demo :test-extension nil) (expect (projectile-test-extension 'ext-demo) :to-be nil)))) +(describe "projectile-toggle-between-implementation-and-test" + (it "opens the counterpart of the file being visited" + (spy-on 'projectile-find-implementation-or-test :and-return-value "/proj/test/a_test.rb") + (spy-on 'find-file) + (with-temp-buffer + (setq buffer-file-name "/proj/lib/a.rb") + (projectile-toggle-between-implementation-and-test) + (setq buffer-file-name nil)) + (expect 'find-file :to-have-been-called-with "/proj/test/a_test.rb")) + + (it "asks about the file being visited, not about the project root" + (spy-on 'projectile-find-implementation-or-test :and-return-value "/proj/x") + (spy-on 'find-file) + (with-temp-buffer + (setq buffer-file-name "/proj/lib/a.rb") + (projectile-toggle-between-implementation-and-test) + (setq buffer-file-name nil)) + (expect 'projectile-find-implementation-or-test + :to-have-been-called-with "/proj/lib/a.rb"))) + ;;; projectile-relation-test.el ends here diff --git a/test/projectile-tasks-test.el b/test/projectile-tasks-test.el index 4b5d65d6b..9d959ae06 100644 --- a/test/projectile-tasks-test.el +++ b/test/projectile-tasks-test.el @@ -601,4 +601,59 @@ main.o: main.c (expect (projectile-repeat-last-task nil) :to-throw 'user-error)))) (provide 'projectile-tasks-test) +;;; The lifecycle commands themselves + +(describe "the lifecycle commands" + ;; Each is a one-line wrapper over `projectile--run-lifecycle-phase', but + ;; nothing was checking that each passes its own phase - a copy-paste slip + ;; between them would have gone unnoticed. + (it "runs the phase named after the command" + (dolist (spec '((projectile-test-project . test) + (projectile-install-project . install) + (projectile-package-project . package))) + (let (seen) + (spy-on 'projectile--run-lifecycle-phase :and-call-fake + (lambda (phase &rest _) (setq seen phase))) + (funcall (car spec) nil) + (expect seen :to-equal (cdr spec))))) + + (it "passes the prefix argument through as the show-prompt flag" + (let (args) + (spy-on 'projectile--run-lifecycle-phase :and-call-fake + (lambda (&rest a) (setq args a))) + (projectile-test-project '(4)) + (expect (nth 0 args) :to-equal 'test) + (expect (nth 1 args) :to-equal '(4))))) + +(describe "projectile-repeat-last-command" + (it "re-runs the newest command in the project's history" + (projectile-test-with-project + (("a.txt" . "x")) + (let ((history (make-ring 4)) + (ran nil)) + (ring-insert history "make old") + (ring-insert history "make check") + (spy-on 'projectile--get-command-history :and-return-value history) + (spy-on 'projectile--run-project-cmd :and-call-fake + (lambda (cmd &rest _) (setq ran cmd) cmd)) + (projectile-repeat-last-command nil) + (expect ran :to-equal "make check")))) + + (it "records an edited command back into the history" + (projectile-test-with-project + (("a.txt" . "x")) + (let ((history (make-ring 4))) + (ring-insert history "make check") + (spy-on 'projectile--get-command-history :and-return-value history) + ;; the user edited the command at the prompt + (spy-on 'projectile--run-project-cmd :and-return-value "make check -j8") + (projectile-repeat-last-command '(4)) + (expect (car (ring-elements history)) :to-equal "make check -j8")))) + + (it "says so when the project has nothing to repeat" + (projectile-test-with-project + (("a.txt" . "x")) + (spy-on 'projectile--get-command-history :and-return-value (make-ring 4)) + (expect (projectile-repeat-last-command nil) :to-throw 'user-error)))) + ;;; projectile-tasks-test.el ends here