Skip to content

Commit 04a0c39

Browse files
authored
Merge pull request #66 from emacs-php/test/add-ert-suite
Add an ERT test suite and run it in CI
2 parents 4f2723c + a634fd0 commit 04a0c39

6 files changed

Lines changed: 268 additions & 4 deletions

File tree

.github/run-emacs-27-2.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,7 @@ eask install-deps
5252
echo "==> eask compile"
5353
eask compile
5454

55+
echo "==> eask test ert"
56+
eask test ert ./test/*-test.el
57+
5558
echo "==> OK"

Eask

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
(package-file "phpstan.el")
1111
(files "*.el")
1212

13-
(script "test" "echo \"Error: no test specified\" && exit 1")
13+
(script "test" "eask test ert ./test/*-test.el")
1414

1515
(source 'gnu)
1616
(source 'melpa)

Eask.27

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
(package-file "phpstan.el")
2222
(files "*.el")
2323

24-
(script "test" "echo \"Error: no test specified\" && exit 1")
24+
(script "test" "eask test ert ./test/*-test.el")
2525

2626
(source 'gnu)
2727
(source 'melpa-stable)

Makefile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@ install:
1010
compile:
1111
$(EASK) compile
1212

13-
all: clean autoloads install compile
13+
test:
14+
$(EASK) test ert ./test/*-test.el
15+
16+
all: clean autoloads install compile test
1417

1518
autoloads:
1619
$(EASK) generate autoloads
1720

1821
clean:
1922
$(EASK) clean all
2023

21-
.PHONY: all autoloads clean
24+
.PHONY: all autoloads clean compile install test

test/flycheck-phpstan-test.el

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
;;; flycheck-phpstan-test.el --- Tests for flycheck-phpstan.el -*- lexical-binding: t; -*-
2+
3+
;; Copyright (C) 2025 Friends of Emacs-PHP development
4+
5+
;; License: GPL-3.0-or-later
6+
7+
;; This program is free software; you can redistribute it and/or modify
8+
;; it under the terms of the GNU General Public License as published by
9+
;; the Free Software Foundation, either version 3 of the License, or
10+
;; (at your option) any later version.
11+
12+
;; This program is distributed in the hope that it will be useful,
13+
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
;; GNU General Public License for more details.
16+
17+
;; You should have received a copy of the GNU General Public License
18+
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
20+
;;; Commentary:
21+
22+
;; ERT tests for `flycheck-phpstan-parse-output', covering the output shapes a
23+
;; container runtime produces (JSON preceded by progress on STDERR) and the
24+
;; no-JSON fallback.
25+
26+
;;; Code:
27+
(require 'ert)
28+
(require 'flycheck-phpstan)
29+
30+
(defconst flycheck-phpstan-test--json
31+
(concat "{\"totals\":{\"errors\":0,\"file_errors\":1},"
32+
"\"files\":{\"/app/test.php\":{\"errors\":1,\"messages\":["
33+
"{\"message\":\"Function f not found.\",\"line\":4,\"ignorable\":true}"
34+
"]}},\"errors\":[]}")
35+
"A minimal PHPStan JSON report with one error on line 4.")
36+
37+
(ert-deftest flycheck-phpstan-test-parse-json ()
38+
"A plain JSON report yields the error it describes."
39+
(let* ((phpstan-disable-buffer-errors t)
40+
(errors (flycheck-phpstan-parse-output flycheck-phpstan-test--json)))
41+
(should (= 1 (length errors)))
42+
(should (= 4 (flycheck-error-line (car errors))))
43+
(should (string-match-p "Function f not found"
44+
(flycheck-error-message (car errors))))))
45+
46+
(ert-deftest flycheck-phpstan-test-parse-json-with-stderr-prefix ()
47+
"The report must be found even when a runtime prefixes it with progress.
48+
Apple container writes progress to STDERR, which the checker merges into
49+
STDOUT, so the JSON does not start at the beginning of the output."
50+
(let* ((phpstan-disable-buffer-errors t)
51+
(output (concat "[0/6] Fetching image\n"
52+
"[6/6] Starting container\n"
53+
flycheck-phpstan-test--json))
54+
(errors (flycheck-phpstan-parse-output output)))
55+
(should (= 1 (length errors)))
56+
(should (= 4 (flycheck-error-line (car errors))))))
57+
58+
(ert-deftest flycheck-phpstan-test-parse-non-json-is-surfaced ()
59+
"When there is no JSON report, the raw output is surfaced, not discarded.
60+
A swallowed fallback would show a failing PHPStan as a clean buffer."
61+
(let* ((phpstan-disable-buffer-errors t)
62+
(output "Bootstrap file /app/tests/bootstrap.php does not exist.")
63+
(errors (flycheck-phpstan-parse-output output)))
64+
(should (= 1 (length errors)))
65+
(should (eq 'warning (flycheck-error-level (car errors))))
66+
(should (string-match-p "Bootstrap file"
67+
(flycheck-error-message (car errors))))))
68+
69+
(provide 'flycheck-phpstan-test)
70+
;;; flycheck-phpstan-test.el ends here

test/phpstan-test.el

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
;;; phpstan-test.el --- Tests for phpstan.el -*- lexical-binding: t; -*-
2+
3+
;; Copyright (C) 2025 Friends of Emacs-PHP development
4+
5+
;; License: GPL-3.0-or-later
6+
7+
;; This program is free software; you can redistribute it and/or modify
8+
;; it under the terms of the GNU General Public License as published by
9+
;; the Free Software Foundation, either version 3 of the License, or
10+
;; (at your option) any later version.
11+
12+
;; This program is distributed in the hope that it will be useful,
13+
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
;; GNU General Public License for more details.
16+
17+
;; You should have received a copy of the GNU General Public License
18+
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
20+
;;; Commentary:
21+
22+
;; ERT tests for the parts of phpstan.el that can be exercised without a
23+
;; running PHPStan. Anything that would touch the project or the filesystem
24+
;; is stubbed, so the tests stay hermetic.
25+
26+
;;; Code:
27+
(require 'ert)
28+
(require 'cl-lib)
29+
(require 'phpstan)
30+
31+
;;; Utilities
32+
33+
(ert-deftest phpstan-test-plist-to-alist ()
34+
(should (equal '(("a" . 1) ("b" . 2))
35+
(phpstan--plist-to-alist '(:a 1 :b 2))))
36+
(should (equal nil (phpstan--plist-to-alist nil))))
37+
38+
;;; Container runtime detection
39+
40+
(ert-deftest phpstan-test-container-runtime-command ()
41+
"Only the symbol forms ask phpstan.el to build a `run' command line."
42+
(let ((phpstan-executable 'docker))
43+
(should (equal "docker" (phpstan--container-runtime-command))))
44+
(let ((phpstan-executable 'container))
45+
(should (equal "container" (phpstan--container-runtime-command))))
46+
;; A complete command line must not be rewritten, so this returns nil.
47+
(let ((phpstan-executable '("docker" "run" "--rm" "img")))
48+
(should-not (phpstan--container-runtime-command)))
49+
(let ((phpstan-executable "/usr/bin/phpstan"))
50+
(should-not (phpstan--container-runtime-command)))
51+
(let ((phpstan-executable nil))
52+
(should-not (phpstan--container-runtime-command))))
53+
54+
(ert-deftest phpstan-test-container-executable-p ()
55+
"Recognize a container even in the explicit command-line form."
56+
(dolist (exe '(docker container))
57+
(let ((phpstan-executable exe))
58+
(should (phpstan--container-executable-p))))
59+
(let ((phpstan-executable '("docker" "run" "--rm" "img")))
60+
(should (phpstan--container-executable-p)))
61+
(let ((phpstan-executable '("container" "run" "--rm" "img")))
62+
(should (phpstan--container-executable-p)))
63+
;; An unknown runtime is not treated as a container.
64+
(let ((phpstan-executable '("podman" "run" "img")))
65+
(should-not (phpstan--container-executable-p)))
66+
(let ((phpstan-executable "/usr/bin/phpstan"))
67+
(should-not (phpstan--container-executable-p)))
68+
(let ((phpstan-executable nil))
69+
(should-not (phpstan--container-executable-p))))
70+
71+
;;; Version parsing
72+
73+
(ert-deftest phpstan-test-version-from-output ()
74+
(should (equal "1.12.33"
75+
(phpstan--version-from-output
76+
"PHPStan - PHP Static Analysis Tool 1.12.33\n")))
77+
;; A container runtime prints progress before the version; the last line wins.
78+
(should (equal "2.2.5"
79+
(phpstan--version-from-output
80+
"[0/6] Fetching image\n[6/6] Starting container\nPHPStan - PHP Static Analysis Tool 2.2.5\n")))
81+
(should-not (phpstan--version-from-output nil))
82+
(should-not (phpstan--version-from-output "")))
83+
84+
(ert-deftest phpstan-test-editor-mode-available-p ()
85+
"Version gating, exercised through the cache to avoid shelling out."
86+
(let ((phpstan-activate-editor-mode nil))
87+
;; The alist is keyed by the whole command line joined with spaces.
88+
(cl-flet ((available (version)
89+
(let ((phpstan-executable-versions-alist (list (cons "phpstan" version))))
90+
(phpstan-editor-mode-available-p "phpstan"))))
91+
(should (available "1.12.27"))
92+
(should (available "1.13.0"))
93+
(should (available "2.1.17"))
94+
(should (available "2.2.5"))
95+
(should (available "1.12.99-dev@abcdef"))
96+
(should-not (available "1.12.26"))
97+
(should-not (available "2.1.16"))
98+
(should-not (available ""))))
99+
;; Explicit overrides ignore the version entirely.
100+
(let ((phpstan-activate-editor-mode 'enabled))
101+
(should (phpstan-editor-mode-available-p "anything")))
102+
(let ((phpstan-activate-editor-mode 'disabled))
103+
(should-not (phpstan-editor-mode-available-p "anything"))))
104+
105+
;;; Path normalization
106+
107+
(ert-deftest phpstan-test-normalize-path ()
108+
;; Use a real absolute root and derive the expected value with the same
109+
;; `expand-file-name' the code uses, so the test survives Windows drive
110+
;; letters and separators rather than assuming a Unix-shaped "/proj/".
111+
(let ((root (file-name-as-directory
112+
(expand-file-name "phpstan-test-proj" temporary-file-directory))))
113+
(cl-letf (((symbol-function 'php-project-get-root-dir) (lambda () root)))
114+
(let ((src (expand-file-name "src/A.php" root)))
115+
;; A containerized PHPStan sees the project under its mount point.
116+
(let ((phpstan-executable 'docker)
117+
(phpstan-replace-path-prefix nil))
118+
(should (equal (expand-file-name "src/A.php" "/app")
119+
(phpstan-normalize-path src))))
120+
;; A local executable leaves the path alone.
121+
(let ((phpstan-executable nil)
122+
(phpstan-replace-path-prefix nil))
123+
(should (equal src (phpstan-normalize-path src))))))))
124+
125+
;;; Command line construction
126+
127+
(defmacro phpstan-test--with-stubbed-project (&rest body)
128+
"Run BODY with the project root and config file stubbed to fixed values.
129+
The root is a real absolute path under `temporary-file-directory', so paths
130+
survive `expand-file-name' on every platform."
131+
(declare (indent 0))
132+
`(let ((phpstan-test--root (file-name-as-directory
133+
(expand-file-name "phpstan-test-proj"
134+
temporary-file-directory))))
135+
(cl-letf (((symbol-function 'php-project-get-root-dir)
136+
(lambda () phpstan-test--root))
137+
((symbol-function 'phpstan-get-config-file)
138+
(lambda () (expand-file-name "phpstan.neon" phpstan-test--root))))
139+
(let ((phpstan-replace-path-prefix nil)
140+
(phpstan-autoload-file nil)
141+
(phpstan-memory-limit nil)
142+
(phpstan-level nil)
143+
(phpstan-use-xdebug-option nil)
144+
(phpstan--use-xdebug-option nil))
145+
,@body))))
146+
147+
(ert-deftest phpstan-test-command-args-keeps-command-name ()
148+
"The `(STRING . (ARGUMENTS ...))' form must run the command, not its first arg."
149+
(phpstan-test--with-stubbed-project
150+
(let ((phpstan-executable '("docker" "run" "--rm" "-v" "/proj:/app" "img")))
151+
(let ((args (phpstan-get-command-args :include-executable t)))
152+
(should (equal "docker" (car args)))
153+
(should (equal '("docker" "run" "--rm" "-v" "/proj:/app" "img" "analyze")
154+
(seq-take args 7)))))))
155+
156+
(ert-deftest phpstan-test-command-args-does-not-mutate-executable ()
157+
"`phpstan-get-command-args' must not grow the caller's `phpstan-executable'."
158+
(phpstan-test--with-stubbed-project
159+
(let* ((original (list "docker" "run" "--rm" "img"))
160+
(phpstan-executable original))
161+
(phpstan-get-command-args :include-executable t)
162+
(phpstan-get-command-args :include-executable t)
163+
(should (equal '("docker" "run" "--rm" "img") original))
164+
;; Two calls must produce the same thing.
165+
(should (equal (phpstan-get-command-args :include-executable t)
166+
(phpstan-get-command-args :include-executable t))))))
167+
168+
(ert-deftest phpstan-test-command-args-does-not-mutate-options ()
169+
"The `:options' list is the caller's; it must come back unchanged."
170+
(phpstan-test--with-stubbed-project
171+
(let ((phpstan-executable "/bin/phpstan")
172+
(options (list "--generate-baseline")))
173+
(cl-letf (((symbol-function 'phpstan-get-executable-and-args)
174+
(lambda () (list "/bin/phpstan"))))
175+
(phpstan-get-command-args :include-executable t :options options)
176+
(phpstan-get-command-args :include-executable t :options options)
177+
(should (equal '("--generate-baseline") options))))))
178+
179+
(ert-deftest phpstan-test-command-args-normalizes-config-for-container ()
180+
"The config file is rewritten to the container mount point."
181+
(phpstan-test--with-stubbed-project
182+
(let ((phpstan-executable 'docker))
183+
(let ((args (phpstan-get-command-args :include-executable t)))
184+
(should (member (expand-file-name "phpstan.neon" "/app") args))
185+
(should-not (member (phpstan-get-config-file) args))))))
186+
187+
(provide 'phpstan-test)
188+
;;; phpstan-test.el ends here

0 commit comments

Comments
 (0)