summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-03 19:44:05 -0500
committerCraig Jennings <c@cjennings.net>2026-05-03 19:44:05 -0500
commit59094e944b4a9e8c5e1a20724c2681ffe9b6155c (patch)
tree0e4732e6e138684b37e1ba8efaef3e2285997441
parentd341beea020cfad52942b854a8f42d0be2e34ae9 (diff)
downloaddotemacs-59094e944b4a9e8c5e1a20724c2681ffe9b6155c.tar.gz
dotemacs-59094e944b4a9e8c5e1a20724c2681ffe9b6155c.zip
fix: shell-quote F6 test-runner command arguments
`cj/--f6-test-runner-cmd-for` was building shell command strings with raw paths and stems via `format`. For ordinary names (`tests/test_foo.py`, `pkg/foo`) that worked fine. But a path with spaces or a stem with shell metacharacters would break or misbehave once the string hit `compile`. A Python test file under `dir with spaces/` would get tokenized as separate arguments. I added `cj/--f6-shell-quote-argument` that escapes only when the argument doesn't match `cj/--f6-shell-safe-argument-regexp` (alphanumerics, slash, dot, dash, plus a small handful of safe punctuation). Ordinary paths skip the quoter and stay readable. Risky paths route through `shell-quote-argument`. I wrapped the four interpolations in the test-runner builder: the elisp `FILE=` basename, the elisp `TEST=^test-stem-` regex, both pytest paths, and the Go `./rel-dir`. The Go branch also handles an empty rel-dir explicitly so the result stays `go test ./` instead of constructing `./` via format with an empty string. I added three boundary tests: a Python path with spaces, an elisp stem with `;`, and a Go directory with spaces. Existing tests for ordinary paths continue to pass since the safe regex covers them.
-rw-r--r--modules/dev-fkeys.el32
-rw-r--r--tests/test-dev-fkeys--f6-test-runner-cmd-for.el21
2 files changed, 48 insertions, 5 deletions
diff --git a/modules/dev-fkeys.el b/modules/dev-fkeys.el
index 8c5b388c..836b7cf6 100644
--- a/modules/dev-fkeys.el
+++ b/modules/dev-fkeys.el
@@ -320,6 +320,18 @@ languages fall back to the basename without extension."
;; ---------- F6 test-runner command builder ----------
+(defconst cj/--f6-shell-safe-argument-regexp "\\`[[:alnum:]_./=+@%:,^-]+\\'"
+ "Regexp matching shell arguments safe to interpolate unchanged.")
+
+(defun cj/--f6-shell-quote-argument (argument)
+ "Quote ARGUMENT for shell command interpolation when needed.
+Simple file paths and test regexes are returned unchanged so existing
+F6 command strings stay readable. Arguments containing whitespace or
+shell-significant characters are escaped with `shell-quote-argument'."
+ (if (string-match-p cj/--f6-shell-safe-argument-regexp argument)
+ argument
+ (shell-quote-argument argument)))
+
(defun cj/--f6-test-runner-cmd-for (language is-test-file rel-path stem rel-dir)
"Return shell command to run tests for the given primitives, or nil.
LANGUAGE is the language symbol; IS-TEST-FILE is non-nil when the file
@@ -337,14 +349,24 @@ TypeScript / JavaScript and unknown languages return nil."
(if is-test-file
;; The project Makefile prepends `tests/' to FILE, so pass the
;; basename only — passing the rel-path produces `tests/tests/...'.
- (format "make test-file FILE=%s" (file-name-nondirectory rel-path))
- (format "make test-name TEST=^test-%s-" stem)))
+ (format "make test-file FILE=%s"
+ (cj/--f6-shell-quote-argument
+ (file-name-nondirectory rel-path)))
+ (format "make test-name TEST=%s"
+ (cj/--f6-shell-quote-argument
+ (format "^test-%s-" stem)))))
('python
(if is-test-file
- (format "pytest %s" rel-path)
- (format "pytest tests/test_%s.py" stem)))
+ (format "pytest %s" (cj/--f6-shell-quote-argument rel-path))
+ (format "pytest %s"
+ (cj/--f6-shell-quote-argument
+ (format "tests/test_%s.py" stem)))))
('go
- (format "go test ./%s" rel-dir))
+ (format "go test %s"
+ (cj/--f6-shell-quote-argument
+ (if (string-empty-p rel-dir)
+ "./"
+ (format "./%s" rel-dir)))))
(_ nil)))
;; ---------- F6 current-file orchestrator ----------
diff --git a/tests/test-dev-fkeys--f6-test-runner-cmd-for.el b/tests/test-dev-fkeys--f6-test-runner-cmd-for.el
index cb242889..36f97548 100644
--- a/tests/test-dev-fkeys--f6-test-runner-cmd-for.el
+++ b/tests/test-dev-fkeys--f6-test-runner-cmd-for.el
@@ -95,6 +95,27 @@ would over-match. Pass just the basename — the Makefile re-prepends
'go nil "main.go" "main" "")
"go test ./")))
+(ert-deftest test-dev-fkeys-f6-cmd-for-python-test-file-quotes-spaces ()
+ "Boundary: a Python test file path with spaces is shell-escaped."
+ (should (string=
+ (cj/--f6-test-runner-cmd-for
+ 'python t "tests/dir with spaces/test_foo.py" "foo" "tests/dir with spaces")
+ "pytest tests/dir\\ with\\ spaces/test_foo.py")))
+
+(ert-deftest test-dev-fkeys-f6-cmd-for-elisp-source-quotes-test-regex ()
+ "Boundary: an elisp source stem with shell metacharacters is escaped."
+ (should (string=
+ (cj/--f6-test-runner-cmd-for
+ 'elisp nil "modules/foo;bar.el" "foo;bar" "modules")
+ "make test-name TEST=\\^test-foo\\;bar-")))
+
+(ert-deftest test-dev-fkeys-f6-cmd-for-go-source-quotes-spaces ()
+ "Boundary: a Go package path with spaces is shell-escaped."
+ (should (string=
+ (cj/--f6-test-runner-cmd-for
+ 'go nil "pkg/with spaces/foo.go" "foo" "pkg/with spaces")
+ "go test ./pkg/with\\ spaces")))
+
;;; Error Cases
(ert-deftest test-dev-fkeys-f6-cmd-for-typescript-returns-nil ()