Personal emacs config
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

464 lines
19 KiB

  1. ;;; polymode-test-utils.el --- Testing utilities for polymode -*- lexical-binding: t -*-
  2. ;;
  3. ;; Copyright (C) 2018-2019, Vitalie Spinu
  4. ;; Author: Vitalie Spinu
  5. ;; URL: https://github.com/polymode/polymode
  6. ;;
  7. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  8. ;;
  9. ;; This file is *NOT* part of GNU Emacs.
  10. ;;
  11. ;; This program is free software; you can redistribute it and/or
  12. ;; modify it under the terms of the GNU General Public License as
  13. ;; published by the Free Software Foundation; either version 3, or
  14. ;; (at your option) any later version.
  15. ;;
  16. ;; This program is distributed in the hope that it will be useful,
  17. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  19. ;; General Public License for more details.
  20. ;;
  21. ;; You should have received a copy of the GNU General Public License
  22. ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
  23. ;;
  24. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  25. ;;
  26. ;;
  27. ;;; Commentary:
  28. ;;
  29. ;; This file should be loaded only in tests.
  30. ;;
  31. ;;; Code:
  32. (require 'ert)
  33. (require 'polymode)
  34. (eval-when-compile
  35. (require 'cl-lib))
  36. ;; (require 'font-lock)
  37. ;; (global-font-lock-mode t)
  38. ;; (add-hook 'after-change-major-mode-hook #'global-font-lock-mode-enable-in-buffers)
  39. ;; (message "ACMH: %s GFL:%s" after-change-major-mode-hook global-font-lock-mode)
  40. (setq ert-batch-backtrace-right-margin 200)
  41. (defvar pm-verbose (getenv "PM_VERBOSE"))
  42. (defvar pm-test-current-change-set nil)
  43. (defun pm-test-get-file (name)
  44. "Find the file with NAME from inside a poly-xyz repo.
  45. Look into tests/input directory then in samples directory."
  46. (let ((files (list (expand-file-name (format "./tests/input/%s" name) default-directory)
  47. (expand-file-name (format "./input/%s" name) default-directory)
  48. (expand-file-name (format "./samples/%s" name) default-directory)
  49. (expand-file-name (format "../samples/%s" name) default-directory))))
  50. (or (cl-loop for f in files
  51. if (file-exists-p f) return f)
  52. (error "No file with name '%s' found in '%s'" name default-directory))))
  53. (defun pm-test-matcher (string span-alist matcher &optional dry-run)
  54. (with-temp-buffer
  55. (insert string)
  56. (goto-char (point-min))
  57. (let (prev-span)
  58. (when dry-run
  59. (message "("))
  60. (while (not (eobp))
  61. (if dry-run
  62. (let ((span (funcall matcher)))
  63. (unless (equal prev-span span)
  64. (setq prev-span span)
  65. (message " (%d . %S)" (nth 1 span) span)))
  66. (let* ((span (funcall matcher))
  67. (sbeg (nth 1 span))
  68. (ref-span (alist-get sbeg span-alist)))
  69. (unless (equal span ref-span)
  70. (ert-fail (list :pos (point) :span span :ref-span ref-span)))
  71. (when (and prev-span
  72. (not (or (eq (nth 1 prev-span) sbeg)
  73. (eq (nth 2 prev-span) sbeg))))
  74. (ert-fail (list :pos (point) :span span :prev-span prev-span)))
  75. (setq prev-span span)))
  76. (forward-char 1))
  77. (when dry-run
  78. (message ")"))
  79. nil)))
  80. (defmacro pm-test-run-on-string (mode string &rest body)
  81. "Run BODY in a temporary buffer containing STRING in MODE.
  82. MODE is a quoted symbol."
  83. (declare (indent 1) (debug (form form body)))
  84. `(let ((*buf* "*pm-test-string-buffer*"))
  85. (when (get-buffer *buf*)
  86. (kill-buffer *buf*))
  87. (with-current-buffer (get-buffer-create *buf*)
  88. (insert (substring-no-properties ,string))
  89. (funcall ,mode)
  90. (setq-default indent-tabs-mode nil)
  91. ;; In emacs 27 this is called from run-mode-hooks
  92. (and (bound-and-true-p syntax-propertize-function)
  93. (not (local-variable-p 'parse-sexp-lookup-properties))
  94. (setq-local parse-sexp-lookup-properties t))
  95. (goto-char (point-min))
  96. (let ((poly-lock-allow-background-adjustment nil))
  97. (when polymode-mode
  98. ;; font-lock not activated in batch mode
  99. (setq-local poly-lock-allow-fontification t)
  100. (poly-lock-mode t))
  101. (font-lock-ensure)
  102. ,@body)
  103. (current-buffer))))
  104. (defun pm-test-spans (mode string)
  105. (declare (indent 1))
  106. (pm-test-run-on-string mode
  107. string
  108. (pm-map-over-spans
  109. (lambda (span)
  110. (let ((range0 (pm-span-to-range span)))
  111. (goto-char (car range0))
  112. (while (< (point) (cdr range0))
  113. (let ((range-pos (pm-innermost-range (point) 'no-cache)))
  114. (unless (equal range0 range-pos)
  115. (switch-to-buffer (current-buffer))
  116. (ert-fail (list :pos (point)
  117. :range0 range0
  118. :range-pos range-pos))))
  119. (forward-char)))))))
  120. (defun pm-test-spans-on-file (mode file-name)
  121. (let ((file (pm-test-get-file file-name)))
  122. (pm-test-spans mode
  123. (with-current-buffer (find-file-noselect file)
  124. (substring-no-properties (buffer-string))))))
  125. (defmacro pm-test-run-on-file (mode file-name &rest body)
  126. "Run BODY in a buffer with the content of FILE-NAME in MODE."
  127. (declare (indent 2) (debug (sexp sexp body)))
  128. (let ((pre-form (when (eq (car body) :pre-form)
  129. (prog1 (cadr body)
  130. (setq body (cddr body))))))
  131. `(let ((poly-lock-allow-background-adjustment nil)
  132. ;; snapshot it during the expansion to be able to run polymode-organization tests
  133. (file ,(pm-test-get-file file-name))
  134. (pm-extra-span-info nil)
  135. (buf "*pm-test-file-buffer*"))
  136. (when (get-buffer buf)
  137. (kill-buffer buf))
  138. (with-current-buffer (get-buffer-create buf)
  139. (when pm-verbose
  140. (message "\n=================== testing %s =======================" file))
  141. (switch-to-buffer buf)
  142. (insert-file-contents file)
  143. (remove-hook 'text-mode-hook 'flyspell-mode) ;; triggers "too much reentrancy" error
  144. (let ((inhibit-message (not pm-verbose)))
  145. (funcall-interactively ',mode))
  146. ;; (flyspell-mode -1) ;; triggers "too much reentrancy" error
  147. (hack-local-variables 'ignore-mode)
  148. (goto-char (point-min))
  149. ,pre-form
  150. ;; need this to activate all chunks
  151. (font-lock-ensure)
  152. (goto-char (point-min))
  153. (save-excursion
  154. (let ((font-lock-mode t))
  155. (pm-map-over-spans
  156. (lambda (_)
  157. (setq font-lock-mode t)
  158. ;; This is not picked up because font-lock is nil on innermode
  159. ;; initialization. Don't know how to fix this more elegantly.
  160. ;; For now our tests are all with font-lock, so we are fine for
  161. ;; now.
  162. ;; !! Font-lock is not activated in batch mode !!
  163. (setq-local poly-lock-allow-fontification t)
  164. (poly-lock-mode t)
  165. ;; redisplay is not triggered in batch and often it doesn't trigger
  166. ;; fontification in X either (waf?)
  167. (add-hook 'after-change-functions #'pm-test-invoke-fontification t t))
  168. (point-min) (point-max))))
  169. (font-lock-ensure)
  170. ,@body
  171. (current-buffer)))))
  172. (defun pm-test-span-faces (span &optional allow-failed-faces)
  173. ;; head/tail is usually highlighted incorrectly by host modes when only head
  174. ;; is in the buffer, so we just skip those head-tails which have
  175. ;; :head/tail-mode 'host
  176. (when (eq (car span) (pm-true-span-type *span*))
  177. (let* ((poly-lock-allow-background-adjustment nil)
  178. (sbeg (nth 1 span))
  179. (send (nth 2 span))
  180. (smode major-mode)
  181. (stext (buffer-substring-no-properties sbeg send))
  182. ;; other buffer
  183. (ref-buf (pm-test-run-on-string smode stext))
  184. (ref-pos 1))
  185. (when pm-verbose
  186. (message "---- testing %s ----" (pm-format-span span t)))
  187. ;; NB: String delimiters '' in pascal mode don't work in batch
  188. ;; (require 'polymode-debug)
  189. ;; (when (and (eq smode 'pascal-mode)
  190. ;; (> (buffer-size ref-buf) 29)
  191. ;; (> (buffer-size) 700))
  192. ;; (message "%s"
  193. ;; (list
  194. ;; :parse-sexp-lookup-properties parse-sexp-lookup-properties
  195. ;; :font-lock-keywords-only font-lock-keywords-only
  196. ;; :font-lock-syntactic-face-function font-lock-syntactic-face-function
  197. ;; :font-lock-sk font-lock-syntactic-keywords
  198. ;; :syntax-prop-fun syntax-propertize-function
  199. ;; :ppss (syntax-ppss 675)
  200. ;; :char (pm--syntax-after 675)))
  201. ;; (with-current-buffer ref-buf
  202. ;; (message "%s"
  203. ;; (list
  204. ;; :parse-sexp-lookup-properties parse-sexp-lookup-properties
  205. ;; :font-lock-keywords-only font-lock-keywords-only
  206. ;; :font-lock-syntactic-face-function font-lock-syntactic-face-function
  207. ;; :font-lock-sk font-lock-syntactic-keywords
  208. ;; :syntax-prop-fun syntax-propertize-function
  209. ;; :ppss-29 (syntax-ppss 29)
  210. ;; :char-29 (pm--syntax-after 29)))))
  211. (while ref-pos
  212. (let* ((pos (1- (+ ref-pos sbeg)))
  213. (face (get-text-property pos 'face))
  214. (ref-face (get-text-property ref-pos 'face ref-buf)))
  215. (unless (or
  216. ;; in markdown fence regexp matches end of line; it's likely
  217. ;; to be a common mismatch between host mode and polymode,
  218. ;; thus don't check first pos if it's a new line
  219. (and (= ref-pos 1)
  220. (with-current-buffer ref-buf
  221. (eq (char-after 1) ?\n)))
  222. (member face allow-failed-faces)
  223. (equal face ref-face))
  224. (let ((data
  225. (append
  226. (when pm-test-current-change-set
  227. (list :change pm-test-current-change-set))
  228. (list
  229. ;; :af poly-lock-allow-fontification
  230. ;; :fl font-lock-mode
  231. :face face
  232. :ref-face ref-face
  233. :pos pos
  234. :ref-pos ref-pos
  235. :line (progn (goto-char pos)
  236. (buffer-substring-no-properties (point-at-bol) (point-at-eol)))
  237. :ref-line (with-current-buffer ref-buf
  238. (goto-char ref-pos)
  239. (buffer-substring-no-properties (point-at-bol) (point-at-eol)))
  240. :mode smode))))
  241. ;; for the interactive convenience
  242. (switch-to-buffer (current-buffer))
  243. (ert-fail data)))
  244. (setq ref-pos (next-single-property-change ref-pos 'face ref-buf)))))))
  245. (defun pm-test-faces (&optional allow-failed-faces)
  246. "Execute `pm-test-span-faces' for every span in the buffer.
  247. ALLOW-FAILED-FACES should be a list of faces on which failures
  248. are OK."
  249. (save-excursion
  250. (pm-map-over-spans
  251. (lambda (span) (pm-test-span-faces span allow-failed-faces)))))
  252. (defun pm-test-goto-loc (loc)
  253. "Go to LOC and switch to polymode indirect buffer.
  254. LOC can be either
  255. - a number giving position in the buffer
  256. - regexp to search for from point-min
  257. - a cons of the form (ROW . COL)
  258. In the last case ROW can be either a number or a regexp to search
  259. for and COL either a column number or symbols beg or end
  260. indicating beginning or end of the line. When COL is nil, goto
  261. indentation."
  262. (cond
  263. ((numberp loc)
  264. (goto-char loc))
  265. ((stringp loc)
  266. (goto-char (point-min))
  267. (re-search-forward loc))
  268. ((consp loc)
  269. (goto-char (point-min))
  270. (let ((row (car loc)))
  271. (goto-char (point-min))
  272. (cond
  273. ((stringp row)
  274. (re-search-forward row))
  275. ((numberp row)
  276. (forward-line (1- row)))
  277. (t (error "Invalid row spec %s" row))))
  278. (let* ((col (cdr loc))
  279. (col (if (listp col)
  280. (car col)
  281. col)))
  282. (cond
  283. ((numberp col)
  284. (forward-char col))
  285. ((eq col 'end)
  286. (end-of-line))
  287. ((eq col 'beg)
  288. (beginning-of-line))
  289. ((null col)
  290. (back-to-indentation))
  291. (t (error "Invalid col spec %s" col))))))
  292. (when polymode-mode
  293. ;; pm-set-buffer would do for programs but not for interactive debugging
  294. (pm-switch-to-buffer (point))))
  295. (defun pm-test-goto-loc-other-window ()
  296. "Utility to navigate to loc at point in other buffer.
  297. LOC is as in `pm-test-goto-loc'."
  298. (interactive)
  299. (let ((loc (or (sexp-at-point)
  300. (read--expression "Loc: "))))
  301. (when (symbolp loc)
  302. (setq loc (string-to-number (thing-at-point 'word))))
  303. (other-window 1)
  304. (pm-test-goto-loc loc)))
  305. (defun pm-test-invoke-fontification (&rest _ignore)
  306. "Mimic calls to fontification functions by redisplay.
  307. Needed because redisplay is not triggered in batch mode."
  308. (when fontification-functions
  309. (save-match-data
  310. (save-restriction
  311. (widen)
  312. (save-excursion
  313. (let (pos)
  314. (while (setq pos (text-property-any (point-min) (point-max) 'fontified nil))
  315. (let ((inhibit-modification-hooks t)
  316. (poly-lock-defer-after-change nil)
  317. (inhibit-redisplay t))
  318. (when pm-verbose
  319. (message "after change fontification-functions (%s)" pos))
  320. (run-hook-with-args 'fontification-functions pos)))))))))
  321. (defmacro pm-test-poly-lock (mode file &rest change-sets)
  322. "Test font-lock for MODE and FILE.
  323. CHANGE-SETS is a collection of forms of the form (NAME-LOC &rest
  324. BODY). NAME-LOC is a list of the form (NAME LOCK) where NAME is a
  325. symbol, LOC is the location as in `pm-test-goto-loc'. Before and
  326. after execution of the BODY undo-boundary is set and after the
  327. execution undo is called once. After each change-set
  328. `pm-test-faces' on the whole file is run."
  329. (declare (indent 2)
  330. (debug (sexp sexp &rest ((name sexp) &rest form))))
  331. `(kill-buffer
  332. (pm-test-run-on-file ,mode ,file
  333. (pm-test-faces)
  334. (dolist (cset ',change-sets)
  335. (let ((poly-lock-defer-after-change nil)
  336. (pm-test-current-change-set (caar cset)))
  337. (setq pm-extra-span-info (caar cset))
  338. (undo-boundary)
  339. (pm-test-goto-loc (nth 1 (car cset)))
  340. (eval (cons 'progn (cdr cset)))
  341. (undo-boundary)
  342. (pm-test-faces)
  343. (let ((inhibit-message (not pm-verbose)))
  344. (undo)))))))
  345. (defun pm-test--run-indentation-tests ()
  346. "Run an automatic batch of indentation tests.
  347. First run `indent-line' on every line and compare original and
  348. indented version. Then compute stasrt,middle and end points of
  349. each span and call `indent-region' on a shuffled set of these
  350. points."
  351. (goto-char (point-min))
  352. (set-buffer-modified-p nil)
  353. (while (not (eobp))
  354. (let ((orig-line (buffer-substring-no-properties (point-at-eol) (point-at-bol))))
  355. (unless (string-match-p "no-indent-test" orig-line)
  356. (undo-boundary)
  357. ;; (pm-switch-to-buffer)
  358. ;; (message "line:%d pos:%s buf:%s ppss:%s spd:%s"
  359. ;; (line-number-at-pos) (point) (current-buffer)
  360. ;; (syntax-ppss) syntax-propertize--done)
  361. (pm-indent-line-dispatcher)
  362. (unless (equal orig-line (buffer-substring-no-properties (point-at-eol) (point-at-bol)))
  363. (undo-boundary)
  364. (pm-switch-to-buffer (point))
  365. (ert-fail (list :pos (point) :line (line-number-at-pos)
  366. :mode major-mode
  367. :indent-line (buffer-substring-no-properties (point-at-bol) (point-at-eol)))))))
  368. (forward-line 1))
  369. (let (points1 points2)
  370. (pm-map-over-spans (lambda (span) (push (/ (+ (nth 1 span) (nth 2 span)) 2) points1)))
  371. (random "some-seed")
  372. (let ((len (length points1)))
  373. (dotimes (_ len)
  374. (push (elt points1 (random len)) points2)))
  375. (let ((points2 (reverse points1)))
  376. (cl-mapc
  377. (lambda (beg end)
  378. (unless (= beg end)
  379. (let ((orig-region (buffer-substring-no-properties beg end)))
  380. (unless (string-match-p "no-indent-test" orig-region)
  381. (undo-boundary)
  382. (indent-region beg end)
  383. (unless (equal orig-region (buffer-substring-no-properties beg end))
  384. (undo-boundary)
  385. (pm-switch-to-buffer beg)
  386. (ert-fail `(indent-region ,beg ,end)))))))
  387. points1 points2))))
  388. (defmacro pm-test-indentation (mode file)
  389. "Test indentation for MODE and FILE."
  390. `(pm-test-run-on-file ,mode ,file
  391. (undo-boundary)
  392. (let ((inhibit-message (not pm-verbose)))
  393. (unwind-protect
  394. (pm-test--run-indentation-tests)
  395. (undo-boundary)))))
  396. (defmacro pm-test-file-indent (mode file-with-indent &optional file-no-indent)
  397. `(pm-test-run-on-file ,mode ,(or file-no-indent file-with-indent)
  398. (let ((indent-tabs-mode nil)
  399. (right (with-current-buffer (find-file-noselect
  400. ,(pm-test-get-file file-with-indent))
  401. (substring-no-properties (buffer-string))))
  402. (inhibit-message t))
  403. (unless ,file-no-indent
  404. (goto-char 1)
  405. (while (re-search-forward "^[ \t]+" nil t)
  406. (replace-match ""))
  407. (goto-char 1))
  408. (indent-region (point-min) (point-max))
  409. (let ((new (substring-no-properties (buffer-string))))
  410. (unless (string= right new)
  411. (require 'pascal)
  412. (let ((pos (1+ (pascal-string-diff right new))))
  413. (ert-fail (list "Wrong indent" :pos pos
  414. :ref (with-temp-buffer
  415. (insert right)
  416. (goto-char pos)
  417. (buffer-substring-no-properties (point-at-bol) (point-at-eol)))
  418. :new (progn
  419. (goto-char pos)
  420. (buffer-substring-no-properties (point-at-bol) (point-at-eol)))))))))))
  421. (defmacro pm-test-map-over-modes (mode file)
  422. `(pm-test-run-on-file ,mode ,file
  423. (let ((beg (point-min))
  424. (end (point-max)))
  425. (with-buffer-prepared-for-poly-lock
  426. (remove-text-properties beg end '(:pm-span :pm-face)))
  427. (pm-map-over-modes (lambda (b e)) beg end)
  428. (while (< beg end)
  429. (let ((span (get-text-property beg :pm-span))
  430. (mid (next-single-property-change beg :pm-span nil end)))
  431. (dolist (pos (list beg
  432. (/ (+ beg mid) 2)
  433. (1- mid)))
  434. (let ((ispan (pm-innermost-span pos t)))
  435. (unless (equal span ispan)
  436. (let ((span (copy-sequence span))
  437. (ispan (copy-sequence ispan)))
  438. (setf (nth 3 span) (eieio-object-name (nth 3 span)))
  439. (setf (nth 3 ispan) (eieio-object-name (nth 3 ispan)))
  440. (pm-switch-to-buffer pos)
  441. (ert-fail (list :pos pos :mode-span span :innermost-span ispan))))))
  442. (setq beg (nth 2 span)))))))
  443. (provide 'polymode-test-utils)
  444. ;;; polymode-test-utils.el ends here