;;; cperl-mode-tests.el --- Test for cperl-mode -*- lexical-binding: t -*- ;; Copyright (C) 2020-2021 Free Software Foundation, Inc. ;; Author: Harald Jörg ;; Maintainer: Harald Jörg ;; Keywords: internal ;; Homepage: https://github.com/HaraldJoerg/cperl-mode ;; This file is part of GNU Emacs. ;; GNU Emacs is free software: you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; GNU Emacs is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License ;; along with GNU Emacs. If not, see . ;;; Commentary: ;; This is a collection of tests for CPerl-mode. ;;; Code: (defvar cperl-test-mode #'cperl-mode) (require 'cperl-mode) (require 'ert) (require 'ert-x) ;;; Utilities (defun cperl-test-ppss (text regexp) "Return the `syntax-ppss' after the last character matched by REGEXP in TEXT." (interactive) (with-temp-buffer (insert text) (funcall cperl-test-mode) (goto-char (point-min)) (re-search-forward regexp) (syntax-ppss))) (defmacro cperl--run-test-cases (file &rest body) "Run all test cases in FILE with BODY. This macro helps with tests which reformat Perl code, e.g. when indenting or rearranging flow control. It extracts source code snippets and corresponding expected results from a resource file, runs BODY on the snippets, and compares the resulting buffer with the expected results. Test cases in FILE are formatted like this: # -------- NAME: input -------- Your input to the test case comes here. Both input and expected output may span several lines. # -------- NAME: expected output -------- The expected output from running BODY on the input goes here. # -------- NAME: end -------- You can have many of these blocks in one test file. You can chose a NAME for each block, which is passed to the 'should' clause for easy identification of the first test case that failed (if any). Text outside these the blocks is ignored by the tests, so you can use it to document the test cases if you wish." `(with-temp-buffer (insert-file-contents ,file) (goto-char (point-min)) (while (re-search-forward (concat "^# ?-+ \\_<\\(?1:.+?\\)\\_>: input ?-+\n" "\\(?2:\\(?:.*\n\\)+?\\)" "# ?-+ \\1: expected output ?-+\n" "\\(?3:\\(?:.*\n\\)+?\\)" "# ?-+ \\1: end ?-+") nil t) (let ((name (match-string 1)) (code (match-string 2)) (expected (match-string 3)) got) (with-temp-buffer (insert code) (goto-char (point-min)) (funcall cperl-test-mode) ,@body (setq expected (concat "test case " name ":\n" expected)) (setq got (concat "test case " name ":\n" (buffer-string))) (should (equal got expected))))))) ;;; Indentation tests (ert-deftest cperl-test-indent-exp () "Run various tests for `cperl-indent-exp' edge cases. These exercise some standard blocks and also the special treatment for Perl expressions where a closing paren isn't the end of the statement." (skip-unless (eq cperl-test-mode #'cperl-mode)) (cperl--run-test-cases (ert-resource-file "cperl-indent-exp.pl") (cperl-indent-exp))) ; here we go! (ert-deftest cperl-test-indent-styles () (skip-unless (eq cperl-test-mode #'cperl-mode)) (cperl--run-test-cases (ert-resource-file "cperl-indent-styles.pl") (cperl-set-style "PBP") (indent-region (point-min) (point-max)) ; here we go! (cperl-set-style-back))) ;;; Fontification tests (ert-deftest cperl-test-fontify-punct-vars () "Test fontification of Perl's punctiation variables. Perl has variable names containing unbalanced quotes for the list separator $\" and pre- and postmatch $` and $'. A reference to these variables, for example \\$\", should not cause the dollar to be escaped, which would then start a string beginning with the quote character. This used to be broken in cperl-mode at some point in the distant past, and is still broken in perl-mode. " (skip-unless (eq cperl-test-mode #'cperl-mode)) (let ((file (ert-resource-file "fontify-punctuation-vars.pl"))) (with-temp-buffer (insert-file-contents file) (goto-char (point-min)) (funcall cperl-test-mode) (while (search-forward "##" nil t) ;; The third element of syntax-ppss is true if in a string, ;; which would indicate bad interpretation of the quote. The ;; fourth element is true if in a comment, which should be the ;; case. (should (equal (nth 3 (syntax-ppss)) nil)) (should (equal (nth 4 (syntax-ppss)) t)))))) (ert-deftest cperl-test-fontify-declarations () "Test that declarations and package usage use consistent fontification." (with-temp-buffer (funcall cperl-test-mode) (insert "package Foo::Bar;\n") (insert "use Fee::Fie::Foe::Foo\n;") (insert "my $xyzzy = 'PLUGH';\n") (goto-char (point-min)) (font-lock-ensure) (search-forward "Bar") (should (equal (get-text-property (match-beginning 0) 'face) 'font-lock-function-name-face)) (search-forward "use") ; This was buggy in perl-mode (should (equal (get-text-property (match-beginning 0) 'face) 'font-lock-keyword-face)) (search-forward "my") (should (equal (get-text-property (match-beginning 0) 'face) 'font-lock-keyword-face)))) (ert-deftest cperl-test-fontify-special-variables () "Test fontification of variables like $^T or ${^ENCODING}. These can occur as \"local\" aliases." (skip-unless (eq cperl-test-mode #'cperl-mode)) (with-temp-buffer (insert "local ($^I, ${^UNICODE});\n") (goto-char (point-min)) (funcall cperl-test-mode) (font-lock-ensure) (search-forward "$") (should (equal (get-text-property (point) 'face) 'font-lock-variable-name-face)) (search-forward "$") (should (equal (get-text-property (point) 'face) 'font-lock-variable-name-face)))) (ert-deftest cperl-test-identify-heredoc () "Test whether a construct containing \"<<\" followed by a bareword is properly identified for a here-document if appropriate." (let ((here-docs '("$text .= <>) {" ; double angle bracket operator "expr < 'value');" "%") (should (equal (get-text-property (1- (point)) 'face) 'cperl-hash-face)) (should (equal (get-text-property (1+ (point)) 'face) 'font-lock-variable-name-face)))) (ert-deftest cperl-test-unicode-hashref () "Verify that a hashref access disambiguates {s}. CPerl mode takes the token \"s\" as a substitution unless detected otherwise. Not for perl-mode: it doesn't stringify bareword hash keys and doesn't recognize a substitution \"s}foo}bar}\"" (skip-unless (eq cperl-test-mode #'cperl-mode)) (with-temp-buffer (cperl--test-unicode-setup "$häshref->{s} # }}" "{") (should (equal (get-text-property (point) 'face) 'font-lock-string-face)) (should (equal (get-text-property (1+ (point)) 'face) nil)))) (ert-deftest cperl-test-unicode-proto () ;; perl-mode doesn't fontify prototypes at all (skip-unless (eq cperl-test-mode #'cperl-mode)) (with-temp-buffer (cperl--test-unicode-setup (concat "sub prötötyped ($) {\n" " ...;" "}\n") "prötötyped (") (should (equal (get-text-property (point) 'face) 'font-lock-string-face)))) (ert-deftest cperl-test-unicode-fhs () (with-temp-buffer (cperl--test-unicode-setup (concat "while () {\n" " ...;)\n" "}\n") "while (<") ; point is before the first char of the handle ;; Testing fontification ;; FIXME 2021-09-10: perl-mode.el and cperl-mode.el handle these ;; completely differently. perl-mode interprets barewords as ;; constants, cperl-mode does not fontify them. Both treat ;; non-barewords as globs, which are not fontified by perl-mode, ;; but fontified as strings in cperl-mode. We keep (and test) ;; that behavior "as is" because both bareword filehandles and ;; syntax are no longer recommended. (let ((bareword-face (if (equal cperl-test-mode 'perl-mode) 'font-lock-constant-face nil))) (should (equal (get-text-property (point) 'face) bareword-face))))) (ert-deftest cperl-test-unicode-hashkeys () "Test stringification of bareword hash keys. Not in perl-mode. perl-mode generally does not stringify bareword hash keys." (skip-unless (eq cperl-test-mode #'cperl-mode)) ;; Plain hash key (with-temp-buffer (cperl--test-unicode-setup "$häsh { kéy }" "{ ") (should (equal (get-text-property (point) 'face) 'font-lock-string-face))) ;; Nested hash key (with-temp-buffer (cperl--test-unicode-setup "$häsh { kéy } { kèy }" "} { ") (should (equal (get-text-property (point) 'face) 'font-lock-string-face))) ;; Key => value (with-temp-buffer (cperl--test-unicode-setup "( kéy => 'value'," "( ") (should (equal (get-text-property (point) 'face) 'font-lock-string-face)))) (ert-deftest cperl-test-word-at-point () "Test whether the function captures non-ASCII words." (skip-unless (eq cperl-test-mode #'cperl-mode)) (let ((words '("rôle" "café" "ångström" "Data::Dump::dump" "_underscore"))) (dolist (word words) (with-temp-buffer (insert " + ") ; this will be the suffix (beginning-of-line) (insert ")") ; A non-word char (insert word) (should (string= word (cperl-word-at-point-hard))))))) ;;; Function test: Building an index for imenu (ert-deftest cperl-test-imenu-index () "Test index creation for imenu. This test relies on the specific layout of the index alist as created by CPerl mode, so skip it for Perl mode." (skip-unless (eq cperl-test-mode #'cperl-mode)) (with-temp-buffer (insert-file-contents (ert-resource-file "grammar.pl")) (cperl-mode) (let ((index (cperl-imenu--create-perl-index)) current-list) (setq current-list (assoc-string "+Unsorted List+..." index)) (should current-list) (let ((expected '("(main)::outside" "Package::in_package" "Shoved::elsewhere" "Package::prototyped" "Versioned::Package::versioned" "Block::attr" "Versioned::Package::outer" "lexical" "Versioned::Block::signatured" "Package::in_package_again" "Erdős::Number::erdős_number"))) (dolist (sub expected) (should (assoc-string sub index))))))) ;;; Tests for issues reported in the Bug Tracker (defun cperl-test--run-bug-10483 () "Runs a short program, intended to be under timer scrutiny. This function is intended to be used by an Emacs subprocess in batch mode. The message buffer is used to report the result of running `cperl-indent-exp' for a very simple input. The result is expected to be different from the input, to verify that indentation actually takes place.." (let ((code "poop ('foo', \n'bar')")) ; see the bug report (message "Test Bug#10483 started") (with-temp-buffer (insert code) (funcall cperl-test-mode) (goto-char (point-min)) (search-forward "poop") (cperl-indent-exp) (message "%s" (buffer-string))))) (ert-deftest cperl-test-bug-10483 () "Check that indenting certain perl code does not loop forever. This verifies that indenting a piece of code that ends in a paren without a statement terminator on the same line does not loop forever. The test starts an asynchronous Emacs batch process under timeout control." :tags '(:expensive-test) (interactive) (skip-unless (not (getenv "EMACS_HYDRA_CI"))) ; FIXME times out (skip-unless (not (< emacs-major-version 28))) ; times out in older Emacsen (skip-unless (eq cperl-test-mode #'cperl-mode)) (let* ((emacs (concat invocation-directory invocation-name)) (test-function 'cperl-test--run-bug-10483) (test-function-name (symbol-name test-function)) (test-file (symbol-file test-function 'defun)) (ran-out-of-time nil) (process-connection-type nil) runner) (with-temp-buffer (with-timeout (2 (delete-process runner) (setq ran-out-of-time t)) (setq runner (start-process "speedy" (current-buffer) emacs "-batch" "--quick" "--load" test-file "--funcall" test-function-name)) (while (accept-process-output runner))) (should (equal ran-out-of-time nil)) (goto-char (point-min)) ;; just a very simple test for indentation: This should ;; be rather robust with regard to indentation defaults (should (string-match "poop ('foo', \n 'bar')" (buffer-string)))))) (ert-deftest cperl-test-bug-14343 () "Verify that inserting text into a HERE-doc string with Elisp does not break fontification." (with-temp-buffer (insert "my $string = <\". Related, check that calling a method named q is not mistaken as a quotelike operator." (with-temp-buffer (insert-file-contents (ert-resource-file "cperl-bug-25098.pl")) (funcall cperl-test-mode) (goto-char (point-min)) ;; good example from the bug report, with a space (search-forward "q{") (should (nth 3 (syntax-ppss))) ;; bad (but now fixed) example from the bug report, without space (search-forward "q{") (should (nth 3 (syntax-ppss))) ;; calling a method "q" (parens instead of braces to make it valid) (search-forward "q(") (should-not (nth 3 (syntax-ppss))))) (ert-deftest cperl-test-bug-28650 () "Verify that regular expressions are recognized after 'return'. The test uses the syntax property \"inside a string\" for the text in regular expressions, which is non-nil for both cperl-mode and perl-mode." (with-temp-buffer (insert-file-contents (ert-resource-file "cperl-bug-26850.pl")) (goto-char (point-min)) (re-search-forward "sub interesting {[^}]*}") (should-not (equal (nth 3 (cperl-test-ppss (match-string 0) "Today")) nil)) (re-search-forward "sub boring {[^}]*}") (should-not (equal (nth 3 (cperl-test-ppss (match-string 0) "likes\\?")) nil)))) (ert-deftest cperl-test-bug-30393 () "Verify that indentation is not disturbed by an open paren in col 0. Perl is not Lisp: An open paren in column 0 does not start a function." (cperl--run-test-cases (ert-resource-file "cperl-bug-30393.pl") (while (null (eobp)) (cperl-indent-command) (forward-line 1)))) (ert-deftest cperl-test-bug-37127 () "Verify that closing a paren in a regex goes without a message. Also check that the message is issued if the regex terminator is missing." ;; The actual fix for this bug is in simple.el, which is not ;; backported to older versions of Emacs. Therefore we skip this ;; test if we're running Emacs 27 or older. (skip-unless (< 27 emacs-major-version)) ;; Part one: Regex is ok, no messages (ert-with-message-capture collected-messages (with-temp-buffer (insert "$_ =~ /(./;") (funcall cperl-test-mode) (goto-char (point-min)) (search-forward ".") (let ((last-command-event ?\)) ;; Don't emit "Matches ..." even if not visible (e.g. in batch). (blink-matching-paren 'jump-offscreen)) (self-insert-command 1) ;; `self-insert-command' doesn't call `blink-matching-open' in ;; batch mode, so we need to call it explicitly. (blink-matching-open)) (syntax-propertize (point-max))) (should (string-equal collected-messages ""))) ;; part two: Regex terminator missing -> message (when (eq cperl-test-mode #'cperl-mode) ;; This test is only run in `cperl-mode' because only cperl-mode ;; emits a message to warn about such unclosed REs. (ert-with-message-capture collected-messages (with-temp-buffer (insert "$_ =~ /(..;") (goto-char (point-min)) (funcall cperl-test-mode) (search-forward ".") (let ((last-command-event ?\))) (self-insert-command 1)) (syntax-propertize (point-max))) (should (string-match "^End of .* string/RE" collected-messages))))) (ert-deftest cperl-test-bug-42168 () "Verify that '/' is a division after ++ or --, not a regexp. Reported in https://github.com/jrockway/cperl-mode/issues/45. If seen as regular expression, then the slash is displayed using font-lock-constant-face. If seen as a division, then it doesn't have a face property." :tags '(:fontification) ;; The next two Perl expressions have divisions. The slash does not ;; start a string. (let ((code "{ $a++ / $b }")) (should (equal (nth 8 (cperl-test-ppss code "/")) nil))) (let ((code "{ $a-- / $b }")) (should (equal (nth 8 (cperl-test-ppss code "/")) nil))) ;; The next two Perl expressions have regular expressions. The slash ;; starts a string. (let ((code "{ $a+ / $b } # /")) (should (equal (nth 8 (cperl-test-ppss code "/")) 7))) (let ((code "{ $a- / $b } # /")) (should (equal (nth 8 (cperl-test-ppss code "/")) 7)))) (ert-deftest cperl-test-bug-45255 () "Verify that \"<<>>\" is recognized as not starting a HERE-doc." (let ((code (concat "while (<<>>) {\n" " ...;\n" "}\n"))) ;; The yadda-yadda operator should not be in a string. (should (equal (nth 8 (cperl-test-ppss code "\\.")) nil)))) (ert-deftest cperl-test-bug-47112 () "Check that in a bareword starting with a quote-like operator followed by an underscore is not interpreted as that quote-like operator. Also check that a quote-like operator followed by a colon (which is, like ?_, a symbol in CPerl mode) _is_ identified as that quote like operator." (with-temp-buffer (funcall cperl-test-mode) (insert "sub y_max { q:bar:; y _bar_foo_; }") (goto-char (point-min)) (syntax-propertize (point-max)) (font-lock-ensure) (search-forward "max") (should (equal (get-text-property (match-beginning 0) 'face) 'font-lock-function-name-face)) (search-forward "bar") (should (equal (get-text-property (match-beginning 0) 'face) 'font-lock-string-face)) ; perl-mode doesn't highlight (when (eq cperl-test-mode #'cperl-mode) (search-forward "_") (should (equal (get-text-property (match-beginning 0) 'face) (if (eq cperl-test-mode #'cperl-mode) 'font-lock-constant-face font-lock-string-face)))))) (ert-deftest cperl-test-hyperactive-electric-else () "Demonstrate cperl-electric-else behavior. If `cperl-electric-keywords' is true, keywords like \"else\" and \"continue\" are expanded by a following empty block, with the cursor in the appropriate position to write that block. This, however, must not happen when the keyword occurs in a variable \"$else\" or \"$continue\"." (skip-unless (eq cperl-test-mode #'cperl-mode)) ;; `self-insert-command' takes a second argument only since Emacs 27 (skip-unless (not (< emacs-major-version 27))) (with-temp-buffer (setq cperl-electric-keywords t) (cperl-mode) (insert "continue") (self-insert-command 1 ?\ ) (indent-region (point-min) (point-max)) (goto-char (point-min)) ;; cperl-mode creates a block here (should (search-forward-regexp "continue {\n[[:blank:]]+\n}"))) (with-temp-buffer (setq cperl-electric-keywords t) (cperl-mode) (insert "$continue") (self-insert-command 1 ?\ ) (indent-region (point-min) (point-max)) (goto-char (point-min)) ;; No block should have been created here (should-not (search-forward-regexp "{" nil t)))) (ert-deftest cperl-test-bug-47598 () "Check that a file test followed by ? is no longer interpreted as a regex." ;; Testing the text from the bug report (with-temp-buffer (insert "my $f = -f ? 'file'\n") (insert " : -l ? [readlink]\n") (insert " : -d ? 'dir'\n") (insert " : 'unknown';\n") (funcall cperl-test-mode) ;; Perl mode doesn't highlight file tests as functions, so we ;; can't test for the function's face. But we can verify that the ;; function is not a string. (goto-char (point-min)) (search-forward "?") (should-not (nth 3 (syntax-ppss (point))))) ;; Testing the actual targets for the regexp: m?foo? (still valid) ;; and ?foo? (invalid since Perl 5.22) (with-temp-buffer (insert "m?foo?;") (funcall cperl-test-mode) (should (nth 3 (syntax-ppss 3)))) (with-temp-buffer (insert " ?foo?;") (funcall cperl-test-mode) (should-not (nth 3 (syntax-ppss 3))))) ;;; cperl-mode-tests.el ends here