summaryrefslogtreecommitdiff
path: root/test/lisp/eshell
diff options
context:
space:
mode:
Diffstat (limited to 'test/lisp/eshell')
-rw-r--r--test/lisp/eshell/em-alias-tests.el87
-rw-r--r--test/lisp/eshell/em-basic-tests.el71
-rw-r--r--test/lisp/eshell/em-dirs-tests.el102
-rw-r--r--test/lisp/eshell/em-extpipe-tests.el205
-rw-r--r--test/lisp/eshell/em-glob-tests.el197
-rw-r--r--test/lisp/eshell/em-hist-tests.el38
-rw-r--r--test/lisp/eshell/em-ls-tests.el57
-rw-r--r--test/lisp/eshell/em-pred-tests.el566
-rw-r--r--test/lisp/eshell/em-script-tests.el62
-rw-r--r--test/lisp/eshell/em-tramp-tests.el88
-rw-r--r--test/lisp/eshell/esh-cmd-tests.el294
-rw-r--r--test/lisp/eshell/esh-io-tests.el292
-rw-r--r--test/lisp/eshell/esh-opt-tests.el289
-rw-r--r--test/lisp/eshell/esh-proc-tests.el249
-rw-r--r--test/lisp/eshell/esh-var-tests.el569
-rw-r--r--test/lisp/eshell/eshell-tests-helpers.el140
-rw-r--r--test/lisp/eshell/eshell-tests.el234
17 files changed, 3370 insertions, 170 deletions
diff --git a/test/lisp/eshell/em-alias-tests.el b/test/lisp/eshell/em-alias-tests.el
new file mode 100644
index 00000000000..aca622220e3
--- /dev/null
+++ b/test/lisp/eshell/em-alias-tests.el
@@ -0,0 +1,87 @@
+;;; em-alias-tests.el --- em-alias test suite -*- lexical-binding:t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Tests for Eshell's alias module.
+
+;;; Code:
+
+(require 'ert)
+(require 'esh-mode)
+(require 'eshell)
+(require 'em-alias)
+
+(require 'eshell-tests-helpers
+ (expand-file-name "eshell-tests-helpers"
+ (file-name-directory (or load-file-name
+ default-directory))))
+;;; Tests:
+
+(ert-deftest em-alias-test/simple-alias ()
+ "Test a simple alias with no arguments"
+ (with-temp-eshell
+ (eshell-insert-command "alias say-hi 'echo hi'")
+ (eshell-match-command-output "say-hi" "hi\n")
+ (eshell-match-command-output "say-hi bye" "hi\n")))
+
+(ert-deftest em-alias-test/alias-arg-vars ()
+ "Test alias with $0, $1, ... variables"
+ (with-temp-eshell
+ (eshell-insert-command "alias show-args 'printnl $0 \"$1 $2\"'")
+ (eshell-match-command-output "show-args one two" "show-args\none two\n")))
+
+(ert-deftest em-alias-test/alias-arg-vars-indices ()
+ "Test alias with $1, $2, ... variables using indices"
+ (with-temp-eshell
+ (eshell-insert-command "alias funny-sum '+ $1[0] $2[1]'")
+ (eshell-match-command-output "funny-sum (list 1 2) (list 3 4)"
+ "5\n")))
+
+(ert-deftest em-alias-test/alias-arg-vars-split-indices ()
+ "Test alias with $0, $1, ... variables using split indices"
+ (with-temp-eshell
+ (eshell-insert-command "alias my-prefix 'echo $0[- 0]'")
+ (eshell-match-command-output "my-prefix"
+ "my\n")
+ (eshell-insert-command "alias funny-sum '+ $1[: 0] $2[: 1]'")
+ (eshell-match-command-output "funny-sum 1:2 3:4"
+ "5\n")))
+
+(ert-deftest em-alias-test/alias-all-args-var ()
+ "Test alias with the $* variable"
+ (with-temp-eshell
+ (eshell-insert-command "alias show-all-args 'printnl $*'")
+ (eshell-match-command-output "show-all-args" "\\`\\'")
+ (eshell-match-command-output "show-all-args a" "a\n")
+ (eshell-match-command-output "show-all-args a b c" "a\nb\nc\n")))
+
+(ert-deftest em-alias-test/alias-all-args-var-indices ()
+ "Test alias with the $* variable using indices"
+ (with-temp-eshell
+ (eshell-insert-command "alias add-pair '+ $*[0] $*[1]'")
+ (eshell-match-command-output "add-pair 1 2" "3\n")))
+
+(ert-deftest em-alias-test/alias-all-args-var-split-indices ()
+ "Test alias with the $* variable using split indices"
+ (with-temp-eshell
+ (eshell-insert-command "alias add-funny-pair '+ $*[0][: 0] $*[1][: 1]'")
+ (eshell-match-command-output "add-funny-pair 1:2 3:4" "5\n")))
+
+;; em-alias-tests.el ends here
diff --git a/test/lisp/eshell/em-basic-tests.el b/test/lisp/eshell/em-basic-tests.el
new file mode 100644
index 00000000000..bc8baeaa035
--- /dev/null
+++ b/test/lisp/eshell/em-basic-tests.el
@@ -0,0 +1,71 @@
+;;; em-basic-tests.el --- em-basic test suite -*- lexical-binding:t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Tests for basic Eshell commands.
+
+;;; Code:
+
+(require 'ert)
+(require 'em-basic)
+
+(require 'eshell-tests-helpers
+ (expand-file-name "eshell-tests-helpers"
+ (file-name-directory (or load-file-name
+ default-directory))))
+
+;;; Tests:
+
+(ert-deftest em-basic-test/umask-print-numeric ()
+ "Test printing umask numerically."
+ (cl-letf (((symbol-function 'default-file-modes) (lambda () #o775)))
+ (eshell-command-result-equal "umask" "002\n"))
+ (cl-letf (((symbol-function 'default-file-modes) (lambda () #o654)))
+ (eshell-command-result-equal "umask" "123\n"))
+ ;; Make sure larger numbers don't cause problems.
+ (cl-letf (((symbol-function 'default-file-modes) (lambda () #o1775)))
+ (eshell-command-result-equal "umask" "002\n")))
+
+(ert-deftest em-basic-test/umask-read-symbolic ()
+ "Test printing umask symbolically."
+ (cl-letf (((symbol-function 'default-file-modes) (lambda () #o775)))
+ (eshell-command-result-equal "umask -S"
+ "u=rwx,g=rwx,o=rx\n"))
+ (cl-letf (((symbol-function 'default-file-modes) (lambda () #o654)))
+ (eshell-command-result-equal "umask -S"
+ "u=wx,g=rx,o=x\n"))
+ ;; Make sure larger numbers don't cause problems.
+ (cl-letf (((symbol-function 'default-file-modes) (lambda () #o1775)))
+ (eshell-command-result-equal "umask -S"
+ "u=rwx,g=rwx,o=rx\n")))
+
+(ert-deftest em-basic-test/umask-set ()
+ "Test setting umask."
+ (let ((file-modes 0))
+ (cl-letf (((symbol-function 'set-default-file-modes)
+ (lambda (mode) (setq file-modes mode))))
+ (eshell-test-command-result "umask 002")
+ (should (= file-modes #o775))
+ (eshell-test-command-result "umask 123")
+ (should (= file-modes #o654))
+ (eshell-test-command-result "umask $(identity #o222)")
+ (should (= file-modes #o555)))))
+
+;; em-basic-tests.el ends here
diff --git a/test/lisp/eshell/em-dirs-tests.el b/test/lisp/eshell/em-dirs-tests.el
new file mode 100644
index 00000000000..f72d708dcae
--- /dev/null
+++ b/test/lisp/eshell/em-dirs-tests.el
@@ -0,0 +1,102 @@
+;;; em-dirs-tests.el --- em-dirs test suite -*- lexical-binding:t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Tests for Eshell's dirs module.
+
+;;; Code:
+
+(require 'ert)
+(require 'esh-mode)
+(require 'eshell)
+(require 'em-dirs)
+
+(require 'eshell-tests-helpers
+ (expand-file-name "eshell-tests-helpers"
+ (file-name-directory (or load-file-name
+ default-directory))))
+;;; Tests:
+
+(ert-deftest em-dirs-test/pwd-var ()
+ "Test using the $PWD variable."
+ (let ((default-directory "/some/path"))
+ (eshell-command-result-equal "echo $PWD"
+ (expand-file-name default-directory))))
+
+(ert-deftest em-dirs-test/pwd-var-indices ()
+ "Test using the $PWD variable with indices."
+ (let ((default-directory "/some/path/here"))
+ (eshell-command-result-equal "echo $PWD[/ 1]"
+ "some")
+ (eshell-command-result-equal "echo $PWD[/ 1 3]"
+ '("some" "here"))))
+
+(ert-deftest em-dirs-test/short-pwd-var ()
+ "Test using the $+ (current directory) variable."
+ (let ((default-directory "/some/path"))
+ (eshell-command-result-equal "echo $+"
+ (expand-file-name default-directory))))
+
+(ert-deftest em-dirs-test/oldpwd-var ()
+ "Test using the $OLDPWD variable."
+ (let (eshell-last-dir-ring-file-name)
+ (with-temp-eshell
+ (eshell-match-command-output "echo $OLDPWD"
+ "\\`\\'")
+ (ring-insert eshell-last-dir-ring "/some/path")
+ (eshell-match-command-output "echo $OLDPWD"
+ "/some/path\n"))))
+
+(ert-deftest em-dirs-test/oldpwd-var-indices ()
+ "Test using the $OLDPWD variable with indices."
+ (let (eshell-last-dir-ring-file-name)
+ (with-temp-eshell
+ (ring-insert eshell-last-dir-ring "/some/path/here")
+ (eshell-match-command-output "echo $OLDPWD[/ 1]"
+ "some\n")
+ (eshell-match-command-output "echo $OLDPWD[/ 1 3]"
+ "(\"some\" \"here\")\n"))))
+
+(ert-deftest em-dirs-test/directory-ring-var ()
+ "Test using the $- (directory ring) variable."
+ (let (eshell-last-dir-ring-file-name)
+ (with-temp-eshell
+ (eshell-match-command-output "echo $-"
+ "\\`\\'")
+ (ring-insert eshell-last-dir-ring "/some/path")
+ (ring-insert eshell-last-dir-ring "/other/path")
+ (eshell-match-command-output "echo $-"
+ "/other/path\n")
+ (eshell-match-command-output "echo $-[0]"
+ "/other/path\n")
+ (eshell-match-command-output "echo $-[1]"
+ "/some/path\n"))))
+
+(ert-deftest em-dirs-test/directory-ring-var-indices ()
+ "Test using the $- (directory ring) variable with multiple indices."
+ (let (eshell-last-dir-ring-file-name)
+ (with-temp-eshell
+ (ring-insert eshell-last-dir-ring "/some/path/here")
+ (eshell-match-command-output "echo $-[0][/ 1]"
+ "some\n")
+ (eshell-match-command-output "echo $-[1][/ 1 3]"
+ "(\"some\" \"here\")\n"))))
+
+;; em-dirs-tests.el ends here
diff --git a/test/lisp/eshell/em-extpipe-tests.el b/test/lisp/eshell/em-extpipe-tests.el
new file mode 100644
index 00000000000..04e78279427
--- /dev/null
+++ b/test/lisp/eshell/em-extpipe-tests.el
@@ -0,0 +1,205 @@
+;;; em-extpipe-tests.el --- em-extpipe test suite -*- lexical-binding:t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; Author: Sean Whitton <spwhitton@spwhitton.name>
+
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'ert)
+(require 'ert-x)
+(require 'em-extpipe)
+(require 'eshell-tests-helpers
+ (expand-file-name "eshell-tests-helpers"
+ (file-name-directory (or load-file-name
+ default-directory))))
+
+(defmacro em-extpipe-tests--deftest (name input &rest body)
+ (declare (indent 2))
+ `(ert-deftest ,name ()
+ (cl-macrolet
+ ((should-parse (expected)
+ `(let ((shell-file-name "sh")
+ (shell-command-switch "-c"))
+ ;; Strip `eshell-trap-errors'.
+ (should (equal ,expected
+ (cadr (eshell-parse-command input))))))
+ (with-substitute-for-temp (&rest body)
+ ;; Substitute name of an actual temporary file and/or
+ ;; buffer into `input'. The substitution logic is
+ ;; appropriate for only the use we put it to in this file.
+ `(ert-with-temp-file temp
+ (let ((temp-buffer (generate-new-buffer " *temp*" t)))
+ (unwind-protect
+ (let ((input
+ (replace-regexp-in-string
+ "temp\\([^>]\\|\\'\\)" temp
+ (string-replace "#<buffer temp>"
+ (buffer-name temp-buffer)
+ input))))
+ ,@body)
+ (when (buffer-name temp-buffer)
+ (kill-buffer temp-buffer))))))
+ (temp-should-string= (expected)
+ `(string= ,expected (string-trim-right
+ (with-temp-buffer
+ (insert-file-contents temp)
+ (buffer-string)))))
+ (temp-buffer-should-string= (expected)
+ `(string= ,expected (string-trim-right
+ (with-current-buffer temp-buffer
+ (buffer-string))))))
+ (skip-unless shell-file-name)
+ (skip-unless shell-command-switch)
+ (skip-unless (executable-find shell-file-name))
+ (let ((input ,input))
+ (with-temp-eshell ,@body)))))
+
+(em-extpipe-tests--deftest em-extpipe-test-1
+ "echo \"bar\" *| rev >temp"
+ (skip-unless (executable-find "rev"))
+ (should-parse '(eshell-named-command
+ "sh" (list "-c" "echo \"bar\" | rev >temp")))
+ (with-substitute-for-temp
+ (eshell-match-command-output input "^$")
+ (temp-should-string= "rab")))
+
+(em-extpipe-tests--deftest em-extpipe-test-2
+ "echo \"bar\" | rev *>temp"
+ (skip-unless (executable-find "rev"))
+ (should-parse
+ '(eshell-execute-pipeline
+ '((eshell-named-command "echo" (list (eshell-escape-arg "bar")))
+ (eshell-named-command "sh" (list "-c" "rev >temp")))))
+ (with-substitute-for-temp
+ (eshell-match-command-output input "^$")
+ (temp-should-string= "rab")))
+
+(em-extpipe-tests--deftest em-extpipe-test-3 "foo *| bar | baz -d"
+ (should-parse
+ '(eshell-execute-pipeline
+ '((eshell-named-command "sh" (list "-c" "foo | bar"))
+ (eshell-named-command "baz" (list "-d"))))))
+
+(em-extpipe-tests--deftest em-extpipe-test-4
+ "echo \"bar\" *| rev >#<buffer temp>"
+ (skip-unless (executable-find "rev"))
+ (should-parse
+ '(progn
+ (ignore
+ (eshell-set-output-handle 1 'overwrite
+ (get-buffer-create "temp")))
+ (eshell-named-command "sh"
+ (list "-c" "echo \"bar\" | rev"))))
+ (with-substitute-for-temp
+ (eshell-match-command-output input "^$")
+ (temp-buffer-should-string= "rab")))
+
+(em-extpipe-tests--deftest em-extpipe-test-5
+ "foo *| bar >#<buffer quux> baz"
+ (should-parse '(eshell-named-command
+ "sh" (list "-c" "foo | bar >#<buffer quux> baz"))))
+
+(em-extpipe-tests--deftest em-extpipe-test-6
+ "foo >#<buffer quux> *| bar baz"
+ (should-parse '(eshell-named-command
+ "sh" (list "-c" "foo >#<buffer quux> | bar baz"))))
+
+(em-extpipe-tests--deftest em-extpipe-test-7
+ "foo *| bar >#<buffer quux> >>#<process other>"
+ (should-parse
+ '(progn
+ (ignore
+ (eshell-set-output-handle 1 'overwrite
+ (get-buffer-create "quux")))
+ (ignore
+ (eshell-set-output-handle 1 'append
+ (get-process "other")))
+ (eshell-named-command "sh"
+ (list "-c" "foo | bar")))))
+
+(em-extpipe-tests--deftest em-extpipe-test-8
+ "foo *| bar >/dev/kill | baz"
+ (should-parse
+ '(eshell-execute-pipeline
+ '((progn
+ (ignore
+ (eshell-set-output-handle 1 'overwrite "/dev/kill"))
+ (eshell-named-command "sh"
+ (list "-c" "foo | bar")))
+ (eshell-named-command "baz")))))
+
+(em-extpipe-tests--deftest em-extpipe-test-9 "foo \\*| bar"
+ (should-parse
+ '(eshell-execute-pipeline
+ '((eshell-named-command "foo"
+ (list (eshell-escape-arg "*")))
+ (eshell-named-command "bar")))))
+
+(em-extpipe-tests--deftest em-extpipe-test-10 "foo \"*|\" *>bar"
+ (should-parse
+ '(eshell-named-command "sh" (list "-c" "foo \"*|\" >bar"))))
+
+(em-extpipe-tests--deftest em-extpipe-test-11 "foo '*|' bar"
+ (should-parse '(eshell-named-command
+ "foo" (list (eshell-escape-arg "*|") "bar"))))
+
+(em-extpipe-tests--deftest em-extpipe-test-12 ">foo bar *| baz"
+ (should-parse
+ '(eshell-named-command "sh" (list "-c" ">foo bar | baz"))))
+
+(em-extpipe-tests--deftest em-extpipe-test-13 "foo*|bar"
+ (should-parse '(eshell-execute-pipeline
+ '((eshell-named-command (eshell-concat nil "foo" "*"))
+ (eshell-named-command "bar")))))
+
+(em-extpipe-tests--deftest em-extpipe-test-14 "tac *<temp"
+ (skip-unless (executable-find "tac"))
+ (should-parse '(eshell-named-command "sh" (list "-c" "tac <temp")))
+ (with-substitute-for-temp
+ (with-temp-buffer (insert "bar\nbaz\n") (write-file temp))
+ (eshell-match-command-output input "baz\nbar")))
+
+(em-extpipe-tests--deftest em-extpipe-test-15 "echo \"bar\" *| cat"
+ (skip-unless (executable-find "cat"))
+ (should-parse
+ '(eshell-named-command "sh" (list "-c" "echo \"bar\" | cat")))
+ (cl-letf (((symbol-function 'eshell/cat)
+ (lambda (&rest _args) (eshell-print "nonsense"))))
+ (eshell-match-command-output input "bar")
+ (eshell-match-command-output "echo \"bar\" | cat" "nonsense")))
+
+(em-extpipe-tests--deftest em-extpipe-test-16 "echo \"bar\" *| rev"
+ (skip-unless (executable-find "rev"))
+ (should-parse
+ '(eshell-named-command "sh" (list "-c" "echo \"bar\" | rev")))
+ (let ((eshell-prefer-lisp-functions t))
+ (cl-letf (((symbol-function 'rev)
+ (lambda (&rest _args) (eshell-print "nonsense"))))
+ (eshell-match-command-output input "rab")
+ (eshell-match-command-output "echo \"bar\" | rev" "nonsense"))))
+
+;; Confirm we don't break input of sharp-quoted symbols (Bug#53518).
+(em-extpipe-tests--deftest em-extpipe-test-17 "funcall #'upcase foo"
+ (eshell-match-command-output input "FOO"))
+
+;;; em-extpipe-tests.el ends here
diff --git a/test/lisp/eshell/em-glob-tests.el b/test/lisp/eshell/em-glob-tests.el
new file mode 100644
index 00000000000..b733be35d9a
--- /dev/null
+++ b/test/lisp/eshell/em-glob-tests.el
@@ -0,0 +1,197 @@
+;;; em-glob-tests.el --- em-glob test suite -*- lexical-binding:t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Tests for Eshell's glob expansion.
+
+;;; Code:
+
+(require 'ert)
+(require 'em-glob)
+
+(defmacro with-fake-files (files &rest body)
+ "Evaluate BODY forms, pretending that FILES exist on the filesystem.
+FILES is a list of file names that should be reported as
+appropriate by `file-name-all-completions'. Any file name
+component ending in \"symlink\" is treated as a symbolic link."
+ (declare (indent 1))
+ `(cl-letf (((symbol-function 'file-name-all-completions)
+ (lambda (file directory)
+ (cl-assert (string= file ""))
+ (setq directory (expand-file-name directory))
+ `("./" "../"
+ ,@(delete-dups
+ (remq nil
+ (mapcar
+ (lambda (file)
+ (setq file (expand-file-name file))
+ (when (string-prefix-p directory file)
+ (replace-regexp-in-string
+ "/.*" "/"
+ (substring file (length directory)))))
+ ,files))))))
+ ((symbol-function 'file-symlink-p)
+ (lambda (file)
+ (string-suffix-p "symlink" file))))
+ ,@body))
+
+;;; Tests:
+
+(ert-deftest em-glob-test/match-any-string ()
+ "Test that \"*\" pattern matches any string."
+ (with-fake-files '("a.el" "b.el" "c.txt" "dir/a.el")
+ (should (equal (eshell-extended-glob "*.el")
+ '("a.el" "b.el")))))
+
+(ert-deftest em-glob-test/match-any-directory ()
+ "Test that \"*/\" pattern matches any directory."
+ (with-fake-files '("a.el" "b.el" "dir/a.el" "dir/sub/a.el" "symlink/")
+ (should (equal (eshell-extended-glob "*/")
+ '("dir/" "symlink/")))))
+
+(ert-deftest em-glob-test/match-any-character ()
+ "Test that \"?\" pattern matches any character."
+ (with-fake-files '("a.el" "b.el" "ccc.el" "d.txt" "dir/a.el")
+ (should (equal (eshell-extended-glob "?.el")
+ '("a.el" "b.el")))))
+
+(ert-deftest em-glob-test/match-recursive ()
+ "Test that \"**/\" recursively matches directories."
+ (with-fake-files '("a.el" "b.el" "ccc.el" "d.txt" "dir/a.el" "dir/sub/a.el"
+ "dir/symlink/a.el" "symlink/a.el" "symlink/sub/a.el")
+ (should (equal (eshell-extended-glob "**/a.el")
+ '("a.el" "dir/a.el" "dir/sub/a.el")))
+ (should (equal (eshell-extended-glob "**/")
+ '("dir/" "dir/sub/")))))
+
+(ert-deftest em-glob-test/match-recursive-follow-symlinks ()
+ "Test that \"***/\" recursively matches directories, following symlinks."
+ (with-fake-files '("a.el" "b.el" "ccc.el" "d.txt" "dir/a.el" "dir/sub/a.el"
+ "dir/symlink/a.el" "symlink/a.el" "symlink/sub/a.el")
+ (should (equal (eshell-extended-glob "***/a.el")
+ '("a.el" "dir/a.el" "dir/sub/a.el" "dir/symlink/a.el"
+ "symlink/a.el" "symlink/sub/a.el")))
+ (should (equal (eshell-extended-glob "***/")
+ '("dir/" "dir/sub/" "dir/symlink/" "symlink/"
+ "symlink/sub/")))))
+
+(ert-deftest em-glob-test/match-recursive-mixed ()
+ "Test combination of \"**/\" and \"***/\"."
+ (with-fake-files '("dir/a.el" "dir/sub/a.el" "dir/sub2/a.el"
+ "dir/symlink/a.el" "dir/sub/symlink/a.el" "symlink/a.el"
+ "symlink/sub/a.el" "symlink/sub/symlink/a.el")
+ (should (equal (eshell-extended-glob "**/sub/***/a.el")
+ '("dir/sub/a.el" "dir/sub/symlink/a.el")))
+ (should (equal (eshell-extended-glob "***/sub/**/a.el")
+ '("dir/sub/a.el" "symlink/sub/a.el")))))
+
+(ert-deftest em-glob-test/match-character-set-individual ()
+ "Test \"[...]\" for individual characters."
+ (with-fake-files '("a.el" "b.el" "c.el" "d.el" "dir/a.el")
+ (should (equal (eshell-extended-glob "[ab].el")
+ '("a.el" "b.el")))
+ (should (equal (eshell-extended-glob "[^ab].el")
+ '("c.el" "d.el")))))
+
+(ert-deftest em-glob-test/match-character-set-range ()
+ "Test \"[...]\" for character ranges."
+ (with-fake-files '("a.el" "b.el" "c.el" "d.el" "dir/a.el")
+ (should (equal (eshell-extended-glob "[a-c].el")
+ '("a.el" "b.el" "c.el")))
+ (should (equal (eshell-extended-glob "[^a-c].el")
+ '("d.el")))))
+
+(ert-deftest em-glob-test/match-character-set-class ()
+ "Test \"[...]\" for character classes."
+ (with-fake-files '("1.el" "a.el" "b.el" "c.el" "dir/a.el")
+ (should (equal (eshell-extended-glob "[[:alpha:]].el")
+ '("a.el" "b.el" "c.el")))
+ (should (equal (eshell-extended-glob "[^[:alpha:]].el")
+ '("1.el")))))
+
+(ert-deftest em-glob-test/match-character-set-mixed ()
+ "Test \"[...]\" with multiple kinds of members at once."
+ (with-fake-files '("1.el" "a.el" "b.el" "c.el" "d.el" "dir/a.el")
+ (should (equal (eshell-extended-glob "[ac-d[:digit:]].el")
+ '("1.el" "a.el" "c.el" "d.el")))
+ (should (equal (eshell-extended-glob "[^ac-d[:digit:]].el")
+ '("b.el")))))
+
+(ert-deftest em-glob-test/match-group-alternative ()
+ "Test \"(x|y)\" matches either \"x\" or \"y\"."
+ (with-fake-files '("em-alias.el" "em-banner.el" "esh-arg.el" "misc.el"
+ "test/em-xtra.el")
+ (should (equal (eshell-extended-glob "e(m|sh)-*.el")
+ '("em-alias.el" "em-banner.el" "esh-arg.el")))))
+
+(ert-deftest em-glob-test/match-n-or-more-characters ()
+ "Test that \"x#\" and \"x#\" match zero or more instances of \"x\"."
+ (with-fake-files '("h.el" "ha.el" "hi.el" "hii.el" "dir/hi.el")
+ (should (equal (eshell-extended-glob "hi#.el")
+ '("h.el" "hi.el" "hii.el")))
+ (should (equal (eshell-extended-glob "hi##.el")
+ '("hi.el" "hii.el")))))
+
+(ert-deftest em-glob-test/match-n-or-more-groups ()
+ "Test that \"(x)#\" and \"(x)#\" match zero or more instances of \"(x)\"."
+ (with-fake-files '("h.el" "ha.el" "hi.el" "hii.el" "dir/hi.el")
+ (should (equal (eshell-extended-glob "hi#.el")
+ '("h.el" "hi.el" "hii.el")))
+ (should (equal (eshell-extended-glob "hi##.el")
+ '("hi.el" "hii.el")))))
+
+(ert-deftest em-glob-test/match-n-or-more-character-sets ()
+ "Test that \"[x]#\" and \"[x]#\" match zero or more instances of \"[x]\"."
+ (with-fake-files '("w.el" "wh.el" "wha.el" "whi.el" "whaha.el" "dir/wha.el")
+ (should (equal (eshell-extended-glob "w[ah]#.el")
+ '("w.el" "wh.el" "wha.el" "whaha.el")))
+ (should (equal (eshell-extended-glob "w[ah]##.el")
+ '("wh.el" "wha.el" "whaha.el")))))
+
+(ert-deftest em-glob-test/match-x-but-not-y ()
+ "Test that \"x~y\" matches \"x\" but not \"y\"."
+ (with-fake-files '("1" "12" "123" "42" "dir/1")
+ (should (equal (eshell-extended-glob "[[:digit:]]##~4?")
+ '("1" "12" "123")))))
+
+(ert-deftest em-glob-test/match-dot-files ()
+ "Test that dot files are matched correctly."
+ (with-fake-files '("foo.el" ".emacs")
+ (should (equal (eshell-extended-glob ".*")
+ '("../" "./" ".emacs")))
+ (let (eshell-glob-include-dot-dot)
+ (should (equal (eshell-extended-glob ".*")
+ '(".emacs"))))
+ (let ((eshell-glob-include-dot-files t))
+ (should (equal (eshell-extended-glob "*")
+ '("../" "./" ".emacs" "foo.el")))
+ (let (eshell-glob-include-dot-dot)
+ (should (equal (eshell-extended-glob "*")
+ '(".emacs" "foo.el")))))))
+
+(ert-deftest em-glob-test/no-matches ()
+ "Test behavior when a glob fails to match any files."
+ (with-fake-files '("foo.el" "bar.el")
+ (should (equal (eshell-extended-glob "*.txt")
+ "*.txt"))
+ (let ((eshell-error-if-no-glob t))
+ (should-error (eshell-extended-glob "*.txt")))))
+
+;; em-glob-tests.el ends here
diff --git a/test/lisp/eshell/em-hist-tests.el b/test/lisp/eshell/em-hist-tests.el
new file mode 100644
index 00000000000..634e9819839
--- /dev/null
+++ b/test/lisp/eshell/em-hist-tests.el
@@ -0,0 +1,38 @@
+;;; em-hist-tests.el --- em-hist test suite -*- lexical-binding:t -*-
+
+;; Copyright (C) 2017-2022 Free Software Foundation, Inc.
+
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert)
+(require 'ert-x)
+(require 'em-hist)
+
+(ert-deftest eshell-write-readonly-history ()
+ "Test that having read-only strings in history is okay."
+ (ert-with-temp-file histfile
+ (let ((eshell-history-ring (make-ring 2)))
+ (ring-insert eshell-history-ring
+ (propertize "echo foo" 'read-only t))
+ (ring-insert eshell-history-ring
+ (propertize "echo bar" 'read-only t))
+ (eshell-write-history histfile))))
+
+(provide 'em-hist-test)
+
+;;; em-hist-tests.el ends here
diff --git a/test/lisp/eshell/em-ls-tests.el b/test/lisp/eshell/em-ls-tests.el
index 35d6171400f..272280e81c7 100644
--- a/test/lisp/eshell/em-ls-tests.el
+++ b/test/lisp/eshell/em-ls-tests.el
@@ -1,6 +1,6 @@
-;;; tests/em-ls-tests.el --- em-ls test suite
+;;; em-ls-tests.el --- em-ls test suite -*- lexical-binding:t -*-
-;; Copyright (C) 2017 Free Software Foundation, Inc.
+;; Copyright (C) 2017-2022 Free Software Foundation, Inc.
;; Author: Tino Calancha <tino.calancha@gmail.com>
@@ -25,29 +25,30 @@
;;; Code:
(require 'ert)
+(require 'ert-x)
(require 'em-ls)
+(require 'dired)
(ert-deftest em-ls-test-bug27631 ()
"Test for https://debbugs.gnu.org/27631 ."
- (let* ((dir (make-temp-file "bug27631" 'dir))
- (dir1 (expand-file-name "dir1" dir))
- (dir2 (expand-file-name "dir2" dir))
- (default-directory dir)
- (orig eshell-ls-use-in-dired)
- buf)
- (unwind-protect
- (progn
- (customize-set-value 'eshell-ls-use-in-dired t)
- (make-directory dir1)
- (make-directory dir2)
- (with-temp-file (expand-file-name "a.txt" dir1))
- (with-temp-file (expand-file-name "b.txt" dir2))
- (setq buf (dired (expand-file-name "dir*/*.txt" dir)))
- (dired-toggle-marks)
- (should (cdr (dired-get-marked-files))))
- (customize-set-variable 'eshell-ls-use-in-dired orig)
- (delete-directory dir 'recursive)
- (when (buffer-live-p buf) (kill-buffer buf)))))
+ (ert-with-temp-directory dir
+ (let* ((dir1 (expand-file-name "dir1" dir))
+ (dir2 (expand-file-name "dir2" dir))
+ (default-directory dir)
+ (orig eshell-ls-use-in-dired)
+ buf)
+ (unwind-protect
+ (progn
+ (customize-set-value 'eshell-ls-use-in-dired t)
+ (make-directory dir1)
+ (make-directory dir2)
+ (with-temp-file (expand-file-name "a.txt" dir1))
+ (with-temp-file (expand-file-name "b.txt" dir2))
+ (setq buf (dired (expand-file-name "dir*/*.txt" dir)))
+ (dired-toggle-marks)
+ (should (cdr (dired-get-marked-files))))
+ (customize-set-variable 'eshell-ls-use-in-dired orig)
+ (when (buffer-live-p buf) (kill-buffer buf))))))
(ert-deftest em-ls-test-bug27817 ()
"Test for https://debbugs.gnu.org/27817 ."
@@ -77,6 +78,11 @@
(ert-deftest em-ls-test-bug27844 ()
"Test for https://debbugs.gnu.org/27844 ."
+ ;; FIXME: it would be better to use something other than source-directory
+ ;; in this test.
+ (skip-unless (and source-directory
+ (file-exists-p
+ (expand-file-name "lisp/subr.el" source-directory))))
(let ((orig eshell-ls-use-in-dired)
(dired-use-ls-dired 'unspecified)
buf insert-directory-program)
@@ -87,7 +93,14 @@
(dired-toggle-marks)
(should (cdr (dired-get-marked-files)))
(kill-buffer buf)
- (setq buf (dired (expand-file-name "lisp/subr.el" source-directory)))
+ ;; Eshell's default format duplicates the year for non-recent files,
+ ;; eg "2015-05-06 2015", which doesn't make a lot of sense,
+ ;; and causes this portion of the test to fail if subr.el
+ ;; is non-recent (eg if building from a tarfile unpacked
+ ;; with a fixed early timestamp for reproducibility). Bug#33734.
+ (let ((eshell-ls-date-format "%b %e"))
+ (setq buf (dired (expand-file-name "lisp/subr.el"
+ source-directory))))
(should (looking-at "subr\\.el")))
(customize-set-variable 'eshell-ls-use-in-dired orig)
(and (buffer-live-p buf) (kill-buffer)))))
diff --git a/test/lisp/eshell/em-pred-tests.el b/test/lisp/eshell/em-pred-tests.el
new file mode 100644
index 00000000000..0d6351ec826
--- /dev/null
+++ b/test/lisp/eshell/em-pred-tests.el
@@ -0,0 +1,566 @@
+;;; em-pred-tests.el --- em-pred test suite -*- lexical-binding:t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Tests for Eshell's argument predicates/modifiers.
+
+;;; Code:
+
+(require 'ert)
+(require 'esh-mode)
+(require 'eshell)
+(require 'em-glob)
+(require 'em-pred)
+
+(require 'eshell-tests-helpers
+ (expand-file-name "eshell-tests-helpers"
+ (file-name-directory (or load-file-name
+ default-directory))))
+
+(defvar eshell-test-value nil)
+
+(defun eshell-eval-predicate (initial-value predicate)
+ "Evaluate PREDICATE on INITIAL-VALUE, returning the result.
+PREDICATE is an Eshell argument predicate/modifier."
+ (let ((eshell-test-value initial-value))
+ (ignore-errors
+ (eshell-test-command-result
+ (format "echo $eshell-test-value(%s)" predicate)))))
+
+(defun eshell-parse-file-name-attributes (file)
+ "Parse a fake FILE name to determine its attributes.
+Fake file names are file names beginning with \"/fake/\". This
+allows defining file names for fake files with various properties
+to query via predicates. Attributes are written as a
+comma-separate list of ATTR=VALUE pairs as the file's base name,
+like:
+
+ /fake/type=-,modes=0755.el
+
+The following attributes are recognized:
+
+ * \"type\": A single character describing the file type;
+ accepts the same values as the first character of the file
+ modes in `ls -l'.
+ * \"modes\": The file's permission modes, in octal.
+ * \"links\": The number of links to this file.
+ * \"uid\": The UID of the file's owner.
+ * \"gid\": The UID of the file's group.
+ * \"atime\": The time the file was last accessed, in seconds
+ since the UNIX epoch.
+ * \"mtime\": As \"atime\", but for modification time.
+ * \"ctime\": As \"atime\", but for inode change time.
+ * \"size\": The file's size in bytes."
+ (mapcar (lambda (i)
+ (pcase (split-string i "=")
+ (`("modes" ,modes)
+ (cons 'modes (string-to-number modes 8)))
+ (`(,(and (or "links" "uid" "gid" "size") key) ,value)
+ (cons (intern key) (string-to-number value)))
+ (`(,(and (or "atime" "mtime" "ctime") key) ,value)
+ (cons (intern key) (time-convert (string-to-number value) t)))
+ (`(,key ,value)
+ (cons (intern key) value))
+ (_ (error "invalid format %S" i))))
+ (split-string (file-name-base file) ",")))
+
+(defmacro eshell-partial-let-func (overrides &rest body)
+ "Temporarily bind to FUNCTION-NAMEs and evaluate BODY.
+This is roughly analogous to advising functions, but only does so
+while BODY is executing, and only calls NEW-FUNCTION if its first
+argument is a string beginning with \"/fake/\".
+
+This allows selectively overriding functions to test file
+properties with fake files without altering the functions'
+behavior for real files.
+
+\(fn ((FUNCTION-NAME NEW-FUNCTION) ...) BODY...)"
+ (declare (indent 1))
+ `(cl-letf
+ ,(mapcar
+ (lambda (override)
+ `((symbol-function #',(car override))
+ (let ((orig-function (symbol-function #',(car override))))
+ (lambda (file &rest rest)
+ (apply
+ (if (and (stringp file) (string-prefix-p "/fake/" file))
+ ,(cadr override)
+ orig-function)
+ file rest)))))
+ overrides)
+ ,@body))
+
+(defmacro eshell-with-file-attributes-from-name (&rest body)
+ "Temporarily override file attribute functions and evaluate BODY."
+ (declare (indent 0))
+ `(eshell-partial-let-func
+ ((file-attributes
+ (lambda (file &optional _id-format)
+ (let ((attrs (eshell-parse-file-name-attributes file)))
+ (list (equal (alist-get 'type attrs) "d")
+ (or (alist-get 'links attrs) 1)
+ (or (alist-get 'uid attrs) 0)
+ (or (alist-get 'gid attrs) 0)
+ (or (alist-get 'atime attrs) nil)
+ (or (alist-get 'mtime attrs) nil)
+ (or (alist-get 'ctime attrs) nil)
+ (or (alist-get 'size attrs) 0)
+ (format "%s---------" (or (alist-get 'type attrs) "-"))
+ nil 0 0))))
+ (file-modes
+ (lambda (file _nofollow)
+ (let ((attrs (eshell-parse-file-name-attributes file)))
+ (or (alist-get 'modes attrs) 0))))
+ (file-exists-p #'always)
+ (file-regular-p
+ (lambda (file)
+ (let ((attrs (eshell-parse-file-name-attributes file)))
+ (member (or (alist-get 'type attrs) "-") '("-" "l")))))
+ (file-symlink-p
+ (lambda (file)
+ (let ((attrs (eshell-parse-file-name-attributes file)))
+ (equal (alist-get 'type attrs) "l"))))
+ (file-executable-p
+ (lambda (file)
+ (let ((attrs (eshell-parse-file-name-attributes file)))
+ ;; For simplicity, just return whether the file is
+ ;; world-executable.
+ (= (logand (or (alist-get 'modes attrs) 0) 1) 1)))))
+ ,@body))
+
+;;; Tests:
+
+
+;; Argument predicates
+
+(ert-deftest em-pred-test/predicate-file-types ()
+ "Test file type predicates."
+ (eshell-with-file-attributes-from-name
+ (let ((files (mapcar (lambda (i) (format "/fake/type=%s" i))
+ '("b" "c" "d/" "p" "s" "l" "-"))))
+ (should (equal (eshell-eval-predicate files "%")
+ '("/fake/type=b" "/fake/type=c")))
+ (should (equal (eshell-eval-predicate files "%b") '("/fake/type=b")))
+ (should (equal (eshell-eval-predicate files "%c") '("/fake/type=c")))
+ (should (equal (eshell-eval-predicate files "/") '("/fake/type=d/")))
+ (should (equal (eshell-eval-predicate files ".") '("/fake/type=-")))
+ (should (equal (eshell-eval-predicate files "p") '("/fake/type=p")))
+ (should (equal (eshell-eval-predicate files "=") '("/fake/type=s")))
+ (should (equal (eshell-eval-predicate files "@") '("/fake/type=l"))))))
+
+(ert-deftest em-pred-test/predicate-executable ()
+ "Test that \"*\" matches only regular, non-symlink executable files."
+ (eshell-with-file-attributes-from-name
+ (let ((files '("/fake/modes=0777" "/fake/modes=0666"
+ "/fake/type=d,modes=0777" "/fake/type=l,modes=0777")))
+ (should (equal (eshell-eval-predicate files "*")
+ '("/fake/modes=0777"))))))
+
+(defmacro em-pred-test--file-modes-deftest (name mode-template predicates
+ &optional docstring)
+ "Define NAME as a file-mode test.
+MODE-TEMPLATE is a format string to convert an integer from 0 to
+7 to an octal file mode. PREDICATES is a list of strings for the
+read, write, and execute predicates to query the file's modes."
+ (declare (indent 4) (doc-string 4))
+ `(ert-deftest ,name ()
+ ,docstring
+ (eshell-with-file-attributes-from-name
+ (let ((file-template (concat "/fake/modes=" ,mode-template)))
+ (cl-flet ((make-files (perms)
+ (mapcar (lambda (i) (format file-template i))
+ perms)))
+ (pcase-let ((files (make-files (number-sequence 0 7)))
+ (`(,read ,write ,exec) ,predicates))
+ (should (equal (eshell-eval-predicate files read)
+ (make-files '(4 5 6 7))))
+ (should (equal (eshell-eval-predicate files (concat "^" read))
+ (make-files '(0 1 2 3))))
+ (should (equal (eshell-eval-predicate files write)
+ (make-files '(2 3 6 7))))
+ (should (equal (eshell-eval-predicate files (concat "^" write))
+ (make-files '(0 1 4 5))))
+ (should (equal (eshell-eval-predicate files exec)
+ (make-files '(1 3 5 7))))
+ (should (equal (eshell-eval-predicate files (concat "^" exec))
+ (make-files '(0 2 4 6))))))))))
+
+(em-pred-test--file-modes-deftest em-pred-test/predicate-file-modes-owner
+ "0%o00" '("r" "w" "x")
+ "Test predicates for file permissions for the owner.")
+
+(em-pred-test--file-modes-deftest em-pred-test/predicate-file-modes-group
+ "00%o0" '("A" "I" "E")
+ "Test predicates for file permissions for the group.")
+
+(em-pred-test--file-modes-deftest em-pred-test/predicate-file-modes-world
+ "000%o" '("R" "W" "X")
+ "Test predicates for file permissions for the world.")
+
+(em-pred-test--file-modes-deftest em-pred-test/predicate-file-modes-flags
+ "%o000" '("s" "S" "t")
+ "Test predicates for \"s\" (setuid), \"S\" (setgid), and \"t\" (sticky).")
+
+(ert-deftest em-pred-test/predicate-effective-uid ()
+ "Test that \"U\" matches files owned by the effective UID."
+ (eshell-with-file-attributes-from-name
+ (cl-letf (((symbol-function 'user-uid) (lambda () 1)))
+ (let ((files '("/fake/uid=1" "/fake/uid=2")))
+ (should (equal (eshell-eval-predicate files "U")
+ '("/fake/uid=1")))))))
+
+(ert-deftest em-pred-test/predicate-effective-gid ()
+ "Test that \"G\" matches files owned by the effective GID."
+ (eshell-with-file-attributes-from-name
+ (cl-letf (((symbol-function 'group-gid) (lambda () 1)))
+ (let ((files '("/fake/gid=1" "/fake/gid=2")))
+ (should (equal (eshell-eval-predicate files "G")
+ '("/fake/gid=1")))))))
+
+(ert-deftest em-pred-test/predicate-links ()
+ "Test that \"l\" filters by number of links."
+ (eshell-with-file-attributes-from-name
+ (let ((files '("/fake/links=1" "/fake/links=2" "/fake/links=3")))
+ (should (equal (eshell-eval-predicate files "l1")
+ '("/fake/links=1")))
+ (should (equal (eshell-eval-predicate files "l+1")
+ '("/fake/links=2" "/fake/links=3")))
+ (should (equal (eshell-eval-predicate files "l-3")
+ '("/fake/links=1" "/fake/links=2"))))))
+
+(ert-deftest em-pred-test/predicate-uid ()
+ "Test that \"u\" filters by UID/user name."
+ (eshell-with-file-attributes-from-name
+ (let ((files '("/fake/uid=1" "/fake/uid=2"))
+ (user-names '("root" "one" "two")))
+ (should (equal (eshell-eval-predicate files "u1")
+ '("/fake/uid=1")))
+ (cl-letf (((symbol-function 'eshell-user-id)
+ (lambda (name) (seq-position user-names name))))
+ (should (equal (eshell-eval-predicate files "u'one'")
+ '("/fake/uid=1")))))))
+
+(ert-deftest em-pred-test/predicate-gid ()
+ "Test that \"g\" filters by GID/group name."
+ (eshell-with-file-attributes-from-name
+ (let ((files '("/fake/gid=1" "/fake/gid=2"))
+ (group-names '("root" "one" "two")))
+ (should (equal (eshell-eval-predicate files "g1")
+ '("/fake/gid=1")))
+ (cl-letf (((symbol-function 'eshell-group-id)
+ (lambda (name) (seq-position group-names name))))
+ (should (equal (eshell-eval-predicate files "g'one'")
+ '("/fake/gid=1")))))))
+
+(defmacro em-pred-test--time-deftest (name file-attribute predicate
+ &optional docstring)
+ "Define NAME as a file-time test.
+FILE-ATTRIBUTE is the file's attribute to set (e.g. \"atime\").
+PREDICATE is the predicate used to query that attribute."
+ (declare (indent 4) (doc-string 4))
+ `(ert-deftest ,name ()
+ ,docstring
+ (eshell-with-file-attributes-from-name
+ (cl-flet ((make-file (time)
+ (format "/fake/%s=%d" ,file-attribute time)))
+ (let* ((now (time-convert nil 'integer))
+ (yesterday (- now 86400))
+ (files (mapcar #'make-file (list now yesterday))))
+ ;; Test comparison against a number of days.
+ (should (equal (eshell-eval-predicate
+ files (concat ,predicate "-1"))
+ (mapcar #'make-file (list now))))
+ (should (equal (eshell-eval-predicate
+ files (concat ,predicate "+1"))
+ (mapcar #'make-file (list yesterday))))
+ (should (equal (eshell-eval-predicate
+ files (concat ,predicate "+2"))
+ nil))
+ ;; Test comparison against a number of hours.
+ (should (equal (eshell-eval-predicate
+ files (concat ,predicate "h-1"))
+ (mapcar #'make-file (list now))))
+ (should (equal (eshell-eval-predicate
+ files (concat ,predicate "h+1"))
+ (mapcar #'make-file (list yesterday))))
+ (should (equal (eshell-eval-predicate
+ files (concat ,predicate "+48"))
+ nil))
+ ;; Test comparison against another file.
+ (should (equal (eshell-eval-predicate
+ files (format "%s-'%s'" ,predicate (make-file now)))
+ nil))
+ (should (equal (eshell-eval-predicate
+ files (format "%s+'%s'" ,predicate (make-file now)))
+ (mapcar #'make-file (list yesterday)))))))))
+
+(em-pred-test--time-deftest em-pred-test/predicate-access-time
+ "atime" "a"
+ "Test that \"a\" filters by access time.")
+
+(em-pred-test--time-deftest em-pred-test/predicate-modification-time
+ "mtime" "m"
+ "Test that \"m\" filters by change time.")
+
+(em-pred-test--time-deftest em-pred-test/predicate-change-time
+ "ctime" "c"
+ "Test that \"c\" filters by change time.")
+
+(ert-deftest em-pred-test/predicate-size ()
+ "Test that \"L\" filters by file size."
+ (eshell-with-file-attributes-from-name
+ (let ((files '("/fake/size=0"
+ ;; 1 and 2 KiB.
+ "/fake/size=1024" "/fake/size=2048"
+ ;; 1 and 2 MiB.
+ "/fake/size=1048576" "/fake/size=2097152")))
+ ;; Size in bytes.
+ (should (equal (eshell-eval-predicate files "L2048")
+ '("/fake/size=2048")))
+ (should (equal (eshell-eval-predicate files "L+2048")
+ '("/fake/size=1048576" "/fake/size=2097152")))
+ (should (equal (eshell-eval-predicate files "L-2048")
+ '("/fake/size=0" "/fake/size=1024")))
+ ;; Size in blocks.
+ (should (equal (eshell-eval-predicate files "Lp4")
+ '("/fake/size=2048")))
+ (should (equal (eshell-eval-predicate files "Lp+4")
+ '("/fake/size=1048576" "/fake/size=2097152")))
+ (should (equal (eshell-eval-predicate files "Lp-4")
+ '("/fake/size=0" "/fake/size=1024")))
+ ;; Size in KiB.
+ (should (equal (eshell-eval-predicate files "Lk2")
+ '("/fake/size=2048")))
+ (should (equal (eshell-eval-predicate files "Lk+2")
+ '("/fake/size=1048576" "/fake/size=2097152")))
+ (should (equal (eshell-eval-predicate files "Lk-2")
+ '("/fake/size=0" "/fake/size=1024")))
+ ;; Size in MiB.
+ (should (equal (eshell-eval-predicate files "LM1")
+ '("/fake/size=1048576")))
+ (should (equal (eshell-eval-predicate files "LM+1")
+ '("/fake/size=2097152")))
+ (should (equal (eshell-eval-predicate files "LM-1")
+ '("/fake/size=0" "/fake/size=1024" "/fake/size=2048"))))))
+
+
+;; Argument modifiers
+
+(ert-deftest em-pred-test/modifier-eval ()
+ "Test that \":E\" re-evaluates the value."
+ (should (equal (eshell-eval-predicate "${echo hi}" ":E") "hi"))
+ (should (equal (eshell-eval-predicate
+ '("${echo hi}" "$(upcase \"bye\")") ":E")
+ '("hi" "BYE"))))
+
+(ert-deftest em-pred-test/modifier-downcase ()
+ "Test that \":L\" downcases values."
+ (should (equal (eshell-eval-predicate "FOO" ":L") "foo"))
+ (should (equal (eshell-eval-predicate '("FOO" "BAR") ":L")
+ '("foo" "bar"))))
+
+(ert-deftest em-pred-test/modifier-upcase ()
+ "Test that \":U\" upcases values."
+ (should (equal (eshell-eval-predicate "foo" ":U") "FOO"))
+ (should (equal (eshell-eval-predicate '("foo" "bar") ":U")
+ '("FOO" "BAR"))))
+
+(ert-deftest em-pred-test/modifier-capitalize ()
+ "Test that \":C\" capitalizes values."
+ (should (equal (eshell-eval-predicate "foo bar" ":C") "Foo Bar"))
+ (should (equal (eshell-eval-predicate '("foo bar" "baz") ":C")
+ '("Foo Bar" "Baz"))))
+
+(ert-deftest em-pred-test/modifier-dirname ()
+ "Test that \":h\" returns the dirname."
+ (should (equal (eshell-eval-predicate "/path/to/file.el" ":h") "/path/to/"))
+ (should (equal (eshell-eval-predicate
+ '("/path/to/file.el" "/other/path/") ":h")
+ '("/path/to/" "/other/path/"))))
+
+(ert-deftest em-pred-test/modifier-basename ()
+ "Test that \":t\" returns the basename."
+ (should (equal (eshell-eval-predicate "/path/to/file.el" ":t") "file.el"))
+ (should (equal (eshell-eval-predicate
+ '("/path/to/file.el" "/other/path/") ":t")
+ '("file.el" ""))))
+
+(ert-deftest em-pred-test/modifier-extension ()
+ "Test that \":e\" returns the extension."
+ (should (equal (eshell-eval-predicate "/path/to/file.el" ":e") "el"))
+ (should (equal (eshell-eval-predicate
+ '("/path/to/file.el" "/other/path/") ":e")
+ '("el" nil))))
+
+(ert-deftest em-pred-test/modifier-sans-extension ()
+ "Test that \":r\" returns the file name san extension."
+ (should (equal (eshell-eval-predicate "/path/to/file.el" ":r")
+ "/path/to/file"))
+ (should (equal (eshell-eval-predicate
+ '("/path/to/file.el" "/other/path/") ":r")
+ '("/path/to/file" "/other/path/"))))
+
+(ert-deftest em-pred-test/modifier-quote ()
+ "Test that \":q\" quotes arguments."
+ (should (equal-including-properties
+ (eshell-eval-predicate '("foo" "bar") ":q")
+ (list (eshell-escape-arg "foo") (eshell-escape-arg "bar")))))
+
+(ert-deftest em-pred-test/modifier-substitute ()
+ "Test that \":s/PAT/REP/\" replaces PAT with REP once."
+ (should (equal (eshell-eval-predicate "bar" ":s/a/*/") "b*r"))
+ (should (equal (eshell-eval-predicate "bar" ":s|a|*|") "b*r"))
+ (should (equal (eshell-eval-predicate "bar" ":s{a}{*}") "b*r"))
+ (should (equal (eshell-eval-predicate "bar" ":s{a}'*'") "b*r"))
+ (should (equal (eshell-eval-predicate '("foo" "bar" "baz") ":s/[ao]/*/")
+ '("f*o" "b*r" "b*z")))
+ (should (equal (eshell-eval-predicate '("foo" "bar" "baz") ":s|[ao]|*|")
+ '("f*o" "b*r" "b*z"))))
+
+(ert-deftest em-pred-test/modifier-global-substitute ()
+ "Test that \":s/PAT/REP/\" replaces PAT with REP for all occurrences."
+ (should (equal (eshell-eval-predicate "foo" ":gs/a/*/") "foo"))
+ (should (equal (eshell-eval-predicate "foo" ":gs|a|*|") "foo"))
+ (should (equal (eshell-eval-predicate "bar" ":gs/a/*/") "b*r"))
+ (should (equal (eshell-eval-predicate "bar" ":gs|a|*|") "b*r"))
+ (should (equal (eshell-eval-predicate "foo" ":gs/o/O/") "fOO"))
+ (should (equal (eshell-eval-predicate '("foo" "bar" "baz") ":gs/[aeiou]/*/")
+ '("f**" "b*r" "b*z")))
+ (should (equal (eshell-eval-predicate '("foo" "bar" "baz") ":gs|[aeiou]|*|")
+ '("f**" "b*r" "b*z"))))
+
+(ert-deftest em-pred-test/modifier-include ()
+ "Test that \":i/PAT/\" filters elements to include only ones matching PAT."
+ (should (equal (eshell-eval-predicate "foo" ":i/a/") nil))
+ (should (equal (eshell-eval-predicate "bar" ":i/a/") "bar"))
+ (should (equal (eshell-eval-predicate '("foo" "bar" "baz") ":i/a/")
+ '("bar" "baz"))))
+
+(ert-deftest em-pred-test/modifier-exclude ()
+ "Test that \":x/PAT/\" filters elements to exclude any matching PAT."
+ (should (equal (eshell-eval-predicate "foo" ":x/a/") "foo"))
+ (should (equal (eshell-eval-predicate "bar" ":x/a/") nil))
+ (should (equal (eshell-eval-predicate '("foo" "bar" "baz") ":x/a/")
+ '("foo"))))
+
+(ert-deftest em-pred-test/modifier-split ()
+ "Test that \":S\" and \":S/PAT/\" split elements by spaces (or PAT)."
+ (should (equal (eshell-eval-predicate "foo bar baz" ":S")
+ '("foo" "bar" "baz")))
+ (should (equal (eshell-eval-predicate '("foo bar" "baz") ":S")
+ '(("foo" "bar") ("baz"))))
+ (should (equal (eshell-eval-predicate "foo-bar-baz" ":S/-/")
+ '("foo" "bar" "baz")))
+ (should (equal (eshell-eval-predicate '("foo-bar" "baz") ":S/-/")
+ '(("foo" "bar") ("baz")))))
+
+(ert-deftest em-pred-test/modifier-join ()
+ "Test that \":j\" and \":j/DELIM/\" join elements by spaces (or DELIM)."
+ (should (equal (eshell-eval-predicate "foo" ":j") "foo"))
+ (should (equal (eshell-eval-predicate '("foo" "bar" "baz") ":j")
+ "foo bar baz"))
+ (should (equal (eshell-eval-predicate "foo" ":j/-/") "foo"))
+ (should (equal (eshell-eval-predicate '("foo" "bar" "baz") ":j/-/")
+ "foo-bar-baz")))
+
+(ert-deftest em-pred-test/modifier-sort ()
+ "Test that \":o\" sorts elements in lexicographic order."
+ (should (equal (eshell-eval-predicate "foo" ":o") "foo"))
+ (should (equal (eshell-eval-predicate '("foo" "bar" "baz") ":o")
+ '("bar" "baz" "foo"))))
+
+(ert-deftest em-pred-test/modifier-sort-reverse ()
+ "Test that \":o\" sorts elements in reverse lexicographic order."
+ (should (equal (eshell-eval-predicate "foo" ":O") "foo"))
+ (should (equal (eshell-eval-predicate '("foo" "bar" "baz") ":O")
+ '("foo" "baz" "bar"))))
+
+(ert-deftest em-pred-test/modifier-unique ()
+ "Test that \":u\" filters out duplicate elements."
+ (should (equal (eshell-eval-predicate "foo" ":u") "foo"))
+ (should (equal (eshell-eval-predicate '("foo" "bar" "baz") ":u")
+ '("foo" "bar" "baz")))
+ (should (equal (eshell-eval-predicate '("foo" "bar" "baz" "foo") ":u")
+ '("foo" "bar" "baz"))))
+
+(ert-deftest em-pred-test/modifier-reverse ()
+ "Test that \":r\" reverses the order of elements."
+ (should (equal (eshell-eval-predicate "foo" ":R") "foo"))
+ (should (equal (eshell-eval-predicate '("foo" "bar" "baz") ":R")
+ '("baz" "bar" "foo"))))
+
+
+;; Miscellaneous
+
+(ert-deftest em-pred-test/combine-predicate-and-modifier ()
+ "Test combination of predicates and modifiers."
+ (eshell-with-file-attributes-from-name
+ (let ((files '("/fake/type=-.el" "/fake/type=-.txt" "/fake/type=s.el"
+ "/fake/subdir/type=-.el")))
+ (should (equal (eshell-eval-predicate files ".:e:u")
+ '("el" "txt"))))))
+
+(ert-deftest em-pred-test/predicate-delimiters ()
+ "Test various delimiter pairs with predicates and modifiers."
+ (dolist (delims eshell-pred-delimiter-pairs)
+ (eshell-with-file-attributes-from-name
+ (let ((files '("/fake/uid=1" "/fake/uid=2"))
+ (user-names '("root" "one" "two")))
+ (cl-letf (((symbol-function 'eshell-user-id)
+ (lambda (name) (seq-position user-names name))))
+ (should (equal (eshell-eval-predicate
+ files (format "u%cone%c" (car delims) (cdr delims)))
+ '("/fake/uid=1"))))))
+ (should (equal (eshell-eval-predicate
+ '("foo" "bar" "baz")
+ (format ":j%c-%c" (car delims) (cdr delims)))
+ "foo-bar-baz"))))
+
+(ert-deftest em-pred-test/predicate-escaping ()
+ "Test string escaping in predicate and modifier parameters."
+ ;; Escaping the delimiter should remove the backslash.
+ (should (equal (eshell-eval-predicate '("foo" "bar" "baz") ":j'\\''")
+ "foo'bar'baz"))
+ ;; Escaping a backlash should remove the first backslash.
+ (should (equal (eshell-eval-predicate '("foo" "bar" "baz") ":j'\\\\'")
+ "foo\\bar\\baz"))
+ ;; Escaping a different character should keep the backslash.
+ (should (equal (eshell-eval-predicate '("foo" "bar" "baz") ":j'\\\"'")
+ "foo\\\"bar\\\"baz")))
+
+(ert-deftest em-pred-test/no-matches ()
+ "Test behavior when a predicate fails to match any files."
+ (eshell-with-file-attributes-from-name
+ (let ((files '("/fake/modes=0666" "/fake/type=d,modes=0777"
+ "/fake/type=l,modes=0777")))
+ (should (equal (eshell-eval-predicate files "*") nil))
+ (let ((eshell-error-if-no-glob t))
+ ;; Don't signal an error if the original list is empty.
+ (should (equal (eshell-eval-predicate nil "*") nil))
+ ;; Ensure this signals an error. This test case is a bit
+ ;; clumsy, since `eshell-do-eval' makes it hard to catch
+ ;; errors otherwise.
+ (let ((modifiers (with-temp-eshell
+ (eshell-with-temp-command "*"
+ (eshell-parse-modifiers)))))
+ (should-error (eshell-apply-modifiers files (car modifiers)
+ (cdr modifiers) "*")))))))
+
+;; em-pred-tests.el ends here
diff --git a/test/lisp/eshell/em-script-tests.el b/test/lisp/eshell/em-script-tests.el
new file mode 100644
index 00000000000..b837d464ccd
--- /dev/null
+++ b/test/lisp/eshell/em-script-tests.el
@@ -0,0 +1,62 @@
+;;; em-script-tests.el --- em-script test suite -*- lexical-binding:t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Tests for Eshell's script module.
+
+;;; Code:
+
+(require 'ert)
+(require 'esh-mode)
+(require 'eshell)
+(require 'em-script)
+
+(require 'eshell-tests-helpers
+ (expand-file-name "eshell-tests-helpers"
+ (file-name-directory (or load-file-name
+ default-directory))))
+;;; Tests:
+
+(ert-deftest em-script-test/source-script ()
+ "Test sourcing script with no argumentss"
+ (ert-with-temp-file temp-file :text "echo hi"
+ (with-temp-eshell
+ (eshell-match-command-output (format "source %s" temp-file)
+ "hi\n"))))
+
+(ert-deftest em-script-test/source-script-arg-vars ()
+ "Test sourcing script with $0, $1, ... variables"
+ (ert-with-temp-file temp-file :text "printnl $0 \"$1 $2\""
+ (with-temp-eshell
+ (eshell-match-command-output (format "source %s one two" temp-file)
+ (format "%s\none two\n" temp-file)))))
+
+(ert-deftest em-script-test/source-script-all-args-var ()
+ "Test sourcing script with the $* variable"
+ (ert-with-temp-file temp-file :text "printnl $*"
+ (with-temp-eshell
+ (eshell-match-command-output (format "source %s" temp-file)
+ "\\`\\'")
+ (eshell-match-command-output (format "source %s a" temp-file)
+ "a\n")
+ (eshell-match-command-output (format "source %s a b c" temp-file)
+ "a\nb\nc\n"))))
+
+;; em-script-tests.el ends here
diff --git a/test/lisp/eshell/em-tramp-tests.el b/test/lisp/eshell/em-tramp-tests.el
new file mode 100644
index 00000000000..8969c1e2294
--- /dev/null
+++ b/test/lisp/eshell/em-tramp-tests.el
@@ -0,0 +1,88 @@
+;;; em-tramp-tests.el --- em-tramp test suite -*- lexical-binding:t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert)
+(require 'em-tramp)
+(require 'tramp)
+
+(ert-deftest em-tramp-test/su-default ()
+ "Test Eshell `su' command with no arguments."
+ (should (equal
+ (catch 'eshell-replace-command (eshell/su))
+ `(eshell-trap-errors
+ (eshell-named-command
+ "cd"
+ (list ,(format "/su:root@%s:%s"
+ tramp-default-host default-directory)))))))
+
+(ert-deftest em-tramp-test/su-user ()
+ "Test Eshell `su' command with USER argument."
+ (should (equal
+ (catch 'eshell-replace-command (eshell/su "USER"))
+ `(eshell-trap-errors
+ (eshell-named-command
+ "cd"
+ (list ,(format "/su:USER@%s:%s"
+ tramp-default-host default-directory)))))))
+
+(ert-deftest em-tramp-test/su-login ()
+ "Test Eshell `su' command with -/-l/--login option."
+ (dolist (args '(("--login")
+ ("-l")
+ ("-")))
+ (should (equal
+ (catch 'eshell-replace-command (apply #'eshell/su args))
+ `(eshell-trap-errors
+ (eshell-named-command
+ "cd"
+ (list ,(format "/su:root@%s:~/" tramp-default-host))))))))
+
+(defun mock-eshell-named-command (&rest args)
+ "Dummy function to test Eshell `sudo' command rewriting."
+ (list default-directory args))
+
+(ert-deftest em-tramp-test/sudo-basic ()
+ "Test Eshell `sudo' command with default user."
+ (cl-letf (((symbol-function 'eshell-named-command)
+ #'mock-eshell-named-command))
+ (should (equal
+ (catch 'eshell-external (eshell/sudo "echo" "hi"))
+ `(,(format "/sudo:root@%s:%s" tramp-default-host default-directory)
+ ("echo" ("hi")))))
+ (should (equal
+ (catch 'eshell-external (eshell/sudo "echo" "-u" "hi"))
+ `(,(format "/sudo:root@%s:%s" tramp-default-host default-directory)
+ ("echo" ("-u" "hi")))))))
+
+(ert-deftest em-tramp-test/sudo-user ()
+ "Test Eshell `sudo' command with specified user."
+ (cl-letf (((symbol-function 'eshell-named-command)
+ #'mock-eshell-named-command))
+ (should (equal
+ (catch 'eshell-external (eshell/sudo "-u" "USER" "echo" "hi"))
+ `(,(format "/sudo:USER@%s:%s" tramp-default-host default-directory)
+ ("echo" ("hi")))))
+ (should (equal
+ (catch 'eshell-external (eshell/sudo "-u" "USER" "echo" "-u" "hi"))
+ `(,(format "/sudo:USER@%s:%s" tramp-default-host default-directory)
+ ("echo" ("-u" "hi")))))))
+
+;;; em-tramp-tests.el ends here
diff --git a/test/lisp/eshell/esh-cmd-tests.el b/test/lisp/eshell/esh-cmd-tests.el
new file mode 100644
index 00000000000..92d785d7fdf
--- /dev/null
+++ b/test/lisp/eshell/esh-cmd-tests.el
@@ -0,0 +1,294 @@
+;;; esh-cmd-tests.el --- esh-cmd test suite -*- lexical-binding:t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Tests for Eshell's command invocation.
+
+;;; Code:
+
+(require 'ert)
+(require 'esh-mode)
+(require 'eshell)
+
+(require 'eshell-tests-helpers
+ (expand-file-name "eshell-tests-helpers"
+ (file-name-directory (or load-file-name
+ default-directory))))
+
+(defvar eshell-test-value nil)
+
+;;; Tests:
+
+
+;; Command invocation
+
+(ert-deftest esh-cmd-test/simple-command-result ()
+ "Test invocation with a simple command."
+ (eshell-command-result-equal "+ 1 2" 3))
+
+(ert-deftest esh-cmd-test/lisp-command ()
+ "Test invocation with an elisp command."
+ (eshell-command-result-equal "(+ 1 2)" 3))
+
+(ert-deftest esh-cmd-test/lisp-command-with-quote ()
+ "Test invocation with an elisp command containing a quote."
+ (eshell-command-result-equal "(eq 'foo nil)" nil))
+
+(ert-deftest esh-cmd-test/lisp-command-args ()
+ "Test invocation with elisp and trailing args.
+Test that trailing arguments outside the S-expression are
+ignored. e.g. \"(+ 1 2) 3\" => 3"
+ (eshell-command-result-equal "(+ 1 2) 3" 3))
+
+(ert-deftest esh-cmd-test/subcommand ()
+ "Test invocation with a simple subcommand."
+ (eshell-command-result-equal "{+ 1 2}" 3))
+
+(ert-deftest esh-cmd-test/subcommand-args ()
+ "Test invocation with a subcommand and trailing args.
+Test that trailing arguments outside the subcommand are ignored.
+e.g. \"{+ 1 2} 3\" => 3"
+ (eshell-command-result-equal "{+ 1 2} 3" 3))
+
+(ert-deftest esh-cmd-test/subcommand-lisp ()
+ "Test invocation with an elisp subcommand and trailing args.
+Test that trailing arguments outside the subcommand are ignored.
+e.g. \"{(+ 1 2)} 3\" => 3"
+ (eshell-command-result-equal "{(+ 1 2)} 3" 3))
+
+
+;; Lisp forms
+
+(ert-deftest esh-cmd-test/quoted-lisp-form ()
+ "Test parsing of a quoted Lisp form."
+ (eshell-command-result-equal "echo #'(1 2)" '(1 2)))
+
+(ert-deftest esh-cmd-test/backquoted-lisp-form ()
+ "Test parsing of a backquoted Lisp form."
+ (let ((eshell-test-value 42))
+ (eshell-command-result-equal "echo `(answer ,eshell-test-value)"
+ '(answer 42))))
+
+(ert-deftest esh-cmd-test/backquoted-lisp-form/splice ()
+ "Test parsing of a backquoted Lisp form using splicing."
+ (let ((eshell-test-value '(2 3)))
+ (eshell-command-result-equal "echo `(1 ,@eshell-test-value)"
+ '(1 2 3))))
+
+
+;; Logical operators
+
+(ert-deftest esh-cmd-test/and-operator ()
+ "Test logical && operator."
+ (skip-unless (executable-find "["))
+ (with-temp-eshell
+ (eshell-match-command-output "[ foo = foo ] && echo hi"
+ "hi\n")
+ (eshell-match-command-output "[ foo = bar ] && echo hi"
+ "\\`\\'")))
+
+(ert-deftest esh-cmd-test/or-operator ()
+ "Test logical || operator."
+ (skip-unless (executable-find "["))
+ (with-temp-eshell
+ (eshell-match-command-output "[ foo = foo ] || echo hi"
+ "\\`\\'")
+ (eshell-match-command-output "[ foo = bar ] || echo hi"
+ "hi\n")))
+
+
+;; Control flow statements
+
+(ert-deftest esh-cmd-test/for-loop ()
+ "Test invocation of a for loop."
+ (with-temp-eshell
+ (eshell-match-command-output "for i in 5 { echo $i }"
+ "5\n")))
+
+(ert-deftest esh-cmd-test/for-loop-list ()
+ "Test invocation of a for loop iterating over a list."
+ (with-temp-eshell
+ (eshell-match-command-output "for i in (list 1 2 (list 3 4)) { echo $i }"
+ "1\n2\n(3 4)\n")))
+
+(ert-deftest esh-cmd-test/for-loop-multiple-args ()
+ "Test invocation of a for loop iterating over multiple arguments."
+ (with-temp-eshell
+ (eshell-match-command-output "for i in 1 2 (list 3 4) { echo $i }"
+ "1\n2\n3\n4\n")))
+
+(ert-deftest esh-cmd-test/for-name-loop () ; bug#15231
+ "Test invocation of a for loop using `name'."
+ (let ((process-environment (cons "name" process-environment)))
+ (eshell-command-result-equal "for name in 3 { echo $name }"
+ 3)))
+
+(ert-deftest esh-cmd-test/for-name-shadow-loop () ; bug#15372
+ "Test invocation of a for loop using an env-var."
+ (let ((process-environment (cons "name=env-value" process-environment)))
+ (with-temp-eshell
+ (eshell-match-command-output
+ "echo $name; for name in 3 { echo $name }; echo $name"
+ "env-value\n3\nenv-value\n"))))
+
+(ert-deftest esh-cmd-test/while-loop ()
+ "Test invocation of a while loop."
+ (with-temp-eshell
+ (let ((eshell-test-value '(0 1 2)))
+ (eshell-match-command-output
+ (concat "while $eshell-test-value "
+ "{ setq eshell-test-value (cdr eshell-test-value) }")
+ "(1 2)\n(2)\n"))))
+
+(ert-deftest esh-cmd-test/while-loop-lisp-form ()
+ "Test invocation of a while loop using a Lisp form."
+ (with-temp-eshell
+ (let ((eshell-test-value 0))
+ (eshell-match-command-output
+ (concat "while (/= eshell-test-value 3) "
+ "{ setq eshell-test-value (1+ eshell-test-value) }")
+ "1\n2\n3\n"))))
+
+(ert-deftest esh-cmd-test/while-loop-ext-cmd ()
+ "Test invocation of a while loop using an external command."
+ (skip-unless (executable-find "["))
+ (with-temp-eshell
+ (let ((eshell-test-value 0))
+ (eshell-match-command-output
+ (concat "while {[ $eshell-test-value -ne 3 ]} "
+ "{ setq eshell-test-value (1+ eshell-test-value) }")
+ "1\n2\n3\n"))))
+
+(ert-deftest esh-cmd-test/until-loop ()
+ "Test invocation of an until loop."
+ (with-temp-eshell
+ (let ((eshell-test-value nil))
+ (eshell-match-command-output
+ (concat "until $eshell-test-value "
+ "{ setq eshell-test-value t }")
+ "t\n"))))
+
+(ert-deftest esh-cmd-test/until-loop-lisp-form ()
+ "Test invocation of an until loop using a Lisp form."
+ (skip-unless (executable-find "["))
+ (with-temp-eshell
+ (let ((eshell-test-value 0))
+ (eshell-match-command-output
+ (concat "until (= eshell-test-value 3) "
+ "{ setq eshell-test-value (1+ eshell-test-value) }")
+ "1\n2\n3\n"))))
+
+(ert-deftest esh-cmd-test/until-loop-ext-cmd ()
+ "Test invocation of an until loop using an external command."
+ (skip-unless (executable-find "["))
+ (with-temp-eshell
+ (let ((eshell-test-value 0))
+ (eshell-match-command-output
+ (concat "until {[ $eshell-test-value -eq 3 ]} "
+ "{ setq eshell-test-value (1+ eshell-test-value) }")
+ "1\n2\n3\n"))))
+
+(ert-deftest esh-cmd-test/if-statement ()
+ "Test invocation of an if statement."
+ (let ((eshell-test-value t))
+ (eshell-command-result-equal "if $eshell-test-value {echo yes}"
+ "yes"))
+ (let ((eshell-test-value nil))
+ (eshell-command-result-equal "if $eshell-test-value {echo yes}"
+ nil)))
+
+(ert-deftest esh-cmd-test/if-else-statement ()
+ "Test invocation of an if/else statement."
+ (let ((eshell-test-value t))
+ (eshell-command-result-equal "if $eshell-test-value {echo yes} {echo no}"
+ "yes"))
+ (let ((eshell-test-value nil))
+ (eshell-command-result-equal "if $eshell-test-value {echo yes} {echo no}"
+ "no")))
+
+(ert-deftest esh-cmd-test/if-else-statement-lisp-form ()
+ "Test invocation of an if/else statement using a Lisp form."
+ (eshell-command-result-equal "if (zerop 0) {echo yes} {echo no}"
+ "yes")
+ (eshell-command-result-equal "if (zerop 1) {echo yes} {echo no}"
+ "no")
+ (let ((debug-on-error nil))
+ (eshell-command-result-equal "if (zerop \"foo\") {echo yes} {echo no}"
+ "no")))
+
+(ert-deftest esh-cmd-test/if-else-statement-lisp-form-2 ()
+ "Test invocation of an if/else statement using a Lisp form.
+This tests when `eshell-lisp-form-nil-is-failure' is nil."
+ (let ((eshell-lisp-form-nil-is-failure nil))
+ (eshell-command-result-equal "if (zerop 0) {echo yes} {echo no}"
+ "yes")
+ (eshell-command-result-equal "if (zerop 1) {echo yes} {echo no}"
+ "yes")
+ (let ((debug-on-error nil))
+ (eshell-command-result-equal "if (zerop \"foo\") {echo yes} {echo no}"
+ "no"))))
+
+(ert-deftest esh-cmd-test/if-else-statement-ext-cmd ()
+ "Test invocation of an if/else statement using an external command."
+ (skip-unless (executable-find "["))
+ (eshell-command-result-equal "if {[ foo = foo ]} {echo yes} {echo no}"
+ "yes")
+ (eshell-command-result-equal "if {[ foo = bar ]} {echo yes} {echo no}"
+ "no"))
+
+(ert-deftest esh-cmd-test/unless-statement ()
+ "Test invocation of an unless statement."
+ (let ((eshell-test-value t))
+ (eshell-command-result-equal "unless $eshell-test-value {echo no}"
+ nil))
+ (let ((eshell-test-value nil))
+ (eshell-command-result-equal "unless $eshell-test-value {echo no}"
+ "no")))
+
+(ert-deftest esh-cmd-test/unless-else-statement ()
+ "Test invocation of an unless/else statement."
+ (let ((eshell-test-value t))
+ (eshell-command-result-equal
+ "unless $eshell-test-value {echo no} {echo yes}"
+ "yes"))
+ (let ((eshell-test-value nil))
+ (eshell-command-result-equal
+ "unless $eshell-test-value {echo no} {echo yes}"
+ "no")))
+
+(ert-deftest esh-cmd-test/unless-else-statement-lisp-form ()
+ "Test invocation of an unless/else statement using a Lisp form."
+ (eshell-command-result-equal "unless (zerop 0) {echo no} {echo yes}"
+ "yes")
+ (eshell-command-result-equal "unless (zerop 1) {echo no} {echo yes}"
+ "no")
+ (let ((debug-on-error nil))
+ (eshell-command-result-equal "unless (zerop \"foo\") {echo no} {echo yes}"
+ "no")))
+
+(ert-deftest esh-cmd-test/unless-else-statement-ext-cmd ()
+ "Test invocation of an unless/else statement using an external command."
+ (skip-unless (executable-find "["))
+ (eshell-command-result-equal "unless {[ foo = foo ]} {echo no} {echo yes}"
+ "yes")
+ (eshell-command-result-equal "unless {[ foo = bar ]} {echo no} {echo yes}"
+ "no"))
+
+;; esh-cmd-tests.el ends here
diff --git a/test/lisp/eshell/esh-io-tests.el b/test/lisp/eshell/esh-io-tests.el
new file mode 100644
index 00000000000..37b234eaf06
--- /dev/null
+++ b/test/lisp/eshell/esh-io-tests.el
@@ -0,0 +1,292 @@
+;;; esh-io-tests.el --- esh-io test suite -*- lexical-binding:t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert)
+(require 'ert-x)
+(require 'esh-mode)
+(require 'eshell)
+
+(require 'eshell-tests-helpers
+ (expand-file-name "eshell-tests-helpers"
+ (file-name-directory (or load-file-name
+ default-directory))))
+
+(defvar eshell-test-value nil)
+
+(defun eshell-test-file-string (file)
+ "Return the contents of FILE as a string."
+ (with-temp-buffer
+ (insert-file-contents file)
+ (buffer-string)))
+
+(defun eshell/test-output ()
+ "Write some test output separately to stdout and stderr."
+ (eshell-printn "stdout")
+ (eshell-errorn "stderr"))
+
+;;; Tests:
+
+
+;; Basic redirection
+
+(ert-deftest esh-io-test/redirect-file/overwrite ()
+ "Check that redirecting to a file in overwrite mode works."
+ (ert-with-temp-file temp-file
+ :text "old"
+ (with-temp-eshell
+ (eshell-insert-command (format "echo new > %s" temp-file)))
+ (should (equal (eshell-test-file-string temp-file) "new"))))
+
+(ert-deftest esh-io-test/redirect-file/append ()
+ "Check that redirecting to a file in append mode works."
+ (ert-with-temp-file temp-file
+ :text "old"
+ (with-temp-eshell
+ (eshell-insert-command (format "echo new >> %s" temp-file)))
+ (should (equal (eshell-test-file-string temp-file) "oldnew"))))
+
+(ert-deftest esh-io-test/redirect-file/insert ()
+ "Check that redirecting to a file in insert works."
+ (ert-with-temp-file temp-file
+ :text "old"
+ (with-temp-eshell
+ (eshell-insert-command (format "echo new >>> %s" temp-file)))
+ (should (equal (eshell-test-file-string temp-file) "newold"))))
+
+(ert-deftest esh-io-test/redirect-buffer/overwrite ()
+ "Check that redirecting to a buffer in overwrite mode works."
+ (eshell-with-temp-buffer bufname "old"
+ (with-temp-eshell
+ (eshell-insert-command (format "echo new > #<%s>" bufname)))
+ (should (equal (buffer-string) "new"))))
+
+(ert-deftest esh-io-test/redirect-buffer/append ()
+ "Check that redirecting to a buffer in append mode works."
+ (eshell-with-temp-buffer bufname "old"
+ (with-temp-eshell
+ (eshell-insert-command (format "echo new >> #<%s>" bufname)))
+ (should (equal (buffer-string) "oldnew"))))
+
+(ert-deftest esh-io-test/redirect-buffer/insert ()
+ "Check that redirecting to a buffer in insert mode works."
+ (eshell-with-temp-buffer bufname "old"
+ (goto-char (point-min))
+ (with-temp-eshell
+ (eshell-insert-command (format "echo new >>> #<%s>" bufname)))
+ (should (equal (buffer-string) "newold"))))
+
+(ert-deftest esh-io-test/redirect-buffer/escaped ()
+ "Check that redirecting to a buffer with escaped characters works."
+ (with-temp-buffer
+ (rename-buffer "eshell\\temp\\buffer" t)
+ (let ((bufname (buffer-name)))
+ (with-temp-eshell
+ (eshell-insert-command (format "echo hi > #<%s>"
+ (string-replace "\\" "\\\\" bufname))))
+ (should (equal (buffer-string) "hi")))))
+
+(ert-deftest esh-io-test/redirect-symbol/overwrite ()
+ "Check that redirecting to a symbol in overwrite mode works."
+ (let ((eshell-test-value "old"))
+ (with-temp-eshell
+ (eshell-insert-command "echo new > #'eshell-test-value"))
+ (should (equal eshell-test-value "new"))))
+
+(ert-deftest esh-io-test/redirect-symbol/append ()
+ "Check that redirecting to a symbol in append mode works."
+ (let ((eshell-test-value "old"))
+ (with-temp-eshell
+ (eshell-insert-command "echo new >> #'eshell-test-value"))
+ (should (equal eshell-test-value "oldnew"))))
+
+(ert-deftest esh-io-test/redirect-marker ()
+ "Check that redirecting to a marker works."
+ (with-temp-buffer
+ (let ((eshell-test-value (point-marker)))
+ (with-temp-eshell
+ (eshell-insert-command "echo hi > $eshell-test-value"))
+ (should (equal (buffer-string) "hi")))))
+
+(ert-deftest esh-io-test/redirect-multiple ()
+ "Check that redirecting to multiple targets works."
+ (let ((eshell-test-value "old"))
+ (eshell-with-temp-buffer bufname "old"
+ (with-temp-eshell
+ (eshell-insert-command (format "echo new > #<%s> > #'eshell-test-value"
+ bufname)))
+ (should (equal (buffer-string) "new"))
+ (should (equal eshell-test-value "new")))))
+
+(ert-deftest esh-io-test/redirect-multiple/repeat ()
+ "Check that redirecting to multiple targets works when repeating a target."
+ (let ((eshell-test-value "old"))
+ (eshell-with-temp-buffer bufname "old"
+ (with-temp-eshell
+ (eshell-insert-command
+ (format "echo new > #<%s> > #'eshell-test-value > #<%s>"
+ bufname bufname)))
+ (should (equal (buffer-string) "new"))
+ (should (equal eshell-test-value "new")))))
+
+
+;; Redirecting specific handles
+
+(ert-deftest esh-io-test/redirect-stdout ()
+ "Check that redirecting to stdout doesn't redirect stderr."
+ (eshell-with-temp-buffer bufname "old"
+ (with-temp-eshell
+ (eshell-match-command-output (format "test-output > #<%s>" bufname)
+ "stderr\n"))
+ (should (equal (buffer-string) "stdout\n")))
+ ;; Also check explicitly specifying the stdout fd.
+ (eshell-with-temp-buffer bufname "old"
+ (with-temp-eshell
+ (eshell-match-command-output (format "test-output 1> #<%s>" bufname)
+ "stderr\n"))
+ (should (equal (buffer-string) "stdout\n"))))
+
+(ert-deftest esh-io-test/redirect-stderr/overwrite ()
+ "Check that redirecting to stderr doesn't redirect stdout."
+ (eshell-with-temp-buffer bufname "old"
+ (with-temp-eshell
+ (eshell-match-command-output (format "test-output 2> #<%s>" bufname)
+ "stdout\n"))
+ (should (equal (buffer-string) "stderr\n"))))
+
+(ert-deftest esh-io-test/redirect-stderr/append ()
+ "Check that redirecting to stderr doesn't redirect stdout."
+ (eshell-with-temp-buffer bufname "old"
+ (with-temp-eshell
+ (eshell-match-command-output (format "test-output 2>> #<%s>" bufname)
+ "stdout\n"))
+ (should (equal (buffer-string) "oldstderr\n"))))
+
+(ert-deftest esh-io-test/redirect-stderr/insert ()
+ "Check that redirecting to stderr doesn't redirect stdout."
+ (eshell-with-temp-buffer bufname "old"
+ (goto-char (point-min))
+ (with-temp-eshell
+ (eshell-match-command-output (format "test-output 2>>> #<%s>" bufname)
+ "stdout\n"))
+ (should (equal (buffer-string) "stderr\nold"))))
+
+(ert-deftest esh-io-test/redirect-stdout-and-stderr ()
+ "Check that redirecting to both stdout and stderr works."
+ (eshell-with-temp-buffer bufname-1 "old"
+ (eshell-with-temp-buffer bufname-2 "old"
+ (with-temp-eshell
+ (eshell-match-command-output (format "test-output > #<%s> 2> #<%s>"
+ bufname-1 bufname-2)
+ "\\`\\'"))
+ (should (equal (buffer-string) "stderr\n")))
+ (should (equal (buffer-string) "stdout\n"))))
+
+(ert-deftest esh-io-test/redirect-all/overwrite ()
+ "Check that redirecting to stdout and stderr via shorthand works."
+ (eshell-with-temp-buffer bufname "old"
+ (with-temp-eshell
+ (eshell-match-command-output (format "test-output &> #<%s>" bufname)
+ "\\`\\'"))
+ (should (equal (buffer-string) "stdout\nstderr\n")))
+ ;; Also check the alternate (and less-preferred in Bash) `>&' syntax.
+ (eshell-with-temp-buffer bufname "old"
+ (with-temp-eshell
+ (eshell-match-command-output (format "test-output >& #<%s>" bufname)
+ "\\`\\'"))
+ (should (equal (buffer-string) "stdout\nstderr\n"))))
+
+(ert-deftest esh-io-test/redirect-all/append ()
+ "Check that redirecting to stdout and stderr via shorthand works."
+ (eshell-with-temp-buffer bufname "old"
+ (with-temp-eshell
+ (eshell-match-command-output (format "test-output &>> #<%s>" bufname)
+ "\\`\\'"))
+ (should (equal (buffer-string) "oldstdout\nstderr\n")))
+ ;; Also check the alternate (and less-preferred in Bash) `>>&' syntax.
+ (eshell-with-temp-buffer bufname "old"
+ (with-temp-eshell
+ (eshell-match-command-output (format "test-output >>& #<%s>" bufname)
+ "\\`\\'"))
+ (should (equal (buffer-string) "oldstdout\nstderr\n"))))
+
+(ert-deftest esh-io-test/redirect-all/insert ()
+ "Check that redirecting to stdout and stderr via shorthand works."
+ (eshell-with-temp-buffer bufname "old"
+ (goto-char (point-min))
+ (with-temp-eshell
+ (eshell-match-command-output (format "test-output &>>> #<%s>" bufname)
+ "\\`\\'"))
+ (should (equal (buffer-string) "stdout\nstderr\nold")))
+ ;; Also check the alternate `>>>&' syntax.
+ (eshell-with-temp-buffer bufname "old"
+ (goto-char (point-min))
+ (with-temp-eshell
+ (eshell-match-command-output (format "test-output >>>& #<%s>" bufname)
+ "\\`\\'"))
+ (should (equal (buffer-string) "stdout\nstderr\nold"))))
+
+(ert-deftest esh-io-test/redirect-copy ()
+ "Check that redirecting stdout and then copying stdout to stderr works.
+This should redirect both stdout and stderr to the same place."
+ (eshell-with-temp-buffer bufname "old"
+ (with-temp-eshell
+ (eshell-match-command-output (format "test-output > #<%s> 2>&1" bufname)
+ "\\`\\'"))
+ (should (equal (buffer-string) "stdout\nstderr\n"))))
+
+(ert-deftest esh-io-test/redirect-copy-first ()
+ "Check that copying stdout to stderr and then redirecting stdout works.
+This should redirect stdout to a buffer, and stderr to where
+stdout originally pointed (the terminal)."
+ (eshell-with-temp-buffer bufname "old"
+ (with-temp-eshell
+ (eshell-match-command-output (format "test-output 2>&1 > #<%s>" bufname)
+ "stderr\n"))
+ (should (equal (buffer-string) "stdout\n"))))
+
+(ert-deftest esh-io-test/redirect-pipe ()
+ "Check that \"redirecting\" to a pipe works."
+ ;; `|' should only redirect stdout.
+ (eshell-command-result-equal "test-output | rev"
+ "stderr\ntuodts\n")
+ ;; `|&' should redirect stdout and stderr.
+ (eshell-command-result-equal "test-output |& rev"
+ "tuodts\nrredts\n"))
+
+
+;; Virtual targets
+
+(ert-deftest esh-io-test/virtual-dev-eshell ()
+ "Check that redirecting to /dev/eshell works."
+ (with-temp-eshell
+ (eshell-match-command-output "echo hi > /dev/eshell" "hi")))
+
+(ert-deftest esh-io-test/virtual-dev-kill ()
+ "Check that redirecting to /dev/kill works."
+ (with-temp-eshell
+ (eshell-insert-command "echo one > /dev/kill")
+ (should (equal (car kill-ring) "one"))
+ (eshell-insert-command "echo two > /dev/kill")
+ (should (equal (car kill-ring) "two"))
+ (eshell-insert-command "echo three >> /dev/kill")
+ (should (equal (car kill-ring) "twothree"))))
+
+;;; esh-io-tests.el ends here
diff --git a/test/lisp/eshell/esh-opt-tests.el b/test/lisp/eshell/esh-opt-tests.el
new file mode 100644
index 00000000000..5b30de414a3
--- /dev/null
+++ b/test/lisp/eshell/esh-opt-tests.el
@@ -0,0 +1,289 @@
+;;; esh-opt-tests.el --- esh-opt test suite -*- lexical-binding:t -*-
+
+;; Copyright (C) 2018-2022 Free Software Foundation, Inc.
+
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert)
+(require 'esh-opt)
+
+(ert-deftest esh-opt-test/process-args ()
+ "Test behavior of `eshell--process-args'."
+ (should
+ (equal '(t)
+ (eshell--process-args
+ "sudo" '("-a")
+ '((?a "all" nil show-all
+ "do not ignore entries starting with .")))))
+ (should
+ (equal '("root" "world")
+ (eshell--process-args
+ "sudo" '("-u" "root" "world")
+ '((?u "user" t user
+ "execute a command as another USER"))))))
+
+(ert-deftest esh-opt-test/process-args-parse-leading-options-only ()
+ "Test behavior of :parse-leading-options-only in `eshell--process-args'."
+ (should
+ (equal '(nil "emerge" "-uDN" "world")
+ (eshell--process-args
+ "sudo" '("emerge" "-uDN" "world")
+ '((?u "user" t user
+ "execute a command as another USER")
+ :parse-leading-options-only))))
+ (should
+ (equal '("root" "emerge" "-uDN" "world")
+ (eshell--process-args
+ "sudo" '("-u" "root" "emerge" "-uDN" "world")
+ '((?u "user" t user
+ "execute a command as another USER")
+ :parse-leading-options-only))))
+ (should
+ (equal '("DN" "emerge" "world")
+ (eshell--process-args
+ "sudo" '("-u" "root" "emerge" "-uDN" "world")
+ '((?u "user" t user
+ "execute a command as another USER"))))))
+
+(ert-deftest esh-opt-test/process-args-external ()
+ "Test behavior of :external in `eshell--process-args'."
+ (cl-letf (((symbol-function 'eshell-search-path) #'ignore))
+ (should
+ (equal '(nil "/some/path")
+ (eshell--process-args
+ "ls" '("/some/path")
+ '((?a "all" nil show-all
+ "do not ignore entries starting with .")
+ :external "ls")))))
+ (cl-letf (((symbol-function 'eshell-search-path) #'identity))
+ (should
+ (equal '(no-catch eshell-ext-command "ls")
+ (should-error
+ (eshell--process-args
+ "ls" '("-u" "/some/path")
+ '((?a "all" nil show-all
+ "do not ignore entries starting with .")
+ :external "ls"))
+ :type 'no-catch))))
+ (cl-letf (((symbol-function 'eshell-search-path) #'ignore))
+ (should-error
+ (eshell--process-args
+ "ls" '("-u" "/some/path")
+ '((?a "all" nil show-all
+ "do not ignore entries starting with .")
+ :external "ls"))
+ :type 'error)))
+
+(ert-deftest esh-opt-test/eval-using-options-short ()
+ "Test `eshell-eval-using-options' with short options."
+ (eshell-eval-using-options
+ "ls" '("-a" "/some/path")
+ '((?a "all" nil show-all
+ "do not ignore entries starting with ."))
+ (should (eq show-all t))
+ (should (equal args '("/some/path"))))
+ (eshell-eval-using-options
+ "ls" '("/some/path")
+ '((?a "all" nil show-all
+ "do not ignore entries starting with ."))
+ (should (eq show-all nil))
+ (should (equal args '("/some/path")))))
+
+(ert-deftest esh-opt-test/eval-using-options-long ()
+ "Test `eshell-eval-using-options' with long options."
+ (eshell-eval-using-options
+ "ls" '("--all" "/some/path")
+ '((?a "all" nil show-all
+ "do not ignore entries starting with ."))
+ (should (eq show-all t))
+ (should (equal args '("/some/path")))))
+
+(ert-deftest esh-opt-test/eval-using-options-constant ()
+ "Test `eshell-eval-using-options' with options with constant values."
+ (eshell-eval-using-options
+ "ls" '("/some/path" "-h")
+ '((?h "human-readable" 1024 human-readable
+ "print sizes in human readable format"))
+ (should (eql human-readable 1024))
+ (should (equal args '("/some/path"))))
+ (eshell-eval-using-options
+ "ls" '("/some/path" "--human-readable")
+ '((?h "human-readable" 1024 human-readable
+ "print sizes in human readable format"))
+ (should (eql human-readable 1024))
+ (should (equal args '("/some/path"))))
+ (eshell-eval-using-options
+ "ls" '("/some/path")
+ '((?h "human-readable" 1024 human-readable
+ "print sizes in human readable format"))
+ (should (eq human-readable nil))
+ (should (equal args '("/some/path")))))
+
+(ert-deftest esh-opt-test/eval-using-options-user-specified ()
+ "Test `eshell-eval-using-options' with options with user-specified values."
+ (eshell-eval-using-options
+ "ls" '("-I" "*.txt" "/some/path")
+ '((?I "ignore" t ignore-pattern
+ "do not list implied entries matching pattern"))
+ (should (equal ignore-pattern "*.txt"))
+ (should (equal args '("/some/path"))))
+ (eshell-eval-using-options
+ "ls" '("-I*.txt" "/some/path")
+ '((?I "ignore" t ignore-pattern
+ "do not list implied entries matching pattern"))
+ (should (equal ignore-pattern "*.txt"))
+ (should (equal args '("/some/path"))))
+ (eshell-eval-using-options
+ "ls" '("--ignore" "*.txt" "/some/path")
+ '((?I "ignore" t ignore-pattern
+ "do not list implied entries matching pattern"))
+ (should (equal ignore-pattern "*.txt"))
+ (should (equal args '("/some/path"))))
+ (eshell-eval-using-options
+ "ls" '("--ignore=*.txt" "/some/path")
+ '((?I "ignore" t ignore-pattern
+ "do not list implied entries matching pattern"))
+ (should (equal ignore-pattern "*.txt"))
+ (should (equal args '("/some/path")))))
+
+(ert-deftest esh-opt-test/eval-using-options-short-single-token ()
+ "Test `eshell-eval-using-options' with multiple short options in one token."
+ (eshell-eval-using-options
+ "ls" '("-al" "/some/path")
+ '((?a "all" nil show-all
+ "do not ignore entries starting with .")
+ (?l nil long-listing listing-style
+ "use a long listing format"))
+ (should (eq t show-all))
+ (should (eql listing-style 'long-listing))
+ (should (equal args '("/some/path"))))
+ (eshell-eval-using-options
+ "ls" '("-aI*.txt" "/some/path")
+ '((?a "all" nil show-all
+ "do not ignore entries starting with .")
+ (?I "ignore" t ignore-pattern
+ "do not list implied entries matching pattern"))
+ (should (eq t show-all))
+ (should (equal ignore-pattern "*.txt"))
+ (should (equal args '("/some/path")))))
+
+(ert-deftest esh-opt-test/eval-using-options-stdin ()
+ "Test that \"-\" is a positional arg in `eshell-eval-using-options'."
+ (eshell-eval-using-options
+ "cat" '("-")
+ '((?A "show-all" nil show-all
+ "show all characters"))
+ (should (eq show-all nil))
+ (should (equal args '("-"))))
+ (eshell-eval-using-options
+ "cat" '("-A" "-")
+ '((?A "show-all" nil show-all
+ "show all characters"))
+ (should (eq show-all t))
+ (should (equal args '("-"))))
+ (eshell-eval-using-options
+ "cat" '("-" "-A")
+ '((?A "show-all" nil show-all
+ "show all characters"))
+ (should (eq show-all t))
+ (should (equal args '("-")))))
+
+(ert-deftest esh-opt-test/eval-using-options-terminate-options ()
+ "Test that \"--\" terminates options in `eshell-eval-using-options'."
+ (eshell-eval-using-options
+ "ls" '("--" "-a")
+ '((?a "all" nil show-all
+ "do not ignore entries starting with ."))
+ (should (eq show-all nil))
+ (should (equal args '("-a"))))
+ (eshell-eval-using-options
+ "ls" '("--" "--all")
+ '((?a "all" nil show-all
+ "do not ignore entries starting with ."))
+ (should (eq show-all nil))
+ (should (equal args '("--all")))))
+
+(ert-deftest esh-opt-test/eval-using-options-parse-leading-options-only ()
+ "Test :parse-leading-options-only in `eshell-eval-using-options'."
+ (eshell-eval-using-options
+ "sudo" '("-u" "root" "whoami")
+ '((?u "user" t user "execute a command as another USER")
+ :parse-leading-options-only)
+ (should (equal user "root"))
+ (should (equal args '("whoami"))))
+ (eshell-eval-using-options
+ "sudo" '("--user" "root" "whoami")
+ '((?u "user" t user "execute a command as another USER")
+ :parse-leading-options-only)
+ (should (equal user "root"))
+ (should (equal args '("whoami"))))
+ (eshell-eval-using-options
+ "sudo" '("emerge" "-uDN" "world")
+ '((?u "user" t user "execute a command as another USER"))
+ (should (equal user "DN"))
+ (should (equal args '("emerge" "world"))))
+ (eshell-eval-using-options
+ "sudo" '("emerge" "-uDN" "world")
+ '((?u "user" t user "execute a command as another USER")
+ :parse-leading-options-only)
+ (should (eq user nil))
+ (should (equal args '("emerge" "-uDN" "world")))))
+
+(ert-deftest esh-opt-test/eval-using-options-unrecognized ()
+ "Test `eshell-eval-using-options' with unrecognized options."
+ (should-error
+ (eshell-eval-using-options
+ "ls" '("-u" "/some/path")
+ '((?a "all" nil _show-all
+ "do not ignore entries starting with ."))))
+ (should-error
+ (eshell-eval-using-options
+ "ls" '("-au" "/some/path")
+ '((?a "all" nil _show-all
+ "do not ignore entries starting with ."))))
+ (should-error
+ (eshell-eval-using-options
+ "ls" '("--unrecognized" "/some/path")
+ '((?a "all" nil _show-all
+ "do not ignore entries starting with .")))))
+
+(ert-deftest esh-opt-test/eval-using-options-external ()
+ "Test :external in `eshell-eval-using-options'."
+ (cl-letf (((symbol-function 'eshell-search-path) #'identity)
+ ((symbol-function 'eshell-external-command) #'list))
+ (should
+ (equal (catch 'eshell-external
+ (eshell-eval-using-options
+ "ls" '("/some/path" "-u")
+ '((?a "all" nil _show-all
+ "do not ignore entries starting with .")
+ :external "ls")))
+ '("ls" ("/some/path" "-u"))))
+ (should
+ (equal (catch 'eshell-external
+ (eshell-eval-using-options
+ "ls" '("/some/path2" "-u")
+ '((?a "all" nil _show-all
+ "do not ignore entries starting with .")
+ :preserve-args
+ :external "ls")))
+ '("ls" ("/some/path2" "-u"))))))
+
+(provide 'esh-opt-tests)
+
+;;; esh-opt-tests.el ends here
diff --git a/test/lisp/eshell/esh-proc-tests.el b/test/lisp/eshell/esh-proc-tests.el
new file mode 100644
index 00000000000..abe363bee0d
--- /dev/null
+++ b/test/lisp/eshell/esh-proc-tests.el
@@ -0,0 +1,249 @@
+;;; esh-proc-tests.el --- esh-proc test suite -*- lexical-binding:t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert)
+(require 'esh-mode)
+(require 'eshell)
+
+(require 'eshell-tests-helpers
+ (expand-file-name "eshell-tests-helpers"
+ (file-name-directory (or load-file-name
+ default-directory))))
+
+(defvar esh-proc-test--output-cmd
+ (concat "sh -c '"
+ "echo stdout; "
+ "echo stderr >&2"
+ "'")
+ "A shell command that prints to both stdout and stderr.")
+
+(defvar esh-proc-test--detect-pty-cmd
+ (concat "sh -c '"
+ "if [ -t 0 ]; then echo stdin; fi; "
+ "if [ -t 1 ]; then echo stdout; fi; "
+ "if [ -t 2 ]; then echo stderr; fi"
+ "'")
+ "A shell command that prints the standard streams connected as TTYs.")
+
+;;; Tests:
+
+
+;; Output and redirection
+
+(ert-deftest esh-proc-test/output/to-screen ()
+ "Check that outputting stdout and stderr to the screen works."
+ (skip-unless (executable-find "sh"))
+ (with-temp-eshell
+ (eshell-match-command-output esh-proc-test--output-cmd
+ "stdout\nstderr\n")))
+
+(ert-deftest esh-proc-test/output/stdout-to-buffer ()
+ "Check that redirecting only stdout works."
+ (skip-unless (executable-find "sh"))
+ (eshell-with-temp-buffer bufname "old"
+ (with-temp-eshell
+ (eshell-match-command-output
+ (format "%s > #<%s>" esh-proc-test--output-cmd bufname)
+ "stderr\n"))
+ (should (equal (buffer-string) "stdout\n"))))
+
+(ert-deftest esh-proc-test/output/stderr-to-buffer ()
+ "Check that redirecting only stderr works."
+ (skip-unless (executable-find "sh"))
+ (eshell-with-temp-buffer bufname "old"
+ (with-temp-eshell
+ (eshell-match-command-output
+ (format "%s 2> #<%s>" esh-proc-test--output-cmd bufname)
+ "stdout\n"))
+ (should (equal (buffer-string) "stderr\n"))))
+
+(ert-deftest esh-proc-test/output/stdout-and-stderr-to-buffer ()
+ "Check that redirecting stdout and stderr works."
+ (skip-unless (executable-find "sh"))
+ (eshell-with-temp-buffer bufname "old"
+ (with-temp-eshell
+ (eshell-match-command-output
+ (format "%s &> #<%s>" esh-proc-test--output-cmd bufname)
+ "\\`\\'"))
+ (should (equal (buffer-string) "stdout\nstderr\n"))))
+
+
+;; Exit status
+
+(ert-deftest esh-proc-test/exit-status/success ()
+ "Check that successful execution is properly recorded."
+ (skip-unless (executable-find "sh"))
+ (with-temp-eshell
+ (eshell-insert-command "sh -c 'exit 0'")
+ (eshell-wait-for-subprocess)
+ (should (= eshell-last-command-status 0))
+ (should (eq eshell-last-command-result t))))
+
+(ert-deftest esh-proc-test/exit-status/failure ()
+ "Check that failed execution is properly recorded."
+ (skip-unless (executable-find "sh"))
+ (with-temp-eshell
+ (eshell-insert-command "sh -c 'exit 1'")
+ (eshell-wait-for-subprocess)
+ (should (= eshell-last-command-status 1))
+ (should (eq eshell-last-command-result nil))))
+
+(ert-deftest esh-proc-test/exit-status/with-stderr-pipe ()
+ "Check that failed execution is properly recorded even with a pipe process."
+ (skip-unless (executable-find "sh"))
+ (eshell-with-temp-buffer bufname "old"
+ (with-temp-eshell
+ (eshell-insert-command (format "sh -c 'exit 1' > #<%s>" bufname))
+ (eshell-wait-for-subprocess)
+ (should (= eshell-last-command-status 1))
+ (should (eq eshell-last-command-result nil)))))
+
+
+;; Pipelines
+
+(ert-deftest esh-proc-test/sigpipe-exits-process ()
+ "Test that a SIGPIPE is properly sent to a process if a pipe closes"
+ (skip-unless (and (executable-find "sh")
+ (executable-find "echo")
+ (executable-find "sleep")))
+ (with-temp-eshell
+ (eshell-match-command-output
+ ;; The first command is like `yes' but slower. This is to prevent
+ ;; it from taxing Emacs's process filter too much and causing a
+ ;; hang. Note that we use "|&" to connect the processes so that
+ ;; Emacs doesn't create an extra pipe process for the first "sh"
+ ;; invocation.
+ (concat "sh -c 'while true; do echo y; sleep 1; done' |& "
+ "sh -c 'read NAME; echo ${NAME}'")
+ "y\n")
+ (eshell-wait-for-subprocess t)
+ (should (eq (process-list) nil))))
+
+(ert-deftest esh-proc-test/pipeline-connection-type/no-pipeline ()
+ "Test that all streams are PTYs when a command is not in a pipeline."
+ (skip-unless (executable-find "sh"))
+ (eshell-command-result-equal
+ esh-proc-test--detect-pty-cmd
+ ;; PTYs aren't supported on MS-Windows.
+ (unless (eq system-type 'windows-nt)
+ "stdin\nstdout\nstderr\n")))
+
+(ert-deftest esh-proc-test/pipeline-connection-type/first ()
+ "Test that only stdin is a PTY when a command starts a pipeline."
+ (skip-unless (and (executable-find "sh")
+ (executable-find "cat")))
+ (eshell-command-result-equal
+ (concat esh-proc-test--detect-pty-cmd " | cat")
+ (unless (eq system-type 'windows-nt)
+ "stdin\n")))
+
+(ert-deftest esh-proc-test/pipeline-connection-type/middle ()
+ "Test that all streams are pipes when a command is in the middle of a
+pipeline."
+ (skip-unless (and (executable-find "sh")
+ (executable-find "cat")))
+ ;; An `eshell-pipe-broken' signal might occur internally; let Eshell
+ ;; handle it!
+ (let ((debug-on-error nil))
+ (eshell-command-result-equal
+ (concat "echo hi | " esh-proc-test--detect-pty-cmd " | cat")
+ nil)))
+
+(ert-deftest esh-proc-test/pipeline-connection-type/last ()
+ "Test that only output streams are PTYs when a command ends a pipeline."
+ (skip-unless (executable-find "sh"))
+ ;; An `eshell-pipe-broken' signal might occur internally; let Eshell
+ ;; handle it!
+ (let ((debug-on-error nil))
+ (eshell-command-result-equal
+ (concat "echo hi | " esh-proc-test--detect-pty-cmd)
+ (unless (eq system-type 'windows-nt)
+ "stdout\nstderr\n"))))
+
+
+;; Killing processes
+
+(ert-deftest esh-proc-test/kill-process/foreground-only ()
+ "Test that `eshell-kill-process' only kills foreground processes."
+ (with-temp-eshell
+ (eshell-insert-command "sleep 100 &")
+ (eshell-insert-command "sleep 100")
+ (should (equal (length eshell-process-list) 2))
+ ;; This should kill only the foreground process.
+ (eshell-kill-process)
+ (eshell-wait-for-subprocess)
+ (should (equal (length eshell-process-list) 1))
+ ;; Now kill everything, including the background process.
+ (eshell-process-interact 'kill-process t)
+ (eshell-wait-for-subprocess t)
+ (should (equal (length eshell-process-list) 0))))
+
+(ert-deftest esh-proc-test/kill-process/background-prompt ()
+ "Test that killing a background process doesn't emit a new
+prompt. See bug#54136."
+ (skip-unless (and (executable-find "sh")
+ (executable-find "sleep")))
+ (with-temp-eshell
+ (eshell-insert-command "sh -c 'while true; do sleep 1; done' &")
+ (kill-process (caar eshell-process-list))
+ (eshell-wait-for-subprocess)
+ (should (eshell-match-output "\\[sh\\(\\.exe\\)?\\] [[:digit:]]+\n"))))
+
+(ert-deftest esh-proc-test/kill-pipeline ()
+ "Test that killing a pipeline of processes only emits a single
+prompt. See bug#54136."
+ (skip-unless (and (executable-find "sh")
+ (executable-find "echo")
+ (executable-find "sleep")))
+ ;; This test doesn't work on EMBA with AOT nativecomp, but works
+ ;; fine elsewhere.
+ (skip-unless (not (getenv "EMACS_EMBA_CI")))
+ (with-temp-eshell
+ (eshell-insert-command
+ (concat "sh -c 'while true; do echo y; sleep 1; done' | "
+ "sh -c 'while true; do read NAME; done'"))
+ (let ((output-start (eshell-beginning-of-output)))
+ (eshell-kill-process)
+ (eshell-wait-for-subprocess t)
+ (should (string-match-p
+ ;; "interrupt\n" is for MS-Windows.
+ (rx (or "interrupt\n" "killed\n" "killed: 9\n"))
+ (buffer-substring-no-properties
+ output-start (eshell-end-of-output)))))))
+
+(ert-deftest esh-proc-test/kill-pipeline-head ()
+ "Test that killing the first process in a pipeline doesn't
+write the exit status to the pipe. See bug#54136."
+ (skip-unless (and (executable-find "sh")
+ (executable-find "echo")
+ (executable-find "sleep")))
+ (with-temp-eshell
+ (eshell-insert-command
+ (concat "sh -c 'while true; do sleep 1; done' | "
+ "sh -c 'while read NAME; do echo =${NAME}=; done'"))
+ (let ((output-start (eshell-beginning-of-output)))
+ (kill-process (eshell-head-process))
+ (eshell-wait-for-subprocess t)
+ (should (equal (buffer-substring-no-properties
+ output-start (eshell-end-of-output))
+ "")))))
+
+;;; esh-proc-tests.el ends here
diff --git a/test/lisp/eshell/esh-var-tests.el b/test/lisp/eshell/esh-var-tests.el
new file mode 100644
index 00000000000..cb5b1766bb5
--- /dev/null
+++ b/test/lisp/eshell/esh-var-tests.el
@@ -0,0 +1,569 @@
+;;; esh-var-tests.el --- esh-var test suite -*- lexical-binding:t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Tests for Eshell's variable handling.
+
+;;; Code:
+
+(require 'ert)
+(require 'esh-mode)
+(require 'eshell)
+
+(require 'eshell-tests-helpers
+ (expand-file-name "eshell-tests-helpers"
+ (file-name-directory (or load-file-name
+ default-directory))))
+
+(defvar eshell-test-value nil)
+
+;;; Tests:
+
+
+;; Variable interpolation
+
+(ert-deftest esh-var-test/interp-var ()
+ "Interpolate variable"
+ (eshell-command-result-equal "echo $user-login-name"
+ user-login-name))
+
+(ert-deftest esh-var-test/interp-quoted-var ()
+ "Interpolate quoted variable"
+ (eshell-command-result-equal "echo $'user-login-name'"
+ user-login-name)
+ (eshell-command-result-equal "echo $\"user-login-name\""
+ user-login-name))
+
+(ert-deftest esh-var-test/interp-quoted-var-concat ()
+ "Interpolate and concat quoted variable"
+ (eshell-command-result-equal "echo $'user-login-name'-foo"
+ (concat user-login-name "-foo"))
+ (eshell-command-result-equal "echo $\"user-login-name\"-foo"
+ (concat user-login-name "-foo")))
+
+(ert-deftest esh-var-test/interp-var-indices ()
+ "Interpolate list variable with indices"
+ (let ((eshell-test-value '("zero" "one" "two" "three" "four")))
+ (eshell-command-result-equal "echo $eshell-test-value[0]"
+ "zero")
+ (eshell-command-result-equal "echo $eshell-test-value[0 2]"
+ '("zero" "two"))
+ (eshell-command-result-equal "echo $eshell-test-value[0 2 4]"
+ '("zero" "two" "four"))))
+
+(ert-deftest esh-var-test/interp-var-split-indices ()
+ "Interpolate string variable with indices"
+ (let ((eshell-test-value "zero one two three four"))
+ (eshell-command-result-equal "echo $eshell-test-value[0]"
+ "zero")
+ (eshell-command-result-equal "echo $eshell-test-value[0 2]"
+ '("zero" "two"))
+ (eshell-command-result-equal "echo $eshell-test-value[0 2 4]"
+ '("zero" "two" "four"))))
+
+(ert-deftest esh-var-test/interp-var-string-split-indices ()
+ "Interpolate string variable with string splitter and indices"
+ (let ((eshell-test-value "zero:one:two:three:four"))
+ (eshell-command-result-equal "echo $eshell-test-value[: 0]"
+ "zero")
+ (eshell-command-result-equal "echo $eshell-test-value[: 0 2]"
+ '("zero" "two")))
+ (let ((eshell-test-value "zeroXoneXtwoXthreeXfour"))
+ (eshell-command-result-equal "echo $eshell-test-value[X 0]"
+ "zero")
+ (eshell-command-result-equal "echo $eshell-test-value[X 0 2]"
+ '("zero" "two"))))
+
+(ert-deftest esh-var-test/interp-var-regexp-split-indices ()
+ "Interpolate string variable with regexp splitter and indices"
+ (let ((eshell-test-value "zero:one!two:three!four"))
+ (eshell-command-result-equal "echo $eshell-test-value['[:!]' 0]"
+ "zero")
+ (eshell-command-result-equal "echo $eshell-test-value['[:!]' 0 2]"
+ '("zero" "two"))
+ (eshell-command-result-equal "echo $eshell-test-value[\"[:!]\" 0]"
+ "zero")
+ (eshell-command-result-equal "echo $eshell-test-value[\"[:!]\" 0 2]"
+ '("zero" "two"))))
+
+(ert-deftest esh-var-test/interp-var-assoc ()
+ "Interpolate alist variable with index"
+ (let ((eshell-test-value '(("foo" . 1) (bar . 2))))
+ (eshell-command-result-equal "echo $eshell-test-value[foo]"
+ 1)
+ (eshell-command-result-equal "echo $eshell-test-value[#'bar]"
+ 2)))
+
+(ert-deftest esh-var-test/interp-var-length-list ()
+ "Interpolate length of list variable"
+ (let ((eshell-test-value '((1 2) (3) (5 (6 7 8 9)))))
+ (eshell-command-result-equal "echo $#eshell-test-value" 3)
+ (eshell-command-result-equal "echo $#eshell-test-value[1]" 1)
+ (eshell-command-result-equal "echo $#eshell-test-value[2][1]" 4)))
+
+(ert-deftest esh-var-test/interp-var-length-string ()
+ "Interpolate length of string variable"
+ (let ((eshell-test-value "foobar"))
+ (eshell-command-result-equal "echo $#eshell-test-value" 6)))
+
+(ert-deftest esh-var-test/interp-var-length-alist ()
+ "Interpolate length of alist variable"
+ (let ((eshell-test-value '(("foo" . (1 2 3)))))
+ (eshell-command-result-equal "echo $#eshell-test-value" 1)
+ (eshell-command-result-equal "echo $#eshell-test-value[foo]" 3)))
+
+(ert-deftest esh-var-test/interp-lisp ()
+ "Interpolate Lisp form evaluation"
+ (eshell-command-result-equal "+ $(+ 1 2) 3" 6))
+
+(ert-deftest esh-var-test/interp-lisp-indices ()
+ "Interpolate Lisp form evaluation with index"
+ (eshell-command-result-equal "+ $(list 1 2)[1] 3" 5))
+
+(ert-deftest esh-var-test/interp-cmd ()
+ "Interpolate command result"
+ (eshell-command-result-equal "+ ${+ 1 2} 3" 6))
+
+(ert-deftest esh-var-test/interp-cmd-indices ()
+ "Interpolate command result with index"
+ (eshell-command-result-equal "+ ${listify 1 2}[1] 3" 5))
+
+(ert-deftest esh-var-test/interp-cmd-external ()
+ "Interpolate command result from external command"
+ (skip-unless (executable-find "echo"))
+ (with-temp-eshell
+ (eshell-match-command-output "echo ${*echo hi}"
+ "hi\n")))
+
+(ert-deftest esh-var-test/interp-cmd-external-indices ()
+ "Interpolate command result from external command with index"
+ (skip-unless (executable-find "echo"))
+ (with-temp-eshell
+ (eshell-match-command-output "echo ${*echo \"hi\nbye\"}[1]"
+ "bye\n")))
+
+(ert-deftest esh-var-test/interp-temp-cmd ()
+ "Interpolate command result redirected to temp file"
+ (eshell-command-result-equal "cat $<echo hi>" "hi"))
+
+(ert-deftest esh-var-test/interp-concat-lisp ()
+ "Interpolate and concat Lisp form"
+ (eshell-command-result-equal "+ $(+ 1 2)3 3" 36))
+
+(ert-deftest esh-var-test/interp-concat-lisp2 ()
+ "Interpolate and concat two Lisp forms"
+ (eshell-command-result-equal "+ $(+ 1 2)$(+ 1 2) 3" 36))
+
+(ert-deftest esh-var-test/interp-concat-cmd ()
+ "Interpolate and concat command with literal"
+ (eshell-command-result-equal "+ ${+ 1 2}3 3" 36)
+ (eshell-command-result-equal "echo ${*echo \"foo\nbar\"}-baz"
+ '("foo" "bar-baz"))
+ ;; Concatenating to a number in a list should produce a number...
+ (eshell-command-result-equal "echo ${*echo \"1\n2\"}3"
+ '(1 23))
+ ;; ... but concatenating to a string that looks like a number in a list
+ ;; should produce a string.
+ (eshell-command-result-equal "echo ${*echo \"hi\n2\"}3"
+ '("hi" "23")))
+
+(ert-deftest esh-var-test/interp-concat-cmd2 ()
+ "Interpolate and concat two commands"
+ (eshell-command-result-equal "+ ${+ 1 2}${+ 1 2} 3" 36))
+
+(ert-deftest esh-var-test/interp-concat-cmd-external ()
+ "Interpolate command result from external command with concatenation"
+ (skip-unless (executable-find "echo"))
+ (with-temp-eshell
+ (eshell-match-command-output "echo ${echo hi}-${*echo there}"
+ "hi-there\n")))
+
+(ert-deftest esh-var-test/quoted-interp-var ()
+ "Interpolate variable inside double-quotes"
+ (eshell-command-result-equal "echo \"$user-login-name\""
+ user-login-name))
+
+(ert-deftest esh-var-test/quoted-interp-quoted-var ()
+ "Interpolate quoted variable inside double-quotes"
+ (eshell-command-result-equal "echo \"hi, $'user-login-name'\""
+ (concat "hi, " user-login-name))
+ (eshell-command-result-equal "echo \"hi, $\\\"user-login-name\\\"\""
+ (concat "hi, " user-login-name)))
+
+(ert-deftest esh-var-test/quoted-interp-var-indices ()
+ "Interpolate string variable with indices inside double-quotes"
+ (let ((eshell-test-value '("zero" "one" "two" "three" "four")))
+ (eshell-command-result-equal "echo \"$eshell-test-value[0]\""
+ "zero")
+ ;; FIXME: These tests would use the 0th index like the other tests
+ ;; here, but evaluating the command just above adds an `escaped'
+ ;; property to the string "zero". This results in the output
+ ;; printing the string properties, which is probably the wrong
+ ;; behavior. See bug#54486.
+ (eshell-command-result-equal "echo \"$eshell-test-value[1 2]\""
+ "(\"one\" \"two\")")
+ (eshell-command-result-equal "echo \"$eshell-test-value[1 2 4]\""
+ "(\"one\" \"two\" \"four\")")))
+
+(ert-deftest esh-var-test/quoted-interp-var-split-indices ()
+ "Interpolate string variable with indices inside double-quotes"
+ (let ((eshell-test-value "zero one two three four"))
+ (eshell-command-result-equal "echo \"$eshell-test-value[0]\""
+ "zero")
+ (eshell-command-result-equal "echo \"$eshell-test-value[0 2]\""
+ "(\"zero\" \"two\")")))
+
+(ert-deftest esh-var-test/quoted-interp-var-string-split-indices ()
+ "Interpolate string variable with string splitter and indices
+inside double-quotes"
+ (let ((eshell-test-value "zero:one:two:three:four"))
+ (eshell-command-result-equal "echo \"$eshell-test-value[: 0]\""
+ "zero")
+ (eshell-command-result-equal "echo \"$eshell-test-value[: 0 2]\""
+ "(\"zero\" \"two\")"))
+ (let ((eshell-test-value "zeroXoneXtwoXthreeXfour"))
+ (eshell-command-result-equal "echo \"$eshell-test-value[X 0]\""
+ "zero")
+ (eshell-command-result-equal "echo \"$eshell-test-value[X 0 2]\""
+ "(\"zero\" \"two\")")))
+
+(ert-deftest esh-var-test/quoted-interp-var-regexp-split-indices ()
+ "Interpolate string variable with regexp splitter and indices"
+ (let ((eshell-test-value "zero:one!two:three!four"))
+ (eshell-command-result-equal "echo \"$eshell-test-value['[:!]' 0]\""
+ "zero")
+ (eshell-command-result-equal "echo \"$eshell-test-value['[:!]' 0 2]\""
+ "(\"zero\" \"two\")")
+ (eshell-command-result-equal "echo \"$eshell-test-value[\\\"[:!]\\\" 0]\""
+ "zero")
+ (eshell-command-result-equal
+ "echo \"$eshell-test-value[\\\"[:!]\\\" 0 2]\""
+ "(\"zero\" \"two\")")))
+
+(ert-deftest esh-var-test/quoted-interp-var-assoc ()
+ "Interpolate alist variable with index inside double-quotes"
+ (let ((eshell-test-value '(("foo" . 1) (bar . 2))))
+ (eshell-command-result-equal "echo \"$eshell-test-value[foo]\""
+ "1")
+ (eshell-command-result-equal "echo \"$eshell-test-value[#'bar]\""
+ "2")))
+
+(ert-deftest esh-var-test/quoted-interp-var-length-list ()
+ "Interpolate length of list variable inside double-quotes"
+ (let ((eshell-test-value '((1 2) (3) (5 (6 7 8 9)))))
+ (eshell-command-result-equal "echo \"$#eshell-test-value\""
+ "3")
+ (eshell-command-result-equal "echo \"$#eshell-test-value[1]\""
+ "1")
+ (eshell-command-result-equal "echo \"$#eshell-test-value[2][1]\""
+ "4")))
+
+(ert-deftest esh-var-test/quoted-interp-var-length-string ()
+ "Interpolate length of string variable inside double-quotes"
+ (let ((eshell-test-value "foobar"))
+ (eshell-command-result-equal "echo \"$#eshell-test-value\""
+ "6")))
+
+(ert-deftest esh-var-test/quoted-interp-var-length-alist ()
+ "Interpolate length of alist variable inside double-quotes"
+ (let ((eshell-test-value '(("foo" . (1 2 3)))))
+ (eshell-command-result-equal "echo \"$#eshell-test-value\""
+ "1")
+ (eshell-command-result-equal "echo \"$#eshell-test-value[foo]\""
+ "3"))
+
+(ert-deftest esh-var-test/quoted-interp-lisp ()
+ "Interpolate Lisp form evaluation inside double-quotes"
+ (eshell-command-result-equal "echo \"hi $(concat \\\"the\\\" \\\"re\\\")\""
+ "hi there"))
+
+(ert-deftest esh-var-test/quoted-interp-lisp-indices ()
+ "Interpolate Lisp form evaluation with index"
+ (eshell-command-result-equal "concat \"$(list 1 2)[1]\" cool"
+ "2cool"))
+
+(ert-deftest esh-var-test/quoted-interp-cmd ()
+ "Interpolate command result inside double-quotes"
+ (eshell-command-result-equal "echo \"hi ${echo \\\"there\\\"}\""
+ "hi there"))
+
+(ert-deftest esh-var-test/quoted-interp-cmd-indices ()
+ "Interpolate command result with index inside double-quotes"
+ (eshell-command-result-equal "concat \"${listify 1 2}[1]\" cool"
+ "2cool"))
+
+(ert-deftest esh-var-test/quoted-interp-temp-cmd ()
+ "Interpolate command result redirected to temp file inside double-quotes"
+ (let ((temporary-file-directory
+ (file-name-as-directory (make-temp-file "esh-vars-tests" t))))
+ (unwind-protect
+ (eshell-command-result-equal "cat \"$<echo hi>\"" "hi"))
+ (delete-directory temporary-file-directory t))))
+
+(ert-deftest esh-var-test/quoted-interp-concat-cmd ()
+ "Interpolate and concat command with literal"
+ (eshell-command-result-equal "echo \"${echo \\\"foo\nbar\\\"} baz\""
+ "foo\nbar baz"))
+
+
+;; Interpolated variable conversion
+
+(ert-deftest esh-var-test/interp-convert-var-number ()
+ "Interpolate numeric variable"
+ (let ((eshell-test-value 123))
+ (eshell-command-result-equal "type-of $eshell-test-value"
+ 'integer)))
+
+(ert-deftest esh-var-test/interp-convert-var-split-indices ()
+ "Interpolate and convert string variable with indices"
+ ;; Check that numeric forms are converted to numbers.
+ (let ((eshell-test-value "000 010 020 030 040"))
+ (eshell-command-result-equal "echo $eshell-test-value[0]"
+ 0)
+ (eshell-command-result-equal "echo $eshell-test-value[0 2]"
+ '(0 20)))
+ ;; Check that multiline forms are preserved as-is.
+ (let ((eshell-test-value "foo\nbar:baz\n"))
+ (eshell-command-result-equal "echo $eshell-test-value[: 0]"
+ "foo\nbar")
+ (eshell-command-result-equal "echo $eshell-test-value[: 1]"
+ "baz\n")))
+
+(ert-deftest esh-var-test/interp-convert-quoted-var-number ()
+ "Interpolate numeric quoted numeric variable"
+ (let ((eshell-test-value 123))
+ (eshell-command-result-equal "type-of $'eshell-test-value'"
+ 'integer)
+ (eshell-command-result-equal "type-of $\"eshell-test-value\""
+ 'integer)))
+
+(ert-deftest esh-var-test/interp-convert-quoted-var-split-indices ()
+ "Interpolate and convert quoted string variable with indices"
+ (let ((eshell-test-value "000 010 020 030 040"))
+ (eshell-command-result-equal "echo $'eshell-test-value'[0]"
+ 0)
+ (eshell-command-result-equal "echo $'eshell-test-value'[0 2]"
+ '(0 20))))
+
+(ert-deftest esh-var-test/interp-convert-cmd-string-newline ()
+ "Interpolate trailing-newline command result"
+ (eshell-command-result-equal "echo ${echo \"foo\n\"}" "foo"))
+
+(ert-deftest esh-var-test/interp-convert-cmd-multiline ()
+ "Interpolate multi-line command result"
+ (eshell-command-result-equal "echo ${echo \"foo\nbar\"}"
+ '("foo" "bar"))
+ ;; Numeric output should be converted to numbers...
+ (eshell-command-result-equal "echo ${echo \"01\n02\n03\"}"
+ '(1 2 3))
+ ;; ... but only if every line is numeric.
+ (eshell-command-result-equal "echo ${echo \"01\n02\nhi\"}"
+ '("01" "02" "hi")))
+
+(ert-deftest esh-var-test/interp-convert-cmd-number ()
+ "Interpolate numeric command result"
+ (eshell-command-result-equal "echo ${echo \"1\"}" 1))
+
+(ert-deftest esh-var-test/interp-convert-cmd-split-indices ()
+ "Interpolate command result with indices"
+ (eshell-command-result-equal "echo ${echo \"000 010 020\"}[0]"
+ 0)
+ (eshell-command-result-equal "echo ${echo \"000 010 020\"}[0 2]"
+ '(0 20)))
+
+(ert-deftest esh-var-test/quoted-interp-convert-var-number ()
+ "Interpolate numeric variable inside double-quotes"
+ (let ((eshell-test-value 123))
+ (eshell-command-result-equal "type-of \"$eshell-test-value\""
+ 'string)))
+
+(ert-deftest esh-var-test/quoted-interp-convert-var-split-indices ()
+ "Interpolate string variable with indices inside double-quotes"
+ (let ((eshell-test-value "000 010 020 030 040"))
+ (eshell-command-result-equal "echo \"$eshell-test-value[0]\""
+ "000")
+ (eshell-command-result-equal "echo \"$eshell-test-value[0 2]\""
+ "(\"000\" \"020\")")))
+
+(ert-deftest esh-var-test/quoted-interp-convert-quoted-var-number ()
+ "Interpolate numeric quoted variable inside double-quotes"
+ (let ((eshell-test-value 123))
+ (eshell-command-result-equal "type-of \"$'eshell-test-value'\""
+ 'string)
+ (eshell-command-result-equal "type-of \"$\\\"eshell-test-value\\\"\""
+ 'string)))
+
+(ert-deftest esh-var-test/quoted-interp-convert-quoted-var-split-indices ()
+ "Interpolate quoted string variable with indices inside double-quotes"
+ (let ((eshell-test-value "000 010 020 030 040"))
+ (eshell-command-result-equal "echo \"$eshell-test-value[0]\""
+ "000")
+ (eshell-command-result-equal "echo \"$eshell-test-value[0 2]\""
+ "(\"000\" \"020\")")))
+
+(ert-deftest esh-var-test/quoted-interp-convert-cmd-string-newline ()
+ "Interpolate trailing-newline command result inside double-quotes"
+ (eshell-command-result-equal "echo \"${echo \\\"foo\n\\\"}\""
+ "foo")
+ (eshell-command-result-equal "echo \"${echo \\\"foo\n\n\\\"}\""
+ "foo"))
+
+(ert-deftest esh-var-test/quoted-interp-convert-cmd-multiline ()
+ "Interpolate multi-line command result inside double-quotes"
+ (eshell-command-result-equal "echo \"${echo \\\"foo\nbar\\\"}\""
+ "foo\nbar"))
+
+(ert-deftest esh-var-test/quoted-interp-convert-cmd-number ()
+ "Interpolate numeric command result inside double-quotes"
+ (eshell-command-result-equal "echo \"${echo \\\"1\\\"}\"" "1"))
+
+(ert-deftest esh-var-test/quoted-interp-convert-cmd-split-indices ()
+ "Interpolate command result with indices inside double-quotes"
+ (eshell-command-result-equal "echo \"${echo \\\"000 010 020\\\"}[0]\""
+ "000"))
+
+
+;; Built-in variables
+
+(ert-deftest esh-var-test/lines-var ()
+ "$LINES should equal (window-body-height nil 'remap)"
+ (eshell-command-result-equal "echo $LINES"
+ (window-body-height nil 'remap)))
+
+(ert-deftest esh-var-test/columns-var ()
+ "$COLUMNS should equal (window-body-width nil 'remap)"
+ (eshell-command-result-equal "echo $COLUMNS"
+ (window-body-width nil 'remap)))
+
+(ert-deftest esh-var-test/inside-emacs-var ()
+ "Test presence of \"INSIDE_EMACS\" in subprocesses"
+ (with-temp-eshell
+ (eshell-match-command-output "env"
+ (format "INSIDE_EMACS=%s,eshell"
+ emacs-version))))
+
+(ert-deftest esh-var-test/inside-emacs-var-split-indices ()
+ "Test using \"INSIDE_EMACS\" with split indices"
+ (with-temp-eshell
+ (eshell-match-command-output "echo $INSIDE_EMACS[, 1]"
+ "eshell")))
+
+(ert-deftest esh-var-test/last-status-var-lisp-command ()
+ "Test using the \"last exit status\" ($?) variable with a Lisp command"
+ (with-temp-eshell
+ (eshell-match-command-output "zerop 0; echo $?"
+ "t\n0\n")
+ (eshell-match-command-output "zerop 1; echo $?"
+ "0\n")
+ (let ((debug-on-error nil))
+ (eshell-match-command-output "zerop foo; echo $?"
+ "1\n"))))
+
+(ert-deftest esh-var-test/last-status-var-lisp-form ()
+ "Test using the \"last exit status\" ($?) variable with a Lisp form"
+ (let ((eshell-lisp-form-nil-is-failure t))
+ (with-temp-eshell
+ (eshell-match-command-output "(zerop 0); echo $?"
+ "t\n0\n")
+ (eshell-match-command-output "(zerop 1); echo $?"
+ "2\n")
+ (let ((debug-on-error nil))
+ (eshell-match-command-output "(zerop \"foo\"); echo $?"
+ "1\n")))))
+
+(ert-deftest esh-var-test/last-status-var-lisp-form-2 ()
+ "Test using the \"last exit status\" ($?) variable with a Lisp form.
+This tests when `eshell-lisp-form-nil-is-failure' is nil."
+ (let ((eshell-lisp-form-nil-is-failure nil))
+ (with-temp-eshell
+ (eshell-match-command-output "(zerop 0); echo $?"
+ "0\n")
+ (eshell-match-command-output "(zerop 0); echo $?"
+ "0\n")
+ (let ((debug-on-error nil))
+ (eshell-match-command-output "(zerop \"foo\"); echo $?"
+ "1\n")))))
+
+(ert-deftest esh-var-test/last-status-var-ext-cmd ()
+ "Test using the \"last exit status\" ($?) variable with an external command"
+ (skip-unless (executable-find "["))
+ (with-temp-eshell
+ (eshell-match-command-output "[ foo = foo ]; echo $?"
+ "0\n")
+ (eshell-match-command-output "[ foo = bar ]; echo $?"
+ "1\n")))
+
+(ert-deftest esh-var-test/last-result-var ()
+ "Test using the \"last result\" ($$) variable"
+ (with-temp-eshell
+ (eshell-match-command-output "+ 1 2; + $$ 2"
+ "3\n5\n")))
+
+(ert-deftest esh-var-test/last-result-var-twice ()
+ "Test using the \"last result\" ($$) variable twice"
+ (with-temp-eshell
+ (eshell-match-command-output "+ 1 2; + $$ $$"
+ "3\n6\n")))
+
+(ert-deftest esh-var-test/last-result-var-ext-cmd ()
+ "Test using the \"last result\" ($$) variable with an external command"
+ (skip-unless (executable-find "["))
+ (with-temp-eshell
+ ;; MS-DOS/MS-Windows have an external command 'format', which we
+ ;; don't want here.
+ (let ((eshell-prefer-lisp-functions t))
+ (eshell-match-command-output "[ foo = foo ]; format \"%s\" $$"
+ "t\n")
+ (eshell-match-command-output "[ foo = bar ]; format \"%s\" $$"
+ "nil\n"))))
+
+(ert-deftest esh-var-test/last-result-var-split-indices ()
+ "Test using the \"last result\" ($$) variable with split indices"
+ (with-temp-eshell
+ (eshell-match-command-output
+ "string-join (list \"01\" \"02\") :; + $$[: 1] 3"
+ "01:02\n5\n")
+ (eshell-match-command-output
+ "string-join (list \"01\" \"02\") :; echo \"$$[: 1]\""
+ "01:02\n02\n")))
+
+(ert-deftest esh-var-test/last-arg-var ()
+ "Test using the \"last arg\" ($_) variable"
+ (with-temp-eshell
+ (eshell-match-command-output "+ 1 2; + $_ 4"
+ "3\n6\n")))
+
+(ert-deftest esh-var-test/last-arg-var-indices ()
+ "Test using the \"last arg\" ($_) variable with indices"
+ (with-temp-eshell
+ (eshell-match-command-output "+ 1 2; + $_[0] 4"
+ "3\n5\n")
+ (eshell-match-command-output "+ 1 2; + $_[1] 4"
+ "3\n6\n")))
+
+(ert-deftest esh-var-test/last-arg-var-split-indices ()
+ "Test using the \"last arg\" ($_) variable with split indices"
+ (with-temp-eshell
+ (eshell-match-command-output "concat 01:02 03:04; + $_[0][: 1] 5"
+ "01:0203:04\n7\n")
+ (eshell-match-command-output "concat 01:02 03:04; echo \"$_[0][: 1]\""
+ "01:0203:04\n02\n")))
+
+;; esh-var-tests.el ends here
diff --git a/test/lisp/eshell/eshell-tests-helpers.el b/test/lisp/eshell/eshell-tests-helpers.el
new file mode 100644
index 00000000000..73abfcbb557
--- /dev/null
+++ b/test/lisp/eshell/eshell-tests-helpers.el
@@ -0,0 +1,140 @@
+;;; eshell-tests-helpers.el --- Eshell test suite helpers -*- lexical-binding:t -*-
+
+;; Copyright (C) 1999-2022 Free Software Foundation, Inc.
+
+;; Author: John Wiegley <johnw@gnu.org>
+
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Eshell test suite helpers.
+
+;;; Code:
+
+(require 'ert)
+(require 'ert-x)
+(require 'esh-mode)
+(require 'eshell)
+
+(defvar eshell-history-file-name nil)
+
+(defvar eshell-test--max-subprocess-time 5
+ "The maximum amount of time to wait for a subprocess to finish, in seconds.
+See `eshell-wait-for-subprocess'.")
+
+(defmacro with-temp-eshell (&rest body)
+ "Evaluate BODY in a temporary Eshell buffer."
+ `(save-current-buffer
+ (ert-with-temp-directory eshell-directory-name
+ (let* (;; We want no history file, so prevent Eshell from falling
+ ;; back on $HISTFILE.
+ (process-environment (cons "HISTFILE" process-environment))
+ (eshell-history-file-name nil)
+ (eshell-buffer (eshell t)))
+ (unwind-protect
+ (with-current-buffer eshell-buffer
+ ,@body)
+ (let (kill-buffer-query-functions)
+ (kill-buffer eshell-buffer)))))))
+
+(defmacro eshell-with-temp-buffer (bufname text &rest body)
+ "Create a temporary buffer containing TEXT and evaluate BODY there.
+BUFNAME will be set to the name of the temporary buffer."
+ (declare (indent 2))
+ `(with-temp-buffer
+ (insert ,text)
+ (rename-buffer "eshell-temp-buffer" t)
+ (let ((,bufname (buffer-name)))
+ ,@body)))
+
+(defun eshell-wait-for-subprocess (&optional all)
+ "Wait until there is no interactive subprocess running in Eshell.
+If ALL is non-nil, wait until there are no Eshell subprocesses at
+all running.
+
+If this takes longer than `eshell-test--max-subprocess-time',
+raise an error."
+ (let ((start (current-time)))
+ (while (if all eshell-process-list (eshell-interactive-process-p))
+ (when (> (float-time (time-since start))
+ eshell-test--max-subprocess-time)
+ (error "timed out waiting for subprocess(es)"))
+ (sit-for 0.1))))
+
+(defun eshell-insert-command (command &optional func)
+ "Insert a COMMAND at the end of the buffer.
+After inserting, call FUNC. If FUNC is nil, instead call
+`eshell-send-input'."
+ (goto-char eshell-last-output-end)
+ (insert-and-inherit command)
+ (funcall (or func 'eshell-send-input)))
+
+(defun eshell-match-output (regexp)
+ "Test whether the output of the last command matches REGEXP."
+ (string-match-p
+ regexp (buffer-substring-no-properties
+ (eshell-beginning-of-output) (eshell-end-of-output))))
+
+(defun eshell-match-output--explainer (regexp)
+ "Explain the result of `eshell-match-output'."
+ `(mismatched-output
+ (command ,(buffer-substring-no-properties
+ eshell-last-input-start eshell-last-input-end))
+ (output ,(buffer-substring-no-properties
+ (eshell-beginning-of-output) (eshell-end-of-output)))
+ (regexp ,regexp)))
+
+(put 'eshell-match-output 'ert-explainer #'eshell-match-output--explainer)
+
+(defun eshell-match-command-output (command regexp &optional func)
+ "Insert a COMMAND at the end of the buffer and match the output with REGEXP."
+ (eshell-insert-command command func)
+ (eshell-wait-for-subprocess)
+ (should (eshell-match-output regexp)))
+
+(defvar eshell-history-file-name)
+
+(defun eshell-test-command-result (command)
+ "Like `eshell-command-result', but not using HOME."
+ (ert-with-temp-directory eshell-directory-name
+ (let ((eshell-history-file-name nil))
+ (eshell-command-result command))))
+
+(defun eshell-command-result--equal (_command actual expected)
+ "Compare the ACTUAL result of a COMMAND with its EXPECTED value."
+ (equal actual expected))
+
+(defun eshell-command-result--equal-explainer (command actual expected)
+ "Explain the result of `eshell-command-result--equal'."
+ `(nonequal-result
+ (command ,command)
+ (result ,actual)
+ (expected ,expected)))
+
+(put 'eshell-command-result--equal 'ert-explainer
+ #'eshell-command-result--equal-explainer)
+
+(defun eshell-command-result-equal (command result)
+ "Execute COMMAND non-interactively and compare it to RESULT."
+ (should (eshell-command-result--equal
+ command
+ (eshell-test-command-result command)
+ result)))
+
+(provide 'eshell-tests-helpers)
+
+;;; eshell-tests-helpers.el ends here
diff --git a/test/lisp/eshell/eshell-tests.el b/test/lisp/eshell/eshell-tests.el
index 4e0d6dc7621..d5112146c2d 100644
--- a/test/lisp/eshell/eshell-tests.el
+++ b/test/lisp/eshell/eshell-tests.el
@@ -1,6 +1,6 @@
-;;; tests/eshell-tests.el --- Eshell test suite
+;;; eshell-tests.el --- Eshell test suite -*- lexical-binding:t -*-
-;; Copyright (C) 1999-2017 Free Software Foundation, Inc.
+;; Copyright (C) 1999-2022 Free Software Foundation, Inc.
;; Author: John Wiegley <johnw@gnu.org>
@@ -26,176 +26,115 @@
;;; Code:
(require 'ert)
+(require 'ert-x)
+(require 'esh-mode)
(require 'eshell)
-
-(defmacro with-temp-eshell (&rest body)
- "Evaluate BODY in a temporary Eshell buffer."
- `(let* ((eshell-directory-name (make-temp-file "eshell" t))
- (eshell-history-file-name nil)
- (eshell-buffer (eshell t)))
- (unwind-protect
- (with-current-buffer eshell-buffer
- ,@body)
- (let (kill-buffer-query-functions)
- (kill-buffer eshell-buffer)
- (delete-directory eshell-directory-name t)))))
-
-(defun eshell-insert-command (text &optional func)
- "Insert a command at the end of the buffer."
- (goto-char eshell-last-output-end)
- (insert-and-inherit text)
- (funcall (or func 'eshell-send-input)))
-
-(defun eshell-match-result (regexp)
- "Check that text after `eshell-last-input-end' matches REGEXP."
- (goto-char eshell-last-input-end)
- (should (string-match-p regexp (buffer-substring-no-properties
- (point) (point-max)))))
-
-(defun eshell-command-result-p (text regexp &optional func)
- "Insert a command at the end of the buffer."
- (eshell-insert-command text func)
- (eshell-match-result regexp))
-
-(defun eshell-test-command-result (command)
- "Like `eshell-command-result', but not using HOME."
- (let ((eshell-directory-name (make-temp-file "eshell" t))
- (eshell-history-file-name nil))
- (unwind-protect
- (eshell-command-result command)
- (delete-directory eshell-directory-name t))))
+(require 'eshell-tests-helpers
+ (expand-file-name "eshell-tests-helpers"
+ (file-name-directory (or load-file-name
+ default-directory))))
;;; Tests:
-(ert-deftest eshell-test/simple-command-result ()
- "Test `eshell-command-result' with a simple command."
- (should (equal (eshell-test-command-result "+ 1 2") 3)))
-
-(ert-deftest eshell-test/lisp-command ()
- "Test `eshell-command-result' with an elisp command."
- (should (equal (eshell-test-command-result "(+ 1 2)") 3)))
-
-(ert-deftest eshell-test/for-loop ()
- "Test `eshell-command-result' with a for loop.."
- (let ((process-environment (cons "foo" process-environment)))
- (should (equal (eshell-test-command-result
- "for foo in 5 { echo $foo }") 5))))
-
-(ert-deftest eshell-test/for-name-loop () ;Bug#15231
- "Test `eshell-command-result' with a for loop using `name'."
- (let ((process-environment (cons "name" process-environment)))
- (should (equal (eshell-test-command-result
- "for name in 3 { echo $name }") 3))))
-
-(ert-deftest eshell-test/for-name-shadow-loop () ; bug#15372
- "Test `eshell-command-result' with a for loop using an env-var."
- (let ((process-environment (cons "name=env-value" process-environment)))
- (with-temp-eshell
- (eshell-command-result-p "echo $name; for name in 3 { echo $name }; echo $name"
- "env-value\n3\nenv-value\n"))))
-
-(ert-deftest eshell-test/lisp-command-args ()
- "Test `eshell-command-result' with elisp and trailing args.
-Test that trailing arguments outside the S-expression are
-ignored. e.g. \"(+ 1 2) 3\" => 3"
- (should (equal (eshell-test-command-result "(+ 1 2) 3") 3)))
-
-(ert-deftest eshell-test/subcommand ()
- "Test `eshell-command-result' with a simple subcommand."
- (should (equal (eshell-test-command-result "{+ 1 2}") 3)))
-
-(ert-deftest eshell-test/subcommand-args ()
- "Test `eshell-command-result' with a subcommand and trailing args.
-Test that trailing arguments outside the subcommand are ignored.
-e.g. \"{+ 1 2} 3\" => 3"
- (should (equal (eshell-test-command-result "{+ 1 2} 3") 3)))
-
-(ert-deftest eshell-test/subcommand-lisp ()
- "Test `eshell-command-result' with an elisp subcommand and trailing args.
-Test that trailing arguments outside the subcommand are ignored.
-e.g. \"{(+ 1 2)} 3\" => 3"
- (should (equal (eshell-test-command-result "{(+ 1 2)} 3") 3)))
-
-(ert-deftest eshell-test/interp-cmd ()
- "Interpolate command result"
- (should (equal (eshell-test-command-result "+ ${+ 1 2} 3") 6)))
-
-(ert-deftest eshell-test/interp-lisp ()
- "Interpolate Lisp form evaluation"
- (should (equal (eshell-test-command-result "+ $(+ 1 2) 3") 6)))
-
-(ert-deftest eshell-test/interp-concat ()
- "Interpolate and concat command"
- (should (equal (eshell-test-command-result "+ ${+ 1 2}3 3") 36)))
-
-(ert-deftest eshell-test/interp-concat-lisp ()
- "Interpolate and concat Lisp form"
- (should (equal (eshell-test-command-result "+ $(+ 1 2)3 3") 36)))
-
-(ert-deftest eshell-test/interp-concat2 ()
- "Interpolate and concat two commands"
- (should (equal (eshell-test-command-result "+ ${+ 1 2}${+ 1 2} 3") 36)))
-
-(ert-deftest eshell-test/interp-concat-lisp2 ()
- "Interpolate and concat two Lisp forms"
- (should (equal (eshell-test-command-result "+ $(+ 1 2)$(+ 1 2) 3") 36)))
-
-(ert-deftest eshell-test/window-height ()
- "$LINES should equal (window-height)"
- (should (eshell-test-command-result "= $LINES (window-height)")))
-
-(ert-deftest eshell-test/window-width ()
- "$COLUMNS should equal (window-width)"
- (should (eshell-test-command-result "= $COLUMNS (window-width)")))
-
-(ert-deftest eshell-test/last-result-var ()
- "Test using the \"last result\" ($$) variable"
+(ert-deftest eshell-test/pipe-headproc ()
+ "Check that piping a non-process to a process command waits for the process"
+ (skip-unless (executable-find "cat"))
+ (with-temp-eshell
+ (eshell-match-command-output "echo hi | *cat"
+ "hi")))
+
+(ert-deftest eshell-test/pipe-tailproc ()
+ "Check that piping a process to a non-process command waits for the process"
+ (skip-unless (executable-find "echo"))
(with-temp-eshell
- (eshell-command-result-p "+ 1 2; + $$ 2"
- "3\n5\n")))
+ (eshell-match-command-output "*echo hi | echo bye"
+ "bye\nhi\n")))
-(ert-deftest eshell-test/last-result-var2 ()
- "Test using the \"last result\" ($$) variable twice"
+(ert-deftest eshell-test/pipe-headproc-stdin ()
+ "Check that standard input is sent to the head process in a pipeline"
+ (skip-unless (and (executable-find "tr")
+ (executable-find "rev")))
+ (with-temp-eshell
+ (eshell-insert-command "tr a-z A-Z | rev")
+ (eshell-insert-command "hello")
+ (eshell-send-eof-to-process)
+ (eshell-wait-for-subprocess)
+ (should (eshell-match-output "OLLEH\n"))))
+
+(ert-deftest eshell-test/pipe-subcommand ()
+ "Check that piping with an asynchronous subcommand works"
+ (skip-unless (and (executable-find "echo")
+ (executable-find "cat")))
(with-temp-eshell
- (eshell-command-result-p "+ 1 2; + $$ $$"
- "3\n6\n")))
+ (eshell-match-command-output "echo ${*echo hi} | *cat"
+ "hi")))
-(ert-deftest eshell-test/last-arg-var ()
- "Test using the \"last arg\" ($_) variable"
+(ert-deftest eshell-test/pipe-subcommand-with-pipe ()
+ "Check that piping with an asynchronous subcommand with its own pipe works"
+ (skip-unless (and (executable-find "echo")
+ (executable-find "cat")))
(with-temp-eshell
- (eshell-command-result-p "+ 1 2; + $_ 4"
- "3\n6\n")))
+ (eshell-match-command-output "echo ${*echo hi | *cat} | *cat"
+ "hi")))
+
+(ert-deftest eshell-test/subcommand-reset-in-pipeline ()
+ "Check that subcommands reset `eshell-in-pipeline-p'."
+ (skip-unless (executable-find "cat"))
+ (dolist (template '("echo {%s} | *cat"
+ "echo ${%s} | *cat"
+ "*cat $<%s> | *cat"))
+ (eshell-command-result-equal
+ (format template "echo $eshell-in-pipeline-p")
+ nil)
+ (eshell-command-result-equal
+ (format template "echo | echo $eshell-in-pipeline-p")
+ "last")
+ (eshell-command-result-equal
+ (format template "echo $eshell-in-pipeline-p | echo")
+ "first")
+ (eshell-command-result-equal
+ (format template "echo | echo $eshell-in-pipeline-p | echo")
+ "t")))
+
+(ert-deftest eshell-test/lisp-reset-in-pipeline ()
+ "Check that interpolated Lisp forms reset `eshell-in-pipeline-p'."
+ (skip-unless (executable-find "cat"))
+ (dolist (template '("echo (%s) | *cat"
+ "echo $(%s) | *cat"))
+ (eshell-command-result-equal
+ (format template "format \"%s\" eshell-in-pipeline-p")
+ "nil")))
(ert-deftest eshell-test/escape-nonspecial ()
"Test that \"\\c\" and \"c\" are equivalent when \"c\" is not a
special character."
(with-temp-eshell
- (eshell-command-result-p "echo he\\llo"
- "hello\n")))
+ (eshell-match-command-output "echo he\\llo"
+ "hello\n")))
(ert-deftest eshell-test/escape-nonspecial-unicode ()
"Test that \"\\c\" and \"c\" are equivalent when \"c\" is a
unicode character (unicode characters are nonspecial by
definition)."
(with-temp-eshell
- (eshell-command-result-p "echo Vid\\éos"
- "Vidéos\n")))
+ (eshell-match-command-output "echo Vid\\éos"
+ "Vidéos\n")))
(ert-deftest eshell-test/escape-nonspecial-quoted ()
"Test that the backslash is preserved for escaped nonspecial
chars"
(with-temp-eshell
- (eshell-command-result-p "echo \"h\\i\""
- ;; Backslashes are doubled for regexp.
- "h\\\\i\n")))
+ (eshell-match-command-output "echo \"h\\i\""
+ ;; Backslashes are doubled for regexp.
+ "h\\\\i\n")))
(ert-deftest eshell-test/escape-special-quoted ()
"Test that the backslash is not preserved for escaped special
chars"
(with-temp-eshell
- (eshell-command-result-p "echo \"h\\\\i\""
- ;; Backslashes are doubled for regexp.
- "h\\\\i\n")))
+ (eshell-match-command-output "echo \"\\\"hi\\\\\""
+ ;; Backslashes are doubled for regexp.
+ "\\\"hi\\\\\n")))
(ert-deftest eshell-test/command-running-p ()
"Modeline should show no command running"
@@ -229,16 +168,15 @@ chars"
(> count 0))
(sit-for 1)
(setq count (1- count))))
- (eshell-match-result "alpha\n")))
+ (should (eshell-match-output "alpha\n"))))
(ert-deftest eshell-test/flush-output ()
"Test flushing of previous output"
(with-temp-eshell
(eshell-insert-command "echo alpha")
(eshell-kill-output)
- (eshell-match-result (regexp-quote "*** output flushed ***\n"))
- (should (forward-line))
- (should (= (point) eshell-last-output-start))))
+ (should (eshell-match-output
+ (concat "^" (regexp-quote "*** output flushed ***\n") "$")))))
(ert-deftest eshell-test/run-old-command ()
"Re-run an old command"
@@ -247,6 +185,6 @@ chars"
(goto-char eshell-last-input-start)
(string= (eshell-get-old-input) "echo alpha")))
-(provide 'esh-test)
+(provide 'eshell-tests)
-;;; tests/eshell-tests.el ends here
+;;; eshell-tests.el ends here