diff --git a/CHANGELOG.md b/CHANGELOG.md index da6a540e8..f461460f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ ### Bugs fixed +- [#2183](https://github.com/bbatsov/projectile/pull/2183): `projectile-run-test-at-point` now builds a usable file path when the project is reached through a symlink (anything under `/tmp` on macOS, or a symlinked `~/src`), instead of one that climbs out of the project and gets rejected by the test runner. - [#2178](https://github.com/bbatsov/projectile/pull/2178): The file-extension filter of `projectile-replace` and friends is now shell-quoted. ag received it unquoted, so a `*.el` filter reached it as the regexp `.el$` and also matched files ending in `model` or `panel`; on the other tools an extension containing a quote broke the command outright. - [#2177](https://github.com/bbatsov/projectile/pull/2177): `projectile-ripgrep` no longer dies under zsh with `no matches found`. The `ripgrep` package runs its command line through the shell, and the ignore exclusions went in unquoted - harmless until the generated-directory defaults introduced a pattern with a `*` in it. - [#2172](https://github.com/bbatsov/projectile/pull/2172): Fix subproject detection in a Go workspace. A `go.work` names its modules as `./api`, and the leading `./` survived into the subproject name, which then matched none of the project's file paths - so `projectile-find-file-in-subproject` offered nothing at all in a Go monorepo. Members are now spelled relative to the project root whatever the manifest wrote. diff --git a/projectile.el b/projectile.el index 3641647f9..b2d3c11ed 100644 --- a/projectile.el +++ b/projectile.el @@ -13543,6 +13543,28 @@ enclosing node whose type is in RULE's `:node-types', call the rule's name)) ;;;###autoload +(defun projectile--test-at-point-file-name (file dir) + "Return FILE spelled relative to DIR for a test-at-point command. + +Prefer the name as spelled. A file under a symlinked subdirectory of +the project resolves to somewhere outside it, so running it through +`file-truename' would produce a `../..' path for a file that is really +sitting right there. + +The spelled name is the wrong one in the mirror-image case: the project +itself reached through a symlink that `projectile-compilation-dir' has +already resolved. Then the two spellings disagree about the root, the +relative name climbs out of the project, and the test runner is handed a +path it rejects - which is what every project under /tmp looks like on +macOS. Fall back to resolving both, and keep that only if it does stay +inside DIR." + (let ((spelled (file-relative-name file dir))) + (if (not (string-prefix-p ".." spelled)) + spelled + (let ((resolved (file-relative-name (file-truename file) + (file-truename dir)))) + (if (string-prefix-p ".." resolved) spelled resolved))))) + (defun projectile-run-test-at-point (arg) "Run the test around point, if any. @@ -13570,13 +13592,11 @@ a prefix ARG you can edit the command before it's run." (let ((test-name (projectile--test-at-point-name rule))) (unless test-name (user-error "No test found at point")) - ;; The command runs in the compilation directory, so the file - ;; name is made relative to it as spelled - not through - ;; `file-truename', which would escape the project for a file - ;; under a symlinked subdirectory and yield a useless `../..' path. + ;; The command runs in the compilation directory, so the file name + ;; is made relative to it. (let ((command (funcall (plist-get rule :command-fn) test-name - (file-relative-name + (projectile--test-at-point-file-name buffer-file-name (projectile-compilation-dir)))) ;; The command was derived from the test at point, so the diff --git a/test/projectile-test-at-point-test.el b/test/projectile-test-at-point-test.el index 7809756e4..e30f8b6ea 100644 --- a/test/projectile-test-at-point-test.el +++ b/test/projectile-test-at-point-test.el @@ -342,6 +342,40 @@ (expect (hash-table-count projectile-test-cmd-map) :to-equal 0) (expect (hash-table-count projectile-project-command-history) :to-equal 0)))) +(describe "projectile--test-at-point-file-name" + (it "spells the file relative to the compilation directory" + (expect (projectile--test-at-point-file-name + "/home/me/app/test/foo_test.go" "/home/me/app/") + :to-equal "test/foo_test.go")) + + (it "keeps the spelling as given when the file is under a symlinked subdirectory" + ;; `file-truename' would send this outside the project; the name as + ;; spelled is both correct and what the runner can open. + (spy-on 'file-truename :and-call-fake + (lambda (f) (if (string-prefix-p "/home/me/app/vendor/" f) + (replace-regexp-in-string + "\\`/home/me/app/vendor/" "/elsewhere/" f) + f))) + (expect (projectile--test-at-point-file-name + "/home/me/app/vendor/lib/foo_test.go" "/home/me/app/") + :to-equal "vendor/lib/foo_test.go")) + + (it "resolves both when the project root itself is reached through a symlink" + ;; `projectile-compilation-dir' resolves the root, `buffer-file-name' + ;; does not, so the spelled name climbs out of the project and the + ;; runner is handed a path it rejects. + (spy-on 'file-truename :and-call-fake + (lambda (f) (replace-regexp-in-string "\\`/tmp/" "/private/tmp/" f))) + (expect (projectile--test-at-point-file-name + "/tmp/app/cart_test.go" "/private/tmp/app/") + :to-equal "cart_test.go")) + + (it "keeps the spelled name when resolving does not help either" + (spy-on 'file-truename :and-call-fake #'identity) + (expect (projectile--test-at-point-file-name + "/somewhere/else/foo_test.go" "/home/me/app/") + :to-equal "../../../somewhere/else/foo_test.go"))) + (provide 'projectile-test-at-point-test) (describe "Ruby test-at-point"