;;; tramp-tests.el --- Tests of remote file access  -*- lexical-binding:t -*-

;; Copyright (C) 2013-2026 Free Software Foundation, Inc.

;; Author: Michael Albinus <michael.albinus@gmx.de>

;; 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:

;; Some of the tests require access to a remote host files.  Since
;; this could be problematic, a mock-up connection method "mock" is
;; used.  Emulating a remote connection, it simply calls "sh -i".
;; Tramp's file name handlers still run, so this test is sufficient
;; except for connection establishing.

;; If you want to test a real Tramp connection, set
;; $REMOTE_TEMPORARY_FILE_DIRECTORY to a suitable value in order to
;; overwrite the default value.  If you want to skip tests accessing a
;; remote host, set this environment variable to "/dev/null" or
;; whatever is appropriate on your system.

;; All temporary Tramp test files are removed prior test run.
;; Therefore, two test runs cannot be performed in parallel.

;; The environment variable $TRAMP_TEST_CLEANUP_TEMP_FILES, when set,
;; forces the removal of all temporary Tramp files prior test run.
;; This shouldn't be set if the test suite runs in parallel using
;; Tramp on a production system.

;; For slow remote connections, `tramp-test45-asynchronous-requests'
;; might be too heavy.  Setting $REMOTE_PARALLEL_PROCESSES to a proper
;; value less than 10 could help.

;; This test suite obeys the environment variable $EMACS_EMBA_CI, used
;; on the Emacs CI/CD platforms.

;; The following test tags are used: `:expensive-test',
;; `:tramp-asynchronous-processes' and `:unstable'.

;; A whole test run can be performed calling the command `tramp-test-all'.

;;; Code:

(require 'cl-lib)
(require 'dired)
(require 'dired-aux)
(require 'tramp)
(require 'ert-x)
(require 'filenotify)
(require 'tar-mode)
(require 'trace)
(require 'vc)
(require 'vc-bzr)
(require 'vc-git)
(require 'vc-hg)

(declare-function edebug-mode "edebug")
(declare-function project-mode-line-format "project")
(declare-function tramp-check-remote-uname "tramp-sh")
(declare-function tramp-file-name-with-sudo "tramp-cmds")
(declare-function tramp-find-executable "tramp-sh")
(declare-function tramp-get-remote-chmod-h "tramp-sh")
(declare-function tramp-get-remote-path "tramp-sh")
(declare-function tramp-get-remote-perl "tramp-sh")
(declare-function tramp-get-remote-stat "tramp-sh")
(declare-function tramp-list-tramp-buffers "tramp-cmds")
(declare-function tramp-method-out-of-band-p "tramp-sh")
(declare-function tramp-smb-get-localname "tramp-smb")
(defvar ange-ftp-make-backup-files)
(defvar auto-revert-notify-watch-descriptor)
(defvar auto-revert-remote-files)
(defvar auto-revert-use-notify)
(defvar comp-warn-primitives)
(defvar tramp-connection-properties)
(defvar tramp-copy-size-limit)
(defvar tramp-fuse-remove-hidden-files)
(defvar tramp-fuse-unmount-on-cleanup)
(defvar tramp-inline-compress-start-size)
(defvar tramp-persistency-file-name)
(defvar tramp-remote-path)
(defvar tramp-remote-process-environment)
(defvar tramp-use-connection-share)

;; Declared in Emacs 29.1.
(defvar completions-max-height)

;; Declared in Emacs 30.1.
(defvar project-mode-line)
(defvar remote-file-name-access-timeout)
(defvar remote-file-name-inhibit-delete-by-moving-to-trash)

;; `ert-remote-temporary-file-directory', `ert-with-temp-file' and
;; `ert-with-temp-directory' were introduced in Emacs 29.1.  Adapting
;; `tramp-remote-path' happens also there.
(unless (boundp 'ert-remote-temporary-file-directory)
  (eval-and-compile
    ;; There is no default value on w32 systems, which could work out
    ;; of the box.
    (defvar ert-remote-temporary-file-directory
      (cond
       ((getenv "REMOTE_TEMPORARY_FILE_DIRECTORY"))
       ((eq system-type 'windows-nt) null-device)
       (t (add-to-list
           'tramp-methods
           `("mock"
	     (tramp-login-program	,tramp-encoding-shell)
	     (tramp-login-args		(("-i")))
             (tramp-direct-async	("-c"))
             (tramp-tmpdir		,temporary-file-directory)
	     (tramp-remote-shell	,tramp-default-remote-shell)
	     (tramp-remote-shell-args	("-c"))
	     (tramp-connection-timeout	10)))
          (add-to-list
           'tramp-default-host-alist
           `("\\`mock\\'" nil ,(system-name)))
          ;; Emacs's Makefile sets $HOME to a nonexistent value.
          ;; Needed in batch mode only, therefore.
          (unless (and (null noninteractive) (file-directory-p "~/"))
            (setenv "HOME" temporary-file-directory))
          (format "/mock::%s" temporary-file-directory)))
      "Temporary directory for remote file tests.")

    (defvar ert-temp-file-prefix "emacs-test-"
      "Prefix used by `ert-with-temp-file' and `ert-with-temp-directory'.")

    (defvar ert-temp-file-suffix nil
      "Suffix used by `ert-with-temp-file' and `ert-with-temp-directory'.")

    (defun ert--with-temp-file-generate-suffix (filename)
      "Generate temp file suffix from FILENAME."
      (thread-last
	(file-name-base filename)
	(replace-regexp-in-string (rx string-start
                                      (group (+? not-newline))
                                      (regexp "-?tests?")
                                      string-end)
				  "\\1")
	(concat "-")))

    (defmacro ert-with-temp-file (name &rest body)
      "Bind NAME to the name of a new temporary file and evaluate BODY."
      (declare (indent 1) (debug (symbolp body)))
      (cl-check-type name symbol)
      (let (keyw prefix suffix directory text extra-keywords buffer coding)
	(while (keywordp (setq keyw (car body)))
	  (setq body (cdr body))
	  (pcase keyw
            (:prefix (setq prefix (pop body)))
            (:suffix (setq suffix (pop body)))
            ;; This is only for internal use by `ert-with-temp-directory'
            ;; and is therefore not documented.
            (:directory (setq directory (pop body)))
            (:text (setq text (pop body)))
            (:buffer (setq buffer (pop body)))
            (:coding (setq coding (pop body)))
            (_ (push keyw extra-keywords) (pop body))))
	(when extra-keywords
	  (error
	   "Invalid keywords: %s" (mapconcat #'symbol-name extra-keywords " ")))
	(let ((temp-file (make-symbol "temp-file"))
              (prefix (or prefix ert-temp-file-prefix))
              (suffix (or suffix ert-temp-file-suffix
			  (ert--with-temp-file-generate-suffix
			   (or (macroexp-file-name) buffer-file-name)))))
	  `(let* ((coding-system-for-write ,(or coding coding-system-for-write))
		  (,temp-file (,(if directory 'file-name-as-directory 'identity)
                               (make-temp-file
				,prefix ,directory ,suffix ,text)))
		  (,name ,(if directory
                              `(file-name-as-directory ,temp-file)
                            temp-file))
		  ,@(when buffer
                      (list `(,buffer (find-file-literally ,temp-file)))))
             (unwind-protect
		 (progn ,@body)
               (ignore-errors
		 ,@(when buffer
                     (list `(with-current-buffer ,buffer
                              (set-buffer-modified-p nil))
			   `(kill-buffer ,buffer))))
               (ignore-errors
		 ,(if directory
                      `(delete-directory ,temp-file :recursive)
                    `(delete-file ,temp-file))))))))

    (defmacro ert-with-temp-directory (name &rest body)
      "Bind NAME to the name of a new temporary directory and evaluate BODY."
      (declare (indent 1) (debug (symbolp body)))
      (let ((tail body) keyw)
	(while (keywordp (setq keyw (car tail)))
	  (setq tail (cddr tail))
	  (pcase keyw (:text (error "Invalid keyword for directory: :text")))))
      `(ert-with-temp-file ,name
	 :directory t
	 ,@body))))

;; Beautify batch mode.
(when noninteractive
  ;; Suppress nasty messages.
  (fset #'shell-command-sentinel #'ignore)
  ;; We do not want to be interrupted.
  (fset #'tramp-action-yesno
	(lambda (_proc vec)
	  (tramp-send-string vec (concat "yes" tramp-local-end-of-line)) t))
  (eval-after-load 'tramp-gvfs
    '(fset 'tramp-gvfs-handler-askquestion
	   (lambda (_message _choices) '(t nil 0)))))

(setq auth-source-cache-expiry nil
      auth-source-save-behavior nil
      auto-revert-remote-files t
      auto-revert-use-notify t
      ert-batch-backtrace-right-margin nil
      ert-remote-temporary-file-directory
      (let ((tramp-show-ad-hoc-proxies t) (non-essential t))
	(expand-file-name ert-remote-temporary-file-directory))
      password-cache-expiry nil
      remote-file-name-inhibit-cache nil
      tramp-allow-unsafe-temporary-files t
      tramp-cache-read-persistent-data t ;; For auth-sources.
      tramp-copy-size-limit nil
      tramp-error-show-message-timeout nil
      tramp-persistency-file-name nil
      tramp-verbose 0
      vc-handled-backends (unless noninteractive vc-handled-backends))

(defconst tramp-test-vec
  (and (file-remote-p ert-remote-temporary-file-directory)
       (tramp-dissect-file-name ert-remote-temporary-file-directory))
  "The used `tramp-file-name' structure.")

(defconst tramp-test-name-prefix "tramp-test"
  "Prefix to use for temporary test files.")

(defun tramp--test-make-temp-prefix (&optional local quoted)
  "Return a temporary file name prefix for test.
If LOCAL is non-nil, a local file name is returned.
If QUOTED is non-nil, the local part of the file name is quoted."
  (funcall
   (if quoted #'file-name-quote #'identity)
   (expand-file-name
    tramp-test-name-prefix
    (if local temporary-file-directory ert-remote-temporary-file-directory))))

(defun tramp--test-make-temp-name (&optional local quoted)
  "Return a temporary file name for test.
If LOCAL is non-nil, a local file name is returned.
If QUOTED is non-nil, the local part of the file name is quoted.
The temporary file is not created."
  (make-temp-name (tramp--test-make-temp-prefix local quoted)))

;; If `system-type' is `windows-nt', making symbolic links is not
;; supported.  Method "smb" supports `make-symbolic-link' only if the
;; remote host has CIFS capabilities.  tramp-adb.el, tramp-gvfs.el,
;; tramp-rclone.el and tramp-sshfs.el do not support symbolic links at
;; all.
(defmacro tramp--test-ignore-make-symbolic-link-error (&rest body)
  "Run BODY, ignoring \"make-symbolic-link not supported\" file error."
  (declare (indent defun) (debug (body)))
  `(condition-case err
       (progn ,@body)
     (file-error
      (unless (string-match-p
	       (rx bol (| "make-symbolic-link not supported"
			  (: "Making symbolic link"
			     (? ":") " Operation not permitted")))
	       (error-message-string err))
	(signal (car err) (cdr err))))))

;; Don't print messages in nested `tramp--test-instrument-test-case' calls.
(defvar tramp--test-instrument-test-case-p nil
  "Whether `tramp--test-instrument-test-case' run.
This shall be used dynamically bound only.")

;; When `tramp-verbose' is greater than 10, and you want to trace
;; other functions as well, do something like
;; (let ((tramp-trace-functions '(file-name-non-special)))
;;   (tramp--test-instrument-test-case 11
;;     ...))
(defmacro tramp--test-instrument-test-case (verbose &rest body)
  "Run BODY with `tramp-verbose' equal VERBOSE.
Print the content of the Tramp connection and debug buffers, if
`tramp-verbose' is greater than 3.  Print traces if `tramp-verbose'
is greater than 10.
`should-error' is not handled properly.  BODY shall not contain a timeout."
  (declare (indent 1) (debug (natnump body)))
  `(let* ((tramp-verbose (max (or ,verbose 0) (or tramp-verbose 0)))
	  (debug-ignored-errors
	   (append
	    '("\\`make-symbolic-link not supported\\'"
	      "\\`error with add-name-to-file")
	    debug-ignored-errors))
	  inhibit-message)
     (unwind-protect
	 (let ((tramp--test-instrument-test-case-p t)) ,@body)
       ;; Unwind forms.
       (when (and (null tramp--test-instrument-test-case-p) (> tramp-verbose 3))
	 (untrace-all)
	 (dolist (buf (tramp-list-tramp-buffers))
	   (message ";; %s\n%s" buf (tramp-get-buffer-string buf))
	   (kill-buffer buf))))))

(defsubst tramp--test-message (fmt-string &rest arguments)
  "Emit a message into \"ERT *Messages*\" and the trace buffer."
  (declare (tramp-suppress-trace t))
  (when (get-buffer trace-buffer)
    (trace-values (apply #'format fmt-string arguments)))
  (tramp--test-instrument-test-case 0
    (apply #'tramp-message tramp-test-vec 0 fmt-string arguments)))

(defsubst tramp--test-backtrace ()
  "Dump a backtrace into ERT *Messages*."
  (tramp--test-instrument-test-case 10
    (tramp-backtrace tramp-test-vec)))

(defmacro tramp--test-print-duration (message &rest body)
  "Run BODY and print a message with duration, prompted by MESSAGE."
  (declare (indent 1) (debug (stringp body)))
  `(let ((start (current-time)))
     (unwind-protect
	 (progn ,@body)
       (tramp--test-message
	"%s %f sec" ,message (float-time (time-subtract nil start))))))

(defvar tramp--test-enabled-checked nil
  "Cached result of `tramp--test-enabled'.
If the function did run, the value is a cons cell, the `cdr'
being the result.")

(defun tramp--test-enabled ()
  "Whether remote file access is enabled."
  (unless (consp tramp--test-enabled-checked)
    (setq
     tramp--test-enabled-checked
     (cons
      t (ignore-errors
	  (and
	   (file-remote-p ert-remote-temporary-file-directory)
	   (file-directory-p ert-remote-temporary-file-directory)
	   (file-writable-p ert-remote-temporary-file-directory))))))

  (when (cdr tramp--test-enabled-checked)
    (ignore-errors
      ;; Remove old test files.
      (dolist (dir `(,temporary-file-directory
		     ,tramp-compat-temporary-file-directory
		     ,ert-remote-temporary-file-directory))
	(dolist (file (directory-files
		       dir 'full
		       (rx-to-string
			`(: bos (? ".#")
			    (| ,tramp-test-name-prefix
			       ,(if (getenv "TRAMP_TEST_CLEANUP_TEMP_FILES")
				    tramp-temp-name-prefix 'unmatchable))))))

	  ;; Exclude sockets and FUSE mount points.
	  (unless
	      (or (string-prefix-p
		   "srw" (file-attribute-modes (file-attributes file)))
		  ;; Prior Emacs 31.1, the FUSE mount points where
		  ;; "tramp.rclone.*" and "tramp.sshfs.*".  We should
		  ;; exclude them as well, in order to avoid trouble.
		  (string-match-p
		   (rx bos (| (literal tramp-fuse-name-prefix)
			      (literal tramp-temp-name-prefix))
		       (| "rclone" "sshfs") ".")
		   (file-name-nondirectory file)))
	    (tramp--test-message "Delete %s" file)
	    (ignore-errors ;; Wrong permissions?
	      (if (file-directory-p file)
		  (delete-directory file 'recursive)
		(delete-file file)))))))
    ;; Cleanup connection.
    (tramp-cleanup-connection tramp-test-vec nil 'keep-password))

  ;; Return result.
  (cdr tramp--test-enabled-checked))

(ert-deftest tramp-test00-availability ()
  "Test availability of Tramp functions."
  :expected-result (if (tramp--test-enabled) :passed :failed)
  (tramp--test-message
   "Remote directory: `%s'" ert-remote-temporary-file-directory)
  (should (ignore-errors
	    (and
	     (file-remote-p ert-remote-temporary-file-directory)
	     (file-directory-p ert-remote-temporary-file-directory)
	     (file-writable-p ert-remote-temporary-file-directory)))))

(ert-deftest tramp-test01-file-name-syntax ()
  "Check remote file name syntax."
  (let ((syntax tramp-syntax))
    (unwind-protect
	(progn
	  (tramp-change-syntax 'default)
	  ;; Simple cases.
	  (should (tramp-tramp-file-p "/method::"))
	  (should (tramp-tramp-file-p "/method:host:"))
	  (should (tramp-tramp-file-p "/method:user@:"))
	  (should (tramp-tramp-file-p "/method:user@host:"))
	  (should (tramp-tramp-file-p "/method:user@email@host:"))

	  ;; Using a port.
	  (should (tramp-tramp-file-p "/method:host#1234:"))
	  (should (tramp-tramp-file-p "/method:user@host#1234:"))

	  ;; Using an IPv4 address.
	  (should (tramp-tramp-file-p "/method:1.2.3.4:"))
	  (should (tramp-tramp-file-p "/method:user@1.2.3.4:"))

	  ;; Using an IPv4 address with port.
	  (should (tramp-tramp-file-p "/method:1.2.3.4#1234:"))
	  (should (tramp-tramp-file-p "/method:user@1.2.3.4#1234:"))

	  ;; Using an IPv6 address.
	  (should (tramp-tramp-file-p "/method:[::1]:"))
	  (should (tramp-tramp-file-p "/method:user@[::1]:"))

	  ;; Using an IPv4 mapped IPv6 address.
	  (should (tramp-tramp-file-p "/method:[::ffff:1.2.3.4]:"))
	  (should (tramp-tramp-file-p "/method:user@[::ffff:1.2.3.4]:"))

	  ;; Using an IPv6 address with port.
	  (should (tramp-tramp-file-p "/method:[::1]#1234:"))
	  (should (tramp-tramp-file-p "/method:user@[::1]#1234:"))

	  ;; Local file name part.
	  (should (tramp-tramp-file-p "/method:::"))
	  (should (tramp-tramp-file-p "/method::/:"))
	  (should (tramp-tramp-file-p "/method::/path/to/file"))
	  (should (tramp-tramp-file-p "/method::/:/path/to/file"))
	  (should (tramp-tramp-file-p "/method::file"))
	  (should (tramp-tramp-file-p "/method::/:file"))

	  ;; Multihop.
	  (should (tramp-tramp-file-p "/method1:|method2::"))
	  (should
	   (tramp-tramp-file-p "/method1:host1|method2:host2:"))
	  (should
	   (tramp-tramp-file-p "/method1:user1@host1|method2:user2@host2:"))
	  (should
	   (tramp-tramp-file-p
	    "/method1:user1@host1|method2:user2@host2|method3:user3@host3:"))

	  ;; No strings.
	  (should-not (tramp-tramp-file-p nil))
	  (should-not (tramp-tramp-file-p 'symbol))
	  ;; No newline or linefeed.
	  (should-not (tramp-tramp-file-p "/method::file\nname"))
	  (should-not (tramp-tramp-file-p "/method::file\rname"))
	  ;; Ange-FTP syntax.
	  (should-not (tramp-tramp-file-p "/host:"))
	  (should-not (tramp-tramp-file-p "/user@host:"))
	  (should-not (tramp-tramp-file-p "/1.2.3.4:"))
	  (should-not (tramp-tramp-file-p "/[]:"))
	  (should-not (tramp-tramp-file-p "/[::1]:"))
	  (should-not (tramp-tramp-file-p "/[::ffff:1.2.3.4]:"))
	  (should-not (tramp-tramp-file-p "/host:/:"))
	  (should-not (tramp-tramp-file-p "/host1|host2:"))
	  (should-not (tramp-tramp-file-p "/user1@host1|user2@host2:"))
	  ;; Quote with "/:" suppresses file name handlers.
	  (should-not (tramp-tramp-file-p "/::"))
	  (should-not (tramp-tramp-file-p "/:@:"))
	  (should-not (tramp-tramp-file-p "/:[]:"))
	  ;; When `tramp-mode' is nil, Tramp is not activated.
	  (let (tramp-mode)
	    (should-not (tramp-tramp-file-p "/method:user@host:")))
	  ;; `tramp-ignored-file-name-regexp' suppresses Tramp.
	  (let ((tramp-ignored-file-name-regexp "\\`/method:user@host:"))
	    (should-not (tramp-tramp-file-p "/method:user@host:")))
	  ;; Methods shall be at least two characters, except the
	  ;; default method.
	  (let ((system-type 'windows-nt))
	    (should-not (tramp-tramp-file-p "/c:/path/to/file"))
	    (should-not (tramp-tramp-file-p "/c::/path/to/file"))
	    (should (tramp-tramp-file-p "/-::/path/to/file"))
	    (should (tramp-tramp-file-p "/mm::/path/to/file")))
	  (let ((system-type 'gnu/linux))
	    (should-not (tramp-tramp-file-p "/m::/path/to/file"))
	    (should (tramp-tramp-file-p "/-:h:/path/to/file"))
	    (should (tramp-tramp-file-p "/mm::/path/to/file"))))

      ;; Exit.
      (tramp-change-syntax syntax))))

(ert-deftest tramp-test01-file-name-syntax-simplified ()
  "Check simplified file name syntax."
  :tags '(:expensive-test)
  (let ((syntax tramp-syntax))
    (unwind-protect
	(progn
	  (tramp-change-syntax 'simplified)
	  ;; Simple cases.
	  (should (tramp-tramp-file-p "/host:"))
	  (should (tramp-tramp-file-p "/user@:"))
	  (should (tramp-tramp-file-p "/user@host:"))
	  (should (tramp-tramp-file-p "/user@email@host:"))

	  ;; Using a port.
	  (should (tramp-tramp-file-p "/host#1234:"))
	  (should (tramp-tramp-file-p "/user@host#1234:"))

	  ;; Using an IPv4 address.
	  (should (tramp-tramp-file-p "/1.2.3.4:"))
	  (should (tramp-tramp-file-p "/user@1.2.3.4:"))

	  ;; Using an IPv4 address with port.
	  (should (tramp-tramp-file-p "/1.2.3.4#1234:"))
	  (should (tramp-tramp-file-p "/user@1.2.3.4#1234:"))

	  ;; Using an IPv6 address.
	  (should (tramp-tramp-file-p "/[::1]:"))
	  (should (tramp-tramp-file-p "/user@[::1]:"))

	  ;; Using an IPv4 mapped IPv6 address.
	  (should (tramp-tramp-file-p "/[::ffff:1.2.3.4]:"))
	  (should (tramp-tramp-file-p "/user@[::ffff:1.2.3.4]:"))

	  ;; Using an IPv6 address with port.
	  (should (tramp-tramp-file-p "/[::1]#1234:"))
	  (should (tramp-tramp-file-p "/user@[::1]#1234:"))

	  ;; Local file name part.
	  (should (tramp-tramp-file-p "/host::"))
	  (should (tramp-tramp-file-p "/host:/:"))
	  (should (tramp-tramp-file-p "/host:/path/to/file"))
	  (should (tramp-tramp-file-p "/host:/:/path/to/file"))
	  (should (tramp-tramp-file-p "/host:file"))
	  (should (tramp-tramp-file-p "/host:/:file"))

	  ;; Multihop.
	  (should (tramp-tramp-file-p "/host1|host2:"))
	  (should (tramp-tramp-file-p "/user1@host1|user2@host2:"))
	  (should (tramp-tramp-file-p "/user1@host1|user2@host2|user3@host3:"))

	  ;; No strings.
	  (should-not (tramp-tramp-file-p nil))
	  (should-not (tramp-tramp-file-p 'symbol))
	  ;; Quote with "/:" suppresses file name handlers.
	  (should-not (tramp-tramp-file-p "/::"))
	  (should-not (tramp-tramp-file-p "/:@:"))
	  (should-not (tramp-tramp-file-p "/:[]:")))

      ;; Exit.
      (tramp-change-syntax syntax))))

(ert-deftest tramp-test01-file-name-syntax-separate ()
  "Check separate file name syntax."
  :tags '(:expensive-test)
  (let ((syntax tramp-syntax))
    (unwind-protect
	(progn
	  (tramp-change-syntax 'separate)
	  ;; Simple cases.
	  (should (tramp-tramp-file-p "/[method/]"))
	  (should (tramp-tramp-file-p "/[method/host]"))
	  (should (tramp-tramp-file-p "/[method/user@]"))
	  (should (tramp-tramp-file-p "/[method/user@host]"))
	  (should (tramp-tramp-file-p "/[method/user@email@host]"))

	  ;; Using a port.
	  (should (tramp-tramp-file-p "/[method/host#1234]"))
	  (should (tramp-tramp-file-p "/[method/user@host#1234]"))

	  ;; Using an IPv4 address.
	  (should (tramp-tramp-file-p "/[method/1.2.3.4]"))
	  (should (tramp-tramp-file-p "/[method/user@1.2.3.4]"))

	  ;; Using an IPv4 address with port.
	  (should (tramp-tramp-file-p "/[method/1.2.3.4#1234]"))
	  (should (tramp-tramp-file-p "/[method/user@1.2.3.4#1234]"))

	  ;; Using an IPv6 address.
	  (should (tramp-tramp-file-p "/[method/::1]"))
	  (should (tramp-tramp-file-p "/[method/user@::1]"))

	  ;; Using an IPv4 mapped IPv6 address.
	  (should (tramp-tramp-file-p "/[method/::ffff:1.2.3.4]"))
	  (should (tramp-tramp-file-p "/[method/user@::ffff:1.2.3.4]"))

	  ;; Using an IPv6 address with port.
	  (should (tramp-tramp-file-p "/[method/::1#1234]"))
	  (should (tramp-tramp-file-p "/[method/user@::1#1234]"))

	  ;; Local file name part.
	  (should (tramp-tramp-file-p "/[method/]"))
	  (should (tramp-tramp-file-p "/[method/]/:"))
	  (should (tramp-tramp-file-p "/[method/]/path/to/file"))
	  (should (tramp-tramp-file-p "/[method/]/:/path/to/file"))
	  (should (tramp-tramp-file-p "/[method/]file"))
	  (should (tramp-tramp-file-p "/[method/]/:file"))

	  ;; Multihop.
	  (should (tramp-tramp-file-p "/[method1/|method2/]"))
	  (should (tramp-tramp-file-p "/[method1/host1|method2/host2]"))
	  (should
	   (tramp-tramp-file-p
	    "/[method1/user1@host1|method2/user2@host2]"))
	  (should
	   (tramp-tramp-file-p
	    "/[method1/user1@host1|method2/user2@host2|method3/user3@host3]"))

	  ;; No strings.
	  (should-not (tramp-tramp-file-p nil))
	  (should-not (tramp-tramp-file-p 'symbol))
	  ;; Ange-FTP syntax.
	  (should-not (tramp-tramp-file-p "/host:"))
	  (should-not (tramp-tramp-file-p "/user@host:"))
	  (should-not (tramp-tramp-file-p "/1.2.3.4:"))
	  (should-not (tramp-tramp-file-p "/host:/:"))
	  (should-not (tramp-tramp-file-p "/host1|host2:"))
	  (should-not (tramp-tramp-file-p "/user1@host1|user2@host2:"))
	  ;; Quote with "/:" suppresses file name handlers.
	  (should-not (tramp-tramp-file-p "/:[]")))

      ;; Exit.
      (tramp-change-syntax syntax))))

(ert-deftest tramp-test02-file-name-dissect ()
  "Check remote file name components."
  (let ((tramp-default-method "default-method")
	(tramp-default-user "default-user")
	(tramp-default-host "default-host")
	tramp-default-method-alist
	tramp-default-user-alist
	tramp-default-host-alist
	tramp-default-proxies-alist
	;; Suppress method name check.
	(non-essential t)
	;; Suppress check for multihops.
	(tramp-cache-data (make-hash-table :test #'equal))
	(tramp-connection-properties '((nil "login-program" t)))
	(syntax tramp-syntax))
    (unwind-protect
	(progn
	  (tramp-change-syntax 'default)
	  ;; An unknown method shall raise an error.
	  (let (non-essential)
	    (should-error
	     (expand-file-name "/method:user@host:")
	     :type 'user-error))

	  ;; Expand `tramp-default-user' and `tramp-default-host'.
	  (should
	   (string-equal
	    (file-remote-p "/method::")
	    (format "/%s:%s@%s:" "method" "default-user" "default-host")))
	  (should (string-equal (file-remote-p "/method::" 'method) "method"))
	  (should
	   (string-equal (file-remote-p "/method::" 'user) "default-user"))
	  (should
	   (string-equal (file-remote-p "/method::" 'host) "default-host"))
	  (should (string-equal (file-remote-p "/method::" 'localname) ""))
	  (should (string-equal (file-remote-p "/method::" 'hop) nil))

	  ;; Expand `tramp-default-method' and `tramp-default-user'.
	  (should
	   (string-equal
	    (file-remote-p "/-:host:")
	    (format "/%s:%s@%s:" "default-method" "default-user" "host")))
	  (should
	   (string-equal (file-remote-p "/-:host:" 'method) "default-method"))
	  (should
	   (string-equal (file-remote-p "/-:host:" 'user) "default-user"))
	  (should (string-equal (file-remote-p "/-:host:" 'host) "host"))
	  (should (string-equal (file-remote-p "/-:host:" 'localname) ""))
	  (should (string-equal (file-remote-p "/-:host:" 'hop) nil))

	  ;; Expand `tramp-default-method' and `tramp-default-host'.
	  (should
	   (string-equal
	    (file-remote-p "/-:user@:")
	    (format "/%s:%s@%s:" "default-method" "user" "default-host")))
	  (should
	   (string-equal (file-remote-p "/-:user@:" 'method) "default-method"))
	  (should (string-equal (file-remote-p "/-:user@:" 'user) "user"))
	  (should
	   (string-equal (file-remote-p "/-:user@:" 'host) "default-host"))
	  (should (string-equal (file-remote-p "/-:user@:" 'localname) ""))
	  (should (string-equal (file-remote-p "/-:user@:" 'hop) nil))

	  ;; Expand `tramp-default-method'.
	  (should (string-equal
		   (file-remote-p "/-:user@host:")
		   (format "/%s:%s@%s:" "default-method" "user" "host")))
	  (should (string-equal
		   (file-remote-p "/-:user@host:" 'method) "default-method"))
	  (should (string-equal (file-remote-p "/-:user@host:" 'user) "user"))
	  (should (string-equal (file-remote-p "/-:user@host:" 'host) "host"))
	  (should (string-equal (file-remote-p "/-:user@host:" 'localname) ""))
	  (should (string-equal (file-remote-p "/-:user@host:" 'hop) nil))

	  ;; Expand `tramp-default-user'.
	  (should (string-equal
		   (file-remote-p "/method:host:")
		   (format "/%s:%s@%s:" "method" "default-user" "host")))
	  (should
	   (string-equal (file-remote-p "/method:host:" 'method) "method"))
	  (should
	   (string-equal (file-remote-p "/method:host:" 'user) "default-user"))
	  (should (string-equal (file-remote-p "/method:host:" 'host) "host"))
	  (should (string-equal (file-remote-p "/method:host:" 'localname) ""))
	  (should (string-equal (file-remote-p "/method:host:" 'hop) nil))

	  ;; Expand `tramp-default-host'.
	  (should
	   (string-equal
	    (file-remote-p "/method:user@:")
	    (format "/%s:%s@%s:" "method" "user" "default-host")))
	  (should
	   (string-equal (file-remote-p "/method:user@:" 'method) "method"))
	  (should (string-equal (file-remote-p "/method:user@:" 'user) "user"))
	  (should
	   (string-equal (file-remote-p "/method:user@:" 'host) "default-host"))
	  (should (string-equal (file-remote-p "/method:user@:" 'localname) ""))
	  (should (string-equal (file-remote-p "/method:user@:" 'hop) nil))

	  ;; No expansion.
	  (should (string-equal
		   (file-remote-p "/method:user@host:")
		   (format "/%s:%s@%s:" "method" "user" "host")))
	  (should (string-equal
		   (file-remote-p "/method:user@host:" 'method) "method"))
	  (should
	   (string-equal (file-remote-p "/method:user@host:" 'user) "user"))
	  (should
	   (string-equal (file-remote-p "/method:user@host:" 'host) "host"))
	  (should
	   (string-equal (file-remote-p "/method:user@host:" 'localname) ""))
	  (should (string-equal (file-remote-p "/method:user@host:" 'hop) nil))

	  ;; No expansion.
	  (should (string-equal
		   (file-remote-p "/method:user@email@host:")
		   (format "/%s:%s@%s:" "method" "user@email" "host")))
	  (should (string-equal
		   (file-remote-p "/method:user@email@host:" 'method) "method"))
	  (should
	   (string-equal
	    (file-remote-p "/method:user@email@host:" 'user) "user@email"))
	  (should (string-equal
		   (file-remote-p "/method:user@email@host:" 'host) "host"))
	  (should (string-equal
		   (file-remote-p "/method:user@email@host:" 'localname) ""))
	  (should (string-equal
		   (file-remote-p "/method:user@email@host:" 'hop) nil))

	  ;; Expand `tramp-default-method' and `tramp-default-user'.
	  (should
	   (string-equal
	    (file-remote-p "/-:host#1234:")
	    (format "/%s:%s@%s:" "default-method" "default-user" "host#1234")))
	  (should (string-equal
		   (file-remote-p "/-:host#1234:" 'method) "default-method"))
	  (should
	   (string-equal (file-remote-p "/-:host#1234:" 'user) "default-user"))
	  (should
	   (string-equal (file-remote-p "/-:host#1234:" 'host) "host#1234"))
	  (should (string-equal (file-remote-p "/-:host#1234:" 'localname) ""))
	  (should (string-equal (file-remote-p "/-:host#1234:" 'hop) nil))

	  ;; Expand `tramp-default-method'.
	  (should (string-equal
		   (file-remote-p "/-:user@host#1234:")
		   (format "/%s:%s@%s:" "default-method" "user" "host#1234")))
	  (should
	   (string-equal
	    (file-remote-p "/-:user@host#1234:" 'method) "default-method"))
	  (should
	   (string-equal (file-remote-p "/-:user@host#1234:" 'user) "user"))
	  (should
	   (string-equal
	    (file-remote-p "/-:user@host#1234:" 'host) "host#1234"))
	  (should
	   (string-equal (file-remote-p "/-:user@host#1234:" 'localname) ""))
	  (should (string-equal (file-remote-p "/-:user@host#1234:" 'hop) nil))

	  ;; Expand `tramp-default-user'.
	  (should (string-equal
		   (file-remote-p "/method:host#1234:")
		   (format "/%s:%s@%s:" "method" "default-user" "host#1234")))
	  (should (string-equal
		   (file-remote-p "/method:host#1234:" 'method) "method"))
	  (should (string-equal
		   (file-remote-p "/method:host#1234:" 'user) "default-user"))
	  (should (string-equal
		   (file-remote-p "/method:host#1234:" 'host) "host#1234"))
	  (should
	   (string-equal (file-remote-p "/method:host#1234:" 'localname) ""))
	  (should (string-equal (file-remote-p "/method:host#1234:" 'hop) nil))

	  ;; No expansion.
	  (should (string-equal
		   (file-remote-p "/method:user@host#1234:")
		   (format "/%s:%s@%s:" "method" "user" "host#1234")))
	  (should (string-equal
		   (file-remote-p "/method:user@host#1234:" 'method) "method"))
	  (should (string-equal
		   (file-remote-p "/method:user@host#1234:" 'user) "user"))
	  (should (string-equal
		   (file-remote-p "/method:user@host#1234:" 'host) "host#1234"))
	  (should (string-equal
		   (file-remote-p "/method:user@host#1234:" 'localname) ""))
	  (should (string-equal
		   (file-remote-p "/method:user@host#1234:" 'hop) nil))

	  ;; Expand `tramp-default-method' and `tramp-default-user'.
	  (should
	   (string-equal
	    (file-remote-p "/-:1.2.3.4:")
	    (format "/%s:%s@%s:" "default-method" "default-user" "1.2.3.4")))
	  (should
	   (string-equal
	    (file-remote-p "/-:1.2.3.4:" 'method) "default-method"))
	  (should
	   (string-equal (file-remote-p "/-:1.2.3.4:" 'user) "default-user"))
	  (should (string-equal (file-remote-p "/-:1.2.3.4:" 'host) "1.2.3.4"))
	  (should (string-equal (file-remote-p "/-:1.2.3.4:" 'localname) ""))
	  (should (string-equal (file-remote-p "/-:1.2.3.4:" 'hop) nil))

	  ;; Expand `tramp-default-method'.
	  (should (string-equal
		   (file-remote-p "/-:user@1.2.3.4:")
		   (format "/%s:%s@%s:" "default-method" "user" "1.2.3.4")))
	  (should (string-equal
		   (file-remote-p "/-:user@1.2.3.4:" 'method) "default-method"))
	  (should
	   (string-equal (file-remote-p "/-:user@1.2.3.4:" 'user) "user"))
	  (should
	   (string-equal (file-remote-p "/-:user@1.2.3.4:" 'host) "1.2.3.4"))
	  (should
	   (string-equal (file-remote-p "/-:user@1.2.3.4:" 'localname) ""))
	  (should
	   (string-equal (file-remote-p "/-:user@1.2.3.4:" 'hop) nil))

	  ;; Expand `tramp-default-user'.
	  (should (string-equal
		   (file-remote-p "/method:1.2.3.4:")
		   (format "/%s:%s@%s:" "method" "default-user" "1.2.3.4")))
	  (should
	   (string-equal (file-remote-p "/method:1.2.3.4:" 'method) "method"))
	  (should (string-equal
		   (file-remote-p "/method:1.2.3.4:" 'user) "default-user"))
	  (should
	   (string-equal (file-remote-p "/method:1.2.3.4:" 'host) "1.2.3.4"))
	  (should
	   (string-equal (file-remote-p "/method:1.2.3.4:" 'localname) ""))
	  (should (string-equal (file-remote-p "/method:1.2.3.4:" 'hop) nil))

	  ;; No expansion.
	  (should (string-equal
		   (file-remote-p "/method:user@1.2.3.4:")
		   (format "/%s:%s@%s:" "method" "user" "1.2.3.4")))
	  (should (string-equal
		   (file-remote-p "/method:user@1.2.3.4:" 'method) "method"))
	  (should
	   (string-equal (file-remote-p "/method:user@1.2.3.4:" 'user) "user"))
	  (should (string-equal
		   (file-remote-p "/method:user@1.2.3.4:" 'host) "1.2.3.4"))
	  (should (string-equal
		   (file-remote-p "/method:user@1.2.3.4:" 'localname) ""))
	  (should (string-equal
		   (file-remote-p "/method:user@1.2.3.4:" 'hop) nil))

	  ;; Expand `tramp-default-method', `tramp-default-user' and
	  ;; `tramp-default-host'.
	  (should
	   (string-equal
	    (file-remote-p "/-:[]:")
	    (format
	     "/%s:%s@%s:" "default-method" "default-user" "default-host")))
	  (should
	   (string-equal (file-remote-p "/-:[]:" 'method) "default-method"))
	  (should (string-equal (file-remote-p "/-:[]:" 'user) "default-user"))
	  (should (string-equal (file-remote-p "/-:[]:" 'host) "default-host"))
	  (should (string-equal (file-remote-p "/-:[]:" 'localname) ""))
	  (should (string-equal (file-remote-p "/-:[]:" 'hop) nil))

	  ;; Expand `tramp-default-method' and `tramp-default-user'.
	  (let ((tramp-default-host "::1"))
	    (should
	     (string-equal
	      (file-remote-p "/-:[]:")
	      (format "/%s:%s@%s:" "default-method" "default-user" "[::1]")))
	    (should
	     (string-equal (file-remote-p "/-:[]:" 'method) "default-method"))
	    (should
	     (string-equal (file-remote-p "/-:[]:" 'user) "default-user"))
	    (should (string-equal (file-remote-p "/-:[]:" 'host) "::1"))
	    (should (string-equal (file-remote-p "/-:[]:" 'localname) ""))
	    (should (string-equal (file-remote-p "/-:[]:" 'hop) nil)))

	  ;; Expand `tramp-default-method' and `tramp-default-user'.
	  (should
	   (string-equal
	    (file-remote-p "/-:[::1]:")
	    (format "/%s:%s@%s:" "default-method" "default-user" "[::1]")))
	  (should
	   (string-equal (file-remote-p "/-:[::1]:" 'method) "default-method"))
	  (should
	   (string-equal (file-remote-p "/-:[::1]:" 'user) "default-user"))
	  (should (string-equal (file-remote-p "/-:[::1]:" 'host) "::1"))
	  (should (string-equal (file-remote-p "/-:[::1]:" 'localname) ""))
	  (should (string-equal (file-remote-p "/-:[::1]:" 'hop) nil))

	  ;; Expand `tramp-default-method'.
	  (should (string-equal
		   (file-remote-p "/-:user@[::1]:")
		   (format "/%s:%s@%s:" "default-method" "user" "[::1]")))
	  (should (string-equal
		   (file-remote-p "/-:user@[::1]:" 'method) "default-method"))
	  (should (string-equal (file-remote-p "/-:user@[::1]:" 'user) "user"))
	  (should (string-equal (file-remote-p "/-:user@[::1]:" 'host) "::1"))
	  (should (string-equal (file-remote-p "/-:user@[::1]:" 'localname) ""))
	  (should (string-equal (file-remote-p "/-:user@[::1]:" 'hop) nil))

	  ;; Expand `tramp-default-user'.
	  (should (string-equal
		   (file-remote-p "/method:[::1]:")
		   (format "/%s:%s@%s:" "method" "default-user" "[::1]")))
	  (should
	   (string-equal (file-remote-p "/method:[::1]:" 'method) "method"))
	  (should (string-equal
		   (file-remote-p "/method:[::1]:" 'user) "default-user"))
	  (should (string-equal (file-remote-p "/method:[::1]:" 'host) "::1"))
	  (should (string-equal (file-remote-p "/method:[::1]:" 'localname) ""))
	  (should (string-equal (file-remote-p "/method:[::1]:" 'hop) nil))

	  ;; No expansion.  Hop.
	  (should (string-equal
		   (file-remote-p "/method:user@[::1]#1234:")
		   (format "/%s:%s@%s#%s:" "method" "user" "[::1]" "1234")))
	  (should (string-equal
		   (file-remote-p "/method:user@[::1]#1234:" 'method) "method"))
	  (should (string-equal (file-remote-p "/method:user@[::1]#1234:" 'user)
				"user"))
	  (should (string-equal
		   (file-remote-p "/method:user@[::1]#1234:" 'host) "::1#1234"))
	  (should (string-equal
		   (file-remote-p "/method:user@[::1]#1234:" 'localname) ""))
	  (should (string-equal
		   (file-remote-p "/method:user@[::1]#1234:" 'hop) nil))

	  ;; Local file name part.
	  (should (string-equal (file-remote-p "/-:host:/:" 'localname) "/:"))
	  (should (string-equal (file-remote-p "/method:::" 'localname) ":"))
	  (should (string-equal (file-remote-p "/method:: " 'localname) " "))
	  (should
	   (string-equal (file-remote-p "/method::file" 'localname) "file"))
	  (should (string-equal
		   (file-remote-p "/method::/path/to/file" 'localname)
		   "/path/to/file"))

	  ;; Multihop.
	  (dolist (tramp-show-ad-hoc-proxies '(nil t))

	    ;; Explicit settings in `tramp-default-proxies-alist'
	    ;; shouldn't show hops.
	    (setq tramp-default-proxies-alist
		  '(("^host2$" "^user2$" "/method1:user1@host1:")))
	    (should
	     (string-equal
	      (file-remote-p "/method2:user2@host2:/path/to/file")
	      "/method2:user2@host2:"))
	    (setq tramp-default-proxies-alist nil)

	    ;; Ad-hoc settings.
	    (should
	     (string-equal
	      (file-remote-p
	       "/method1:user1@host1|method2:user2@host2:/path/to/file")
	      (if tramp-show-ad-hoc-proxies
		  "/method1:user1@host1|method2:user2@host2:"
		"/method2:user2@host2:")))
	    (should
	     (string-equal
	      (file-remote-p
	       "/method1:user1@host1|method2:user2@host2:/path/to/file" 'method)
	      "method2"))
	    (should
	     (string-equal
	      (file-remote-p
	       "/method1:user1@host1|method2:user2@host2:/path/to/file" 'user)
	      "user2"))
	    (should
	     (string-equal
	      (file-remote-p
	       "/method1:user1@host1|method2:user2@host2:/path/to/file" 'host)
	      "host2"))
	    (should
	     (string-equal
	      (file-remote-p
	       "/method1:user1@host1|method2:user2@host2:/path/to/file"
	       'localname)
	      "/path/to/file"))
	    (should
	     (string-equal
	      (file-remote-p
	       "/method1:user1@host1|method2:user2@host2:/path/to/file" 'hop)
	      (format "%s:%s@%s|"
		      "method1" "user1" "host1")))

	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/method1:user1@host1"
		"|method2:user2@host2"
		"|method3:user3@host3:/path/to/file"))
	      (if tramp-show-ad-hoc-proxies
		  (concat
		   "/method1:user1@host1"
		   "|method2:user2@host2"
		   "|method3:user3@host3:")
		"/method3:user3@host3:")))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/method1:user1@host1"
		"|method2:user2@host2"
		"|method3:user3@host3:/path/to/file")
	       'method)
	      "method3"))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/method1:user1@host1"
		"|method2:user2@host2"
		"|method3:user3@host3:/path/to/file")
	       'user)
	      "user3"))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/method1:user1@host1"
		"|method2:user2@host2"
		"|method3:user3@host3:/path/to/file")
	       'host)
	      "host3"))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/method1:user1@host1"
		"|method2:user2@host2"
		"|method3:user3@host3:/path/to/file")
	       'localname)
	      "/path/to/file"))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/method1:user1@host1"
		"|method2:user2@host2"
		"|method3:user3@host3:/path/to/file")
	       'hop)
	      (format "%s:%s@%s|%s:%s@%s|"
		      "method1" "user1" "host1" "method2" "user2" "host2")))

	    ;; Expand `tramp-default-method-alist'.
	    (add-to-list
	     'tramp-default-method-alist '("host1" "user1" "method1"))
	    (add-to-list
	     'tramp-default-method-alist '("host2" "user2" "method2"))
	    (add-to-list
	     'tramp-default-method-alist '("host3" "user3" "method3"))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/-:user1@host1"
		"|-:user2@host2"
		"|-:user3@host3:/path/to/file"))
	      (if tramp-show-ad-hoc-proxies
		  (concat
		   "/method1:user1@host1"
		   "|method2:user2@host2"
		   "|method3:user3@host3:")
		"/method3:user3@host3:")))

	    ;; Expand `tramp-default-user-alist'.
	    (add-to-list 'tramp-default-user-alist '("method1" "host1" "user1"))
	    (add-to-list 'tramp-default-user-alist '("method2" "host2" "user2"))
	    (add-to-list 'tramp-default-user-alist '("method3" "host3" "user3"))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/method1:host1"
		"|method2:host2"
		"|method3:host3:/path/to/file"))
	      (if tramp-show-ad-hoc-proxies
		  (concat
		   "/method1:user1@host1"
		   "|method2:user2@host2"
		   "|method3:user3@host3:")
		"/method3:user3@host3:")))

	    ;; Expand `tramp-default-host-alist'.
	    (add-to-list 'tramp-default-host-alist '("method1" "user1" "host1"))
	    (add-to-list 'tramp-default-host-alist '("method2" "user2" "host2"))
	    (add-to-list 'tramp-default-host-alist '("method3" "user3" "host3"))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/method1:user1@"
		"|method2:user2@"
		"|method3:user3@:/path/to/file"))
	      (if tramp-show-ad-hoc-proxies
		  (concat
		   "/method1:user1@host1"
		   "|method2:user2@host2"
		   "|method3:user3@host3:")
		"/method3:user3@host3:")))

	    ;; Ad-hoc user name and host name expansion.
	    (setq tramp-default-method-alist nil
		  tramp-default-user-alist nil
		  tramp-default-host-alist nil)
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/method1:user1@host1"
		"|method2:user2@"
		"|method3:user3@:/path/to/file"))
	      (if tramp-show-ad-hoc-proxies
		  (concat
		   "/method1:user1@host1"
		   "|method2:user2@host1"
		   "|method3:user3@host1:")
		"/method3:user3@host1:")))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/method1:%u@%h"
		"|method2:user2@host2"
		"|method3:%u@%h"
		"|method4:user4%domain4@host4#1234:/path/to/file"))
	      (if tramp-show-ad-hoc-proxies
		  (concat
		   "/method1:user2@host2"
		   "|method2:user2@host2"
		   "|method3:user4@host4"
		   "|method4:user4%domain4@host4#1234:")
		"/method4:user4%domain4@host4#1234:")))))

      ;; Exit.
      (tramp-change-syntax syntax))))

(ert-deftest tramp-test02-file-name-dissect-simplified ()
  "Check simplified file name components."
  :tags '(:expensive-test)
  (let ((tramp-default-method "default-method")
	(tramp-default-user "default-user")
	(tramp-default-host "default-host")
	tramp-default-user-alist
	tramp-default-host-alist
	tramp-default-proxies-alist
	;; Suppress method name check.
	(non-essential t)
	;; Suppress check for multihops.
	(tramp-cache-data (make-hash-table :test #'equal))
	(tramp-connection-properties '((nil "login-program" t)))
	(syntax tramp-syntax))
    (unwind-protect
	(progn
	  (tramp-change-syntax 'simplified)
	  ;; An unknown default method shall raise an error.
	  (let (non-essential)
	    (should-error
	     (expand-file-name "/user@host:")
	     :type 'user-error))

	  ;; Expand `tramp-default-method' and `tramp-default-user'.
	  (should (string-equal
		   (file-remote-p "/host:")
		   (format "/%s@%s:" "default-user" "host")))
	  (should (string-equal
		   (file-remote-p "/host:" 'method) "default-method"))
	  (should (string-equal (file-remote-p "/host:" 'user) "default-user"))
	  (should (string-equal (file-remote-p "/host:" 'host) "host"))
	  (should (string-equal (file-remote-p "/host:" 'localname) ""))
	  (should (string-equal (file-remote-p "/host:" 'hop) nil))

	  ;; Expand `tramp-default-method' and `tramp-default-host'.
	  (should (string-equal
		   (file-remote-p "/user@:")
		   (format "/%s@%s:" "user" "default-host")))
	  (should (string-equal
		   (file-remote-p "/user@:" 'method) "default-method"))
	  (should (string-equal (file-remote-p "/user@:" 'user) "user"))
	  (should (string-equal (file-remote-p "/user@:" 'host) "default-host"))
	  (should (string-equal (file-remote-p "/user@:" 'localname) ""))
	  (should (string-equal (file-remote-p "/user@:" 'hop) nil))

	  ;; Expand `tramp-default-method'.
	  (should (string-equal
		   (file-remote-p "/user@host:")
		   (format "/%s@%s:" "user" "host")))
	  (should (string-equal
		   (file-remote-p "/user@host:" 'method) "default-method"))
	  (should (string-equal (file-remote-p "/user@host:" 'user) "user"))
	  (should (string-equal (file-remote-p "/user@host:" 'host) "host"))
	  (should (string-equal (file-remote-p "/user@host:" 'localname) ""))
	  (should (string-equal (file-remote-p "/user@host:" 'hop) nil))

	  ;; No expansion.
	  (should (string-equal
		   (file-remote-p "/user@email@host:")
		   (format "/%s@%s:" "user@email" "host")))
	  (should (string-equal
		   (file-remote-p "/user@email@host:" 'method) "default-method"))
	  (should (string-equal
		   (file-remote-p "/user@email@host:" 'user) "user@email"))
	  (should (string-equal
		   (file-remote-p "/user@email@host:" 'host) "host"))
	  (should (string-equal
		   (file-remote-p "/user@email@host:" 'localname) ""))
	  (should (string-equal
		   (file-remote-p "/user@email@host:" 'hop) nil))

	  ;; Expand `tramp-default-method' and `tramp-default-user'.
	  (should (string-equal
		   (file-remote-p "/host#1234:")
		   (format "/%s@%s:" "default-user" "host#1234")))
	  (should (string-equal
		   (file-remote-p "/host#1234:" 'method) "default-method"))
	  (should (string-equal
		   (file-remote-p "/host#1234:" 'user) "default-user"))
	  (should (string-equal
		   (file-remote-p "/host#1234:" 'host) "host#1234"))
	  (should (string-equal (file-remote-p "/host#1234:" 'localname) ""))
	  (should (string-equal (file-remote-p "/host#1234:" 'hop) nil))

	  ;; Expand `tramp-default-method'.
	  (should (string-equal
		   (file-remote-p "/user@host#1234:")
		   (format "/%s@%s:" "user" "host#1234")))
	  (should (string-equal
		   (file-remote-p "/user@host#1234:" 'method) "default-method"))
	  (should (string-equal
		   (file-remote-p "/user@host#1234:" 'user) "user"))
	  (should (string-equal
		   (file-remote-p "/user@host#1234:" 'host) "host#1234"))
	  (should (string-equal
		   (file-remote-p "/user@host#1234:" 'localname) ""))
	  (should (string-equal (file-remote-p "/user@host#1234:" 'hop) nil))

	  ;; Expand `tramp-default-method' and `tramp-default-user'.
	  (should (string-equal
		   (file-remote-p "/1.2.3.4:")
		   (format "/%s@%s:" "default-user" "1.2.3.4")))
	  (should (string-equal
		   (file-remote-p "/1.2.3.4:" 'method) "default-method"))
	  (should (string-equal
		   (file-remote-p "/1.2.3.4:" 'user) "default-user"))
	  (should (string-equal (file-remote-p "/1.2.3.4:" 'host) "1.2.3.4"))
	  (should (string-equal (file-remote-p "/1.2.3.4:" 'localname) ""))
	  (should (string-equal (file-remote-p "/1.2.3.4:" 'hop) nil))

	  ;; Expand `tramp-default-method'.
	  (should (string-equal
		   (file-remote-p "/user@1.2.3.4:")
		   (format "/%s@%s:" "user" "1.2.3.4")))
	  (should (string-equal
		   (file-remote-p "/user@1.2.3.4:" 'method) "default-method"))
	  (should (string-equal (file-remote-p "/user@1.2.3.4:" 'user) "user"))
	  (should (string-equal
		   (file-remote-p "/user@1.2.3.4:" 'host) "1.2.3.4"))
	  (should (string-equal (file-remote-p "/user@1.2.3.4:" 'localname) ""))
	  (should (string-equal (file-remote-p "/user@1.2.3.4:" 'hop) nil))

	  ;; Expand `tramp-default-method', `tramp-default-user' and
	  ;; `tramp-default-host'.
	  (should (string-equal
		   (file-remote-p "/[]:")
		   (format
		    "/%s@%s:" "default-user" "default-host")))
	  (should (string-equal
		   (file-remote-p "/[]:" 'method) "default-method"))
	  (should (string-equal (file-remote-p "/[]:" 'user) "default-user"))
	  (should (string-equal (file-remote-p "/[]:" 'host) "default-host"))
	  (should (string-equal (file-remote-p "/[]:" 'localname) ""))
	  (should (string-equal (file-remote-p "/[]:" 'hop) nil))

	  ;; Expand `tramp-default-method' and `tramp-default-user'.
	  (let ((tramp-default-host "::1"))
	    (should (string-equal
		     (file-remote-p "/[]:")
		     (format "/%s@%s:" "default-user" "[::1]")))
	    (should (string-equal
		     (file-remote-p "/[]:" 'method) "default-method"))
	    (should (string-equal (file-remote-p "/[]:" 'user) "default-user"))
	    (should (string-equal (file-remote-p "/[]:" 'host) "::1"))
	    (should (string-equal (file-remote-p "/[]:" 'localname) ""))
	    (should (string-equal (file-remote-p "/[]:" 'hop) nil)))

	  ;; Expand `tramp-default-method' and `tramp-default-user'.
	  (should (string-equal
		   (file-remote-p "/[::1]:")
		   (format "/%s@%s:" "default-user" "[::1]")))
	  (should (string-equal
		   (file-remote-p "/[::1]:" 'method) "default-method"))
	  (should (string-equal (file-remote-p "/[::1]:" 'user) "default-user"))
	  (should (string-equal (file-remote-p "/[::1]:" 'host) "::1"))
	  (should (string-equal (file-remote-p "/[::1]:" 'localname) ""))
	  (should (string-equal (file-remote-p "/[::1]:" 'hop) nil))

	  ;; Expand `tramp-default-method'.
	  (should (string-equal
		   (file-remote-p "/user@[::1]:")
		   (format "/%s@%s:" "user" "[::1]")))
	  (should (string-equal
		   (file-remote-p "/user@[::1]:" 'method) "default-method"))
	  (should (string-equal (file-remote-p "/user@[::1]:" 'user) "user"))
	  (should (string-equal (file-remote-p "/user@[::1]:" 'host) "::1"))
	  (should (string-equal (file-remote-p "/user@[::1]:" 'localname) ""))
	  (should (string-equal (file-remote-p "/user@[::1]:" 'hop) nil))

	  ;; No expansion.  Hop.
	  (should (string-equal
		   (file-remote-p "/user@[::1]#1234:")
		   (format "/%s@%s#%s:" "user" "[::1]" "1234")))
	  (should (string-equal
		   (file-remote-p "/user@[::1]#1234:" 'method) "default-method"))
	  (should
	   (string-equal (file-remote-p "/user@[::1]#1234:" 'user) "user"))
	  (should
	   (string-equal (file-remote-p "/user@[::1]#1234:" 'host) "::1#1234"))
	  (should
	   (string-equal (file-remote-p "/user@[::1]#1234:" 'localname) ""))
	  (should (string-equal (file-remote-p "/user@[::1]#1234:" 'hop) nil))

	  ;; Local file name part.
	  (should (string-equal (file-remote-p "/host:/:" 'localname) "/:"))
	  (should (string-equal (file-remote-p "/host::" 'localname) ":"))
	  (should (string-equal (file-remote-p "/host: " 'localname) " "))
	  (should (string-equal (file-remote-p "/host:file" 'localname) "file"))
	  (should (string-equal
		   (file-remote-p "/host:/path/to/file" 'localname)
		   "/path/to/file"))

	  ;; Multihop.
	  (dolist (tramp-show-ad-hoc-proxies '(nil t))

	    ;; Explicit settings in `tramp-default-proxies-alist'
	    ;; shouldn't show hops.
	    (setq tramp-default-proxies-alist
		  '(("^host2$" "^user2$" "/user1@host1:")))
	    (should
	     (string-equal
	      (file-remote-p "/user2@host2:/path/to/file")
	      "/user2@host2:"))
	    (setq tramp-default-proxies-alist nil)

	    ;; Ad-hoc settings.
	    (should
	     (string-equal
	      (file-remote-p "/user1@host1|user2@host2:/path/to/file")
	      (if tramp-show-ad-hoc-proxies
		  "/user1@host1|user2@host2:"
		"/user2@host2:")))
	    (should
	     (string-equal
	      (file-remote-p
	       "/user1@host1|user2@host2:/path/to/file" 'method)
	      "default-method"))
	    (should
	     (string-equal
	      (file-remote-p
	       "/user1@host1|user2@host2:/path/to/file" 'user)
	      "user2"))
	    (should
	     (string-equal
	      (file-remote-p
	       "/user1@host1|user2@host2:/path/to/file" 'host)
	      "host2"))
	    (should
	     (string-equal
	      (file-remote-p
	       "/user1@host1|user2@host2:/path/to/file" 'localname)
	      "/path/to/file"))
	    (should
	     (string-equal
	      (file-remote-p
	       "/user1@host1|user2@host2:/path/to/file" 'hop)
	      (format "%s@%s|" "user1" "host1")))

	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/user1@host1"
		"|user2@host2"
		"|user3@host3:/path/to/file"))
	      (if tramp-show-ad-hoc-proxies
		  (concat
		   "/user1@host1"
		   "|user2@host2"
		   "|user3@host3:")
		"/user3@host3:")))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/user1@host1"
		"|user2@host2"
		"|user3@host3:/path/to/file")
	       'method)
	      "default-method"))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/user1@host1"
		"|user2@host2"
		"|user3@host3:/path/to/file")
	       'user)
	      "user3"))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/user1@host1"
		"|user2@host2"
		"|user3@host3:/path/to/file")
	       'host)
	      "host3"))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/user1@host1"
		"|user2@host2"
		"|user3@host3:/path/to/file")
	       'localname)
	      "/path/to/file"))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/user1@host1"
		"|user2@host2"
		"|user3@host3:/path/to/file")
	       'hop)
	      (format "%s@%s|%s@%s|"
		      "user1" "host1" "user2" "host2")))

	    ;; Expand `tramp-default-user-alist'.
	    (add-to-list 'tramp-default-user-alist '(nil "host1" "user1"))
	    (add-to-list 'tramp-default-user-alist '(nil "host2" "user2"))
	    (add-to-list 'tramp-default-user-alist '(nil "host3" "user3"))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/host1"
		"|host2"
		"|host3:/path/to/file"))
	      (if tramp-show-ad-hoc-proxies
		  (concat
		   "/user1@host1"
		   "|user2@host2"
		   "|user3@host3:")
		"/user3@host3:")))

	    ;; Expand `tramp-default-host-alist'.
	    (add-to-list 'tramp-default-host-alist '(nil "user1" "host1"))
	    (add-to-list 'tramp-default-host-alist '(nil "user2" "host2"))
	    (add-to-list 'tramp-default-host-alist '(nil "user3" "host3"))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/user1@"
		"|user2@"
		"|user3@:/path/to/file"))
	      (if tramp-show-ad-hoc-proxies
		  (concat
		   "/user1@host1"
		   "|user2@host2"
		   "|user3@host3:")
		"/user3@host3:")))

	    ;; Ad-hoc user name and host name expansion.
	    (setq tramp-default-user-alist nil
		  tramp-default-host-alist nil)
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/user1@host1"
		"|user2@"
		"|user3@:/path/to/file"))
	      (if tramp-show-ad-hoc-proxies
		  (concat
		   "/user1@host1"
		   "|user2@host1"
		   "|user3@host1:")
		"/user3@host1:")))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/%u@%h"
		"|user2@host2"
		"|%u@%h"
		"|user4%domain4@host4#1234:/path/to/file"))
	      (if tramp-show-ad-hoc-proxies
		  (concat
		   "/user2@host2"
		   "|user2@host2"
		   "|user4@host4"
		   "|user4%domain4@host4#1234:")
		"/user4%domain4@host4#1234:")))))

      ;; Exit.
      (tramp-change-syntax syntax))))

(ert-deftest tramp-test02-file-name-dissect-separate ()
  "Check separate file name components."
  :tags '(:expensive-test)
  (let ((tramp-default-method "default-method")
	(tramp-default-user "default-user")
	(tramp-default-host "default-host")
	tramp-default-method-alist
	tramp-default-user-alist
	tramp-default-host-alist
	tramp-default-proxies-alist
	;; Suppress method name check.
	(non-essential t)
	;; Suppress check for multihops.
	(tramp-cache-data (make-hash-table :test #'equal))
	(tramp-connection-properties '((nil "login-program" t)))
	(syntax tramp-syntax)
	;; We must transform `tramp-crypt-directories'.
	(tramp-crypt-directories
	 (mapcar #'tramp-dissect-file-name tramp-crypt-directories)))
    (unwind-protect
	(progn
	  (tramp-change-syntax 'separate)
	  ;; We must transform `tramp-crypt-directories'.
	  (setq tramp-crypt-directories
		(mapcar
		 (lambda (vec)
		   (tramp-make-tramp-file-name
		    vec (tramp-file-name-localname vec)))
		 tramp-crypt-directories))
	  ;; An unknown method shall raise an error.
	  (let (non-essential)
	    (should-error
	     (expand-file-name "/[method/user@host]")
	     :type 'user-error))

	  ;; Expand `tramp-default-user' and `tramp-default-host'.
	  (should (string-equal
		   (file-remote-p "/[method/]")
		   (format
		    "/[%s/%s@%s]" "method" "default-user" "default-host")))
	  (should (string-equal (file-remote-p "/[method/]" 'method) "method"))
	  (should (string-equal
		   (file-remote-p "/[method/]" 'user) "default-user"))
	  (should (string-equal
		   (file-remote-p "/[method/]" 'host) "default-host"))
	  (should (string-equal (file-remote-p "/[method/]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[method/]" 'hop) nil))

	  ;; Expand `tramp-default-method' and `tramp-default-user'.
	  (should (string-equal
		   (file-remote-p "/[/host]")
		   (format
		    "/[%s/%s@%s]" "default-method" "default-user" "host")))
	  (should (string-equal
		   (file-remote-p "/[/host]" 'method) "default-method"))
	  (should (string-equal
		   (file-remote-p "/[/host]" 'user) "default-user"))
	  (should (string-equal (file-remote-p "/[/host]" 'host) "host"))
	  (should (string-equal (file-remote-p "/[/host]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[/host]" 'hop) nil))

	  ;; Expand `tramp-default-method' and `tramp-default-host'.
	  (should (string-equal
		   (file-remote-p "/[/user@]")
		   (format
		    "/[%s/%s@%s]" "default-method" "user" "default-host")))
	  (should (string-equal
		   (file-remote-p "/[/user@]" 'method) "default-method"))
	  (should (string-equal (file-remote-p "/[/user@]" 'user) "user"))
	  (should (string-equal
		   (file-remote-p "/[/user@]" 'host) "default-host"))
	  (should (string-equal (file-remote-p "/[/user@]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[/user@]" 'hop) nil))

	  ;; Expand `tramp-default-method'.
	  (should (string-equal
		   (file-remote-p "/[/user@host]")
		   (format "/[%s/%s@%s]" "default-method" "user" "host")))
	  (should (string-equal
		   (file-remote-p "/[/user@host]" 'method) "default-method"))
	  (should (string-equal (file-remote-p "/[/user@host]" 'user) "user"))
	  (should (string-equal (file-remote-p "/[/user@host]" 'host) "host"))
	  (should (string-equal (file-remote-p "/[/user@host]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[/user@host]" 'hop) nil))

	  ;; Expand `tramp-default-method' and `tramp-default-user'.
	  (should (string-equal
		   (file-remote-p "/[-/host]")
		   (format
		    "/[%s/%s@%s]" "default-method" "default-user" "host")))
	  (should (string-equal
		   (file-remote-p "/[-/host]" 'method) "default-method"))
	  (should (string-equal
		   (file-remote-p "/[-/host]" 'user) "default-user"))
	  (should (string-equal (file-remote-p "/[-/host]" 'host) "host"))
	  (should (string-equal (file-remote-p "/[-/host]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[-/host]" 'hop) nil))

	  ;; Expand `tramp-default-method' and `tramp-default-host'.
	  (should (string-equal
		   (file-remote-p "/[-/user@]")
		   (format
		    "/[%s/%s@%s]" "default-method" "user" "default-host")))
	  (should (string-equal
		   (file-remote-p "/[-/user@]" 'method) "default-method"))
	  (should (string-equal (file-remote-p "/[-/user@]" 'user) "user"))
	  (should (string-equal
		   (file-remote-p "/[-/user@]" 'host) "default-host"))
	  (should (string-equal (file-remote-p "/[-/user@]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[-/user@]" 'hop) nil))

	  ;; Expand `tramp-default-method'.
	  (should (string-equal
		   (file-remote-p "/[-/user@host]")
		   (format "/[%s/%s@%s]" "default-method" "user" "host")))
	  (should (string-equal
		   (file-remote-p "/[-/user@host]" 'method) "default-method"))
	  (should (string-equal (file-remote-p "/[-/user@host]" 'user) "user"))
	  (should (string-equal (file-remote-p "/[-/user@host]" 'host) "host"))
	  (should (string-equal (file-remote-p "/[-/user@host]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[-/user@host]" 'hop) nil))

	  ;; Expand `tramp-default-user'.
	  (should (string-equal
		   (file-remote-p "/[method/host]")
		   (format "/[%s/%s@%s]" "method" "default-user" "host")))
	  (should (string-equal
		   (file-remote-p "/[method/host]" 'method) "method"))
	  (should (string-equal
		   (file-remote-p "/[method/host]" 'user) "default-user"))
	  (should (string-equal (file-remote-p "/[method/host]" 'host) "host"))
	  (should (string-equal (file-remote-p "/[method/host]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[method/host]" 'hop) nil))

	  ;; Expand `tramp-default-host'.
	  (should (string-equal
		   (file-remote-p "/[method/user@]")
		   (format "/[%s/%s@%s]" "method" "user" "default-host")))
	  (should (string-equal
		   (file-remote-p "/[method/user@]" 'method) "method"))
	  (should (string-equal (file-remote-p "/[method/user@]" 'user) "user"))
	  (should (string-equal
		   (file-remote-p "/[method/user@]" 'host) "default-host"))
	  (should (string-equal
		   (file-remote-p "/[method/user@]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[method/user@]" 'hop) nil))

	  ;; No expansion.
	  (should (string-equal
		   (file-remote-p "/[method/user@host]")
		   (format "/[%s/%s@%s]" "method" "user" "host")))
	  (should (string-equal
		   (file-remote-p "/[method/user@host]" 'method) "method"))
	  (should (string-equal
		   (file-remote-p "/[method/user@host]" 'user) "user"))
	  (should (string-equal
		   (file-remote-p "/[method/user@host]" 'host) "host"))
	  (should (string-equal
		   (file-remote-p "/[method/user@host]" 'localname) ""))
	  (should (string-equal
		   (file-remote-p "/[method/user@host]" 'hop) nil))

	  ;; No expansion.
	  (should (string-equal
		   (file-remote-p "/[method/user@email@host]")
		   (format "/[%s/%s@%s]" "method" "user@email" "host")))
	  (should (string-equal
		   (file-remote-p "/[method/user@email@host]" 'method) "method"))
	  (should (string-equal
		   (file-remote-p "/[method/user@email@host]" 'user)
		   "user@email"))
	  (should (string-equal
		   (file-remote-p "/[method/user@email@host]" 'host) "host"))
	  (should (string-equal
		   (file-remote-p "/[method/user@email@host]" 'localname) ""))
	  (should (string-equal
		   (file-remote-p "/[method/user@email@host]" 'hop) nil))

	  ;; Expand `tramp-default-method' and `tramp-default-user'.
	  (should (string-equal
		   (file-remote-p "/[/host#1234]")
		   (format
		    "/[%s/%s@%s]" "default-method" "default-user" "host#1234")))
	  (should (string-equal
		   (file-remote-p "/[/host#1234]" 'method) "default-method"))
	  (should (string-equal
		   (file-remote-p "/[/host#1234]" 'user) "default-user"))
	  (should (string-equal
		   (file-remote-p "/[/host#1234]" 'host) "host#1234"))
	  (should (string-equal (file-remote-p "/[/host#1234]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[/host#1234]" 'hop) nil))

	  ;; Expand `tramp-default-method'.
	  (should (string-equal
		   (file-remote-p "/[/user@host#1234]")
		   (format "/[%s/%s@%s]" "default-method" "user" "host#1234")))
	  (should (string-equal
		   (file-remote-p "/[/user@host#1234]" 'method)
		   "default-method"))
	  (should (string-equal
		   (file-remote-p "/[/user@host#1234]" 'user) "user"))
	  (should (string-equal
		   (file-remote-p "/[/user@host#1234]" 'host) "host#1234"))
	  (should (string-equal
		   (file-remote-p "/[/user@host#1234]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[/user@host#1234]" 'hop) nil))

	  ;; Expand `tramp-default-method' and `tramp-default-user'.
	  (should (string-equal
		   (file-remote-p "/[-/host#1234]")
		   (format
		    "/[%s/%s@%s]" "default-method" "default-user" "host#1234")))
	  (should (string-equal
		   (file-remote-p "/[-/host#1234]" 'method) "default-method"))
	  (should (string-equal
		   (file-remote-p "/[-/host#1234]" 'user) "default-user"))
	  (should (string-equal
		   (file-remote-p "/[-/host#1234]" 'host) "host#1234"))
	  (should (string-equal (file-remote-p "/[-/host#1234]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[-/host#1234]" 'hop) nil))

	  ;; Expand `tramp-default-method'.
	  (should (string-equal
		   (file-remote-p "/[-/user@host#1234]")
		   (format "/[%s/%s@%s]" "default-method" "user" "host#1234")))
	  (should (string-equal
		   (file-remote-p "/[-/user@host#1234]" 'method)
		   "default-method"))
	  (should (string-equal
		   (file-remote-p "/[-/user@host#1234]" 'user) "user"))
	  (should (string-equal
		   (file-remote-p "/[-/user@host#1234]" 'host) "host#1234"))
	  (should (string-equal
		   (file-remote-p "/[-/user@host#1234]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[-/user@host#1234]" 'hop) nil))

	  ;; Expand `tramp-default-user'.
	  (should (string-equal
		   (file-remote-p "/[method/host#1234]")
		   (format "/[%s/%s@%s]" "method" "default-user" "host#1234")))
	  (should (string-equal
		   (file-remote-p "/[method/host#1234]" 'method) "method"))
	  (should (string-equal
		   (file-remote-p "/[method/host#1234]" 'user) "default-user"))
	  (should (string-equal
		   (file-remote-p "/[method/host#1234]" 'host) "host#1234"))
	  (should (string-equal
		   (file-remote-p "/[method/host#1234]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[method/host#1234]" 'hop) nil))

	  ;; No expansion.
	  (should (string-equal
		   (file-remote-p "/[method/user@host#1234]")
		   (format "/[%s/%s@%s]" "method" "user" "host#1234")))
	  (should (string-equal
		   (file-remote-p "/[method/user@host#1234]" 'method) "method"))
	  (should (string-equal
		   (file-remote-p "/[method/user@host#1234]" 'user) "user"))
	  (should (string-equal
		   (file-remote-p "/[method/user@host#1234]" 'host) "host#1234"))
	  (should (string-equal
		   (file-remote-p "/[method/user@host#1234]" 'localname) ""))
	  (should (string-equal
		   (file-remote-p "/[method/user@host#1234]" 'hop) nil))

	  ;; Expand `tramp-default-method' and `tramp-default-user'.
	  (should (string-equal
		   (file-remote-p "/[/1.2.3.4]")
		   (format
		    "/[%s/%s@%s]" "default-method" "default-user" "1.2.3.4")))
	  (should (string-equal
		   (file-remote-p "/[/1.2.3.4]" 'method) "default-method"))
	  (should (string-equal
		   (file-remote-p "/[/1.2.3.4]" 'user) "default-user"))
	  (should (string-equal
		   (file-remote-p "/[/1.2.3.4]" 'host) "1.2.3.4"))
	  (should (string-equal (file-remote-p "/[/1.2.3.4]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[/1.2.3.4]" 'hop) nil))

	  ;; Expand `tramp-default-method'.
	  (should (string-equal
		   (file-remote-p "/[/user@1.2.3.4]")
		   (format "/[%s/%s@%s]" "default-method" "user" "1.2.3.4")))
	  (should (string-equal
		   (file-remote-p "/[/user@1.2.3.4]" 'method) "default-method"))
	  (should (string-equal
		   (file-remote-p "/[/user@1.2.3.4]" 'user) "user"))
	  (should (string-equal
		   (file-remote-p "/[/user@1.2.3.4]" 'host) "1.2.3.4"))
	  (should (string-equal
		   (file-remote-p "/[/user@1.2.3.4]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[/user@1.2.3.4]" 'hop) nil))

	  ;; Expand `tramp-default-method' and `tramp-default-user'.
	  (should (string-equal
		   (file-remote-p "/[-/1.2.3.4]")
		   (format
		    "/[%s/%s@%s]" "default-method" "default-user" "1.2.3.4")))
	  (should (string-equal
		   (file-remote-p "/[-/1.2.3.4]" 'method) "default-method"))
	  (should (string-equal
		   (file-remote-p "/[-/1.2.3.4]" 'user) "default-user"))
	  (should (string-equal
		   (file-remote-p "/[-/1.2.3.4]" 'host) "1.2.3.4"))
	  (should (string-equal (file-remote-p "/[-/1.2.3.4]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[-/1.2.3.4]" 'hop) nil))

	  ;; Expand `tramp-default-method'.
	  (should (string-equal
		   (file-remote-p "/[-/user@1.2.3.4]")
		   (format "/[%s/%s@%s]" "default-method" "user" "1.2.3.4")))
	  (should (string-equal
		   (file-remote-p "/[-/user@1.2.3.4]" 'method) "default-method"))
	  (should (string-equal
		   (file-remote-p "/[-/user@1.2.3.4]" 'user) "user"))
	  (should (string-equal
		   (file-remote-p "/[-/user@1.2.3.4]" 'host) "1.2.3.4"))
	  (should (string-equal
		   (file-remote-p "/[-/user@1.2.3.4]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[-/user@1.2.3.4]" 'hop) nil))

	  ;; Expand `tramp-default-user'.
	  (should (string-equal
		   (file-remote-p "/[method/1.2.3.4]")
		   (format "/[%s/%s@%s]" "method" "default-user" "1.2.3.4")))
	  (should (string-equal
		   (file-remote-p "/[method/1.2.3.4]" 'method) "method"))
	  (should (string-equal
		   (file-remote-p "/[method/1.2.3.4]" 'user) "default-user"))
	  (should (string-equal
		   (file-remote-p "/[method/1.2.3.4]" 'host) "1.2.3.4"))
	  (should (string-equal
		   (file-remote-p "/[method/1.2.3.4]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[method/1.2.3.4]" 'hop) nil))

	  ;; No expansion.
	  (should (string-equal
		   (file-remote-p "/[method/user@1.2.3.4]")
		   (format "/[%s/%s@%s]" "method" "user" "1.2.3.4")))
	  (should (string-equal
		   (file-remote-p "/[method/user@1.2.3.4]" 'method) "method"))
	  (should (string-equal
		   (file-remote-p "/[method/user@1.2.3.4]" 'user) "user"))
	  (should (string-equal
		   (file-remote-p "/[method/user@1.2.3.4]" 'host) "1.2.3.4"))
	  (should (string-equal
		   (file-remote-p "/[method/user@1.2.3.4]" 'localname) ""))
	  (should (string-equal
		   (file-remote-p "/[method/user@1.2.3.4]" 'hop) nil))

	  ;; Expand `tramp-default-method', `tramp-default-user' and
	  ;; `tramp-default-host'.
	  (should (string-equal
		   (file-remote-p "/[/]")
		   (format
		    "/[%s/%s@%s]"
		    "default-method" "default-user" "default-host")))
	  (should (string-equal
		   (file-remote-p "/[/]" 'method) "default-method"))
	  (should (string-equal (file-remote-p "/[/]" 'user) "default-user"))
	  (should (string-equal (file-remote-p "/[/]" 'host) "default-host"))
	  (should (string-equal (file-remote-p "/[/]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[/]" 'hop) nil))

	  ;; Expand `tramp-default-method' and `tramp-default-user'.
	  (let ((tramp-default-host "::1"))
	    (should (string-equal
		     (file-remote-p "/[/]")
		     (format
		      "/[%s/%s@%s]"
		      "default-method" "default-user" "::1")))
	    (should (string-equal
		     (file-remote-p "/[/]" 'method) "default-method"))
	    (should (string-equal (file-remote-p "/[/]" 'user) "default-user"))
	    (should (string-equal (file-remote-p "/[/]" 'host) "::1"))
	    (should (string-equal (file-remote-p "/[/]" 'localname) ""))
	    (should (string-equal (file-remote-p "/[/]" 'hop) nil)))

	  ;; Expand `tramp-default-method' and `tramp-default-user'.
	  (should (string-equal
		   (file-remote-p "/[/::1]")
		   (format
		    "/[%s/%s@%s]" "default-method" "default-user" "::1")))
	  (should (string-equal
		   (file-remote-p "/[/::1]" 'method) "default-method"))
	  (should (string-equal
		   (file-remote-p "/[/::1]" 'user) "default-user"))
	  (should (string-equal (file-remote-p "/[/::1]" 'host) "::1"))
	  (should (string-equal (file-remote-p "/[/::1]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[/::1]" 'hop) nil))

	  ;; Expand `tramp-default-method'.
	  (should (string-equal
		   (file-remote-p "/[/user@::1]")
		   (format "/[%s/%s@%s]" "default-method" "user" "::1")))
	  (should (string-equal
		   (file-remote-p "/[/user@::1]" 'method) "default-method"))
	  (should (string-equal (file-remote-p "/[/user@::1]" 'user) "user"))
	  (should (string-equal (file-remote-p "/[/user@::1]" 'host) "::1"))
	  (should (string-equal (file-remote-p "/[/user@::1]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[/user@::1]" 'hop) nil))

	  ;; Expand `tramp-default-method', `tramp-default-user' and
	  ;; `tramp-default-host'.
	  (should (string-equal
		   (file-remote-p "/[-/]")
		   (format
		    "/[%s/%s@%s]"
		    "default-method" "default-user" "default-host")))
	  (should (string-equal
		   (file-remote-p "/[-/]" 'method) "default-method"))
	  (should (string-equal (file-remote-p "/[-/]" 'user) "default-user"))
	  (should (string-equal (file-remote-p "/[-/]" 'host) "default-host"))
	  (should (string-equal (file-remote-p "/[-/]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[-/]" 'hop) nil))

	  ;; Expand `tramp-default-method' and `tramp-default-user'.
	  (let ((tramp-default-host "::1"))
	    (should (string-equal
		     (file-remote-p "/[-/]")
		     (format
		      "/[%s/%s@%s]"
		      "default-method" "default-user" "::1")))
	    (should (string-equal
		     (file-remote-p "/[-/]" 'method) "default-method"))
	    (should (string-equal (file-remote-p "/[-/]" 'user) "default-user"))
	    (should (string-equal (file-remote-p "/[-/]" 'host) "::1"))
	    (should (string-equal (file-remote-p "/[-/]" 'localname) ""))
	    (should (string-equal (file-remote-p "/[-/]" 'hop) nil)))

	  ;; Expand `tramp-default-method' and `tramp-default-user'.
	  (should (string-equal
		   (file-remote-p "/[-/::1]")
		   (format
		    "/[%s/%s@%s]" "default-method" "default-user" "::1")))
	  (should (string-equal
		   (file-remote-p "/[-/::1]" 'method) "default-method"))
	  (should (string-equal
		   (file-remote-p "/[-/::1]" 'user) "default-user"))
	  (should (string-equal (file-remote-p "/[-/::1]" 'host) "::1"))
	  (should (string-equal (file-remote-p "/[-/::1]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[-/::1]" 'hop) nil))

	  ;; Expand `tramp-default-method'.
	  (should (string-equal
		   (file-remote-p "/[-/user@::1]")
		   (format "/[%s/%s@%s]" "default-method" "user" "::1")))
	  (should (string-equal
		   (file-remote-p "/[-/user@::1]" 'method) "default-method"))
	  (should (string-equal (file-remote-p "/[-/user@::1]" 'user) "user"))
	  (should (string-equal (file-remote-p "/[-/user@::1]" 'host) "::1"))
	  (should (string-equal (file-remote-p "/[-/user@::1]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[-/user@::1]" 'hop) nil))

	  ;; Expand `tramp-default-user'.
	  (should (string-equal
		   (file-remote-p "/[method/::1]")
		   (format "/[%s/%s@%s]" "method" "default-user" "::1")))
	  (should (string-equal
		   (file-remote-p "/[method/::1]" 'method) "method"))
	  (should (string-equal
		   (file-remote-p "/[method/::1]" 'user) "default-user"))
	  (should (string-equal (file-remote-p "/[method/::1]" 'host) "::1"))
	  (should (string-equal (file-remote-p "/[method/::1]" 'localname) ""))
	  (should (string-equal (file-remote-p "/[method/::1]" 'hop) nil))

	  ;; No expansion.  Hop.
	  (should (string-equal
		   (file-remote-p "/[method/user@::1#1234]")
		   (format "/[%s/%s@%s#%s]" "method" "user" "::1" "1234")))
	  (should (string-equal
		   (file-remote-p "/[method/user@::1#1234]" 'method) "method"))
	  (should (string-equal
		   (file-remote-p "/[method/user@::1#1234]" 'user) "user"))
	  (should (string-equal
		   (file-remote-p "/[method/user@::1#1234]" 'host) "::1#1234"))
	  (should (string-equal
		   (file-remote-p "/[method/user@::1#1234]" 'localname) ""))
	  (should (string-equal
		   (file-remote-p "/[method/user@::1#1234]" 'hop) nil))

	  ;; Local file name part.
	  (should (string-equal (file-remote-p "/[/host]/:" 'localname) "/:"))
	  (should (string-equal (file-remote-p "/[-/host]/:" 'localname) "/:"))
	  (should (string-equal (file-remote-p "/[method/]:" 'localname) ":"))
	  (should (string-equal (file-remote-p "/[method/] " 'localname) " "))
	  (should (string-equal
		   (file-remote-p "/[method/]file" 'localname) "file"))
	  (should (string-equal
		   (file-remote-p "/[method/]/path/to/file" 'localname)
		   "/path/to/file"))

	  ;; Multihop.
	  (dolist (tramp-show-ad-hoc-proxies '(nil t))

	    ;; Explicit settings in `tramp-default-proxies-alist'
	    ;; shouldn't show hops.
	    (setq tramp-default-proxies-alist
		  '(("^host2$" "^user2$" "/[method1/user1@host1]")))
	    (should
	     (string-equal
	      (file-remote-p "/[method2/user2@host2]/path/to/file")
	      "/[method2/user2@host2]"))
	    (setq tramp-default-proxies-alist nil)

	    ;; Ad-hoc settings.
	    (should
	     (string-equal
	      (file-remote-p
	       "/[method1/user1@host1|method2/user2@host2]/path/to/file")
	      (if tramp-show-ad-hoc-proxies
		  "/[method1/user1@host1|method2/user2@host2]"
		"/[method2/user2@host2]")))
	    (should
	     (string-equal
	      (file-remote-p
	       "/[method1/user1@host1|method2/user2@host2]/path/to/file" 'method)
	      "method2"))
	    (should
	     (string-equal
	      (file-remote-p
	       "/[method1/user1@host1|method2/user2@host2]/path/to/file" 'user)
	      "user2"))
	    (should
	     (string-equal
	      (file-remote-p
	       "/[method1/user1@host1|method2/user2@host2]/path/to/file" 'host)
	      "host2"))
	    (should
	     (string-equal
	      (file-remote-p
	       "/[method1/user1@host1|method2/user2@host2]/path/to/file"
	       'localname)
	      "/path/to/file"))
	    (should
	     (string-equal
	      (file-remote-p
	       "/[method1/user1@host1|method2/user2@host2]/path/to/file" 'hop)
	      (format "%s/%s@%s|"
		      "method1" "user1" "host1")))

	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/[method1/user1@host1"
		"|method2/user2@host2"
		"|method3/user3@host3]/path/to/file"))
	      (if tramp-show-ad-hoc-proxies
		  (concat
		   "/[method1/user1@host1"
		   "|method2/user2@host2"
		   "|method3/user3@host3]")
		"/[method3/user3@host3]")))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/[method1/user1@host1"
		"|method2/user2@host2"
		"|method3/user3@host3]/path/to/file")
	       'method)
	      "method3"))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/[method1/user1@host1"
		"|method2/user2@host2"
		"|method3/user3@host3]/path/to/file")
	       'user)
	      "user3"))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/[method1/user1@host1"
		"|method2/user2@host2"
		"|method3/user3@host3]/path/to/file")
	       'host)
	      "host3"))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/[method1/user1@host1"
		"|method2/user2@host2"
		"|method3/user3@host3]/path/to/file")
	       'localname)
	      "/path/to/file"))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/[method1/user1@host1"
		"|method2/user2@host2"
		"|method3/user3@host3]/path/to/file")
	       'hop)
	      (format "%s/%s@%s|%s/%s@%s|"
		      "method1" "user1" "host1" "method2" "user2" "host2")))

	    ;; Expand `tramp-default-method-alist'.
	    (add-to-list
	     'tramp-default-method-alist '("host1" "user1" "method1"))
	    (add-to-list
	     'tramp-default-method-alist '("host2" "user2" "method2"))
	    (add-to-list
	     'tramp-default-method-alist '("host3" "user3" "method3"))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/[/user1@host1"
		"|/user2@host2"
		"|/user3@host3]/path/to/file"))
	      (if tramp-show-ad-hoc-proxies
		  (concat
		   "/[method1/user1@host1"
		   "|method2/user2@host2"
		   "|method3/user3@host3]")
		"/[method3/user3@host3]")))

	    ;; Expand `tramp-default-user-alist'.
	    (add-to-list 'tramp-default-user-alist '("method1" "host1" "user1"))
	    (add-to-list 'tramp-default-user-alist '("method2" "host2" "user2"))
	    (add-to-list 'tramp-default-user-alist '("method3" "host3" "user3"))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/[method1/host1"
		"|method2/host2"
		"|method3/host3]/path/to/file"))
	      (if tramp-show-ad-hoc-proxies
		  (concat
		   "/[method1/user1@host1"
		   "|method2/user2@host2"
		   "|method3/user3@host3]")
		"/[method3/user3@host3]")))

	    ;; Expand `tramp-default-host-alist'.
	    (add-to-list 'tramp-default-host-alist '("method1" "user1" "host1"))
	    (add-to-list 'tramp-default-host-alist '("method2" "user2" "host2"))
	    (add-to-list 'tramp-default-host-alist '("method3" "user3" "host3"))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/[method1/user1@"
		"|method2/user2@"
		"|method3/user3@]/path/to/file"))
	      (if tramp-show-ad-hoc-proxies
		  (concat
		   "/[method1/user1@host1"
		   "|method2/user2@host2"
		   "|method3/user3@host3]")
		"/[method3/user3@host3]")))

	    ;; Ad-hoc user name and host name expansion.
	    (setq tramp-default-method-alist nil
		  tramp-default-user-alist nil
		  tramp-default-host-alist nil)
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/[method1/user1@host1"
		"|method2/user2@"
		"|method3/user3@]/path/to/file"))
	      (if tramp-show-ad-hoc-proxies
		  (concat
		   "/[method1/user1@host1"
		   "|method2/user2@host1"
		   "|method3/user3@host1]")
		"/[method3/user3@host1]")))
	    (should
	     (string-equal
	      (file-remote-p
	       (concat
		"/[method1/%u@%h"
		"|method2/user2@host2"
		"|method3/%u@%h"
		"|method4/user4%domain4@host4#1234]/path/to/file"))
	      (if tramp-show-ad-hoc-proxies
		  (concat
		   "/[method1/user2@host2"
		   "|method2/user2@host2"
		   "|method3/user4@host4"
		   "|method4/user4%domain4@host4#1234]")
		"/[method4/user4%domain4@host4#1234]")))))

      ;; Exit.
      (tramp-change-syntax syntax))))

(ert-deftest tramp-test03-file-name-defaults ()
  "Check default values for some methods."
  :tags '(:expensive-test)
  (skip-unless (eq tramp-syntax 'default))

  ;; Default values in tramp-adb.el.
  (when (assoc "adb" tramp-methods)
    (should (string-equal (file-remote-p "/adb::" 'host) "")))
  ;; Default values in tramp-ftp.el.
  (when (assoc "ftp" tramp-methods)
    (should (string-equal (file-remote-p "/-:ftp.host:" 'method) "ftp"))
    (dolist (u '("ftp" "anonymous"))
      (should
       (string-equal (file-remote-p (format "/-:%s@:" u) 'method) "ftp"))))
  ;; Default values in tramp-sh.el and tramp-sudoedit.el.
  (when (assoc "su" tramp-methods)
    ;; "::1" is used as "[::1]" in remote file names.
    (dolist (h (cons "[::1]" (delete "::1" tramp-local-host-names)))
      (should
       (string-equal (file-remote-p (format "/-:root@%s:" h) 'method) "su"))))
  (dolist (m '("su" "sudo" "ksu" "doas" "sudoedit"))
    (when (assoc m tramp-methods)
      (should (string-equal (file-remote-p (format "/%s::" m) 'user) "root"))
      (should
       (string-equal (file-remote-p (format "/%s::" m) 'host) (system-name)))))
  (dolist (m '("rcp" "remcp" "rsh" "telnet" "krlogin" "fcp" "nc"))
    (when (assoc m tramp-methods)
      (should
       (string-equal
	(file-remote-p (format "/%s::" m) 'user) (user-login-name)))))
  ;; Default values in tramp-smb.el.
  (when (assoc "smb" tramp-methods)
    (should (string-equal (file-remote-p "/smb::" 'user) nil))))

;; The following test is inspired by Bug#30946.
(ert-deftest tramp-test03-file-name-host-rules ()
  "Check host name rules for host-less methods."
  (skip-unless (eq tramp-syntax 'default))
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-sh-p))

  ;; Host names must match rules in case the command template of a
  ;; method doesn't use them.
  (dolist (m '("su" "sg" "sudo" "doas" "ksu"))
    (when (assoc m tramp-methods)
      (let (tramp-connection-properties tramp-default-proxies-alist)
	(tramp-cleanup-connection tramp-test-vec 'keep-debug 'keep-password)
	;; Single hop.  The host name must match `tramp-local-host-regexp'.
	(should-error
	 (find-file (format "/%s:foo:" m))
	 :type 'user-error)
	;; Multi hop.  The host name must match the previous hop.
	(should-error
	 (find-file
	  (format
	   "%s|%s:foo:"
	   (substring (file-remote-p ert-remote-temporary-file-directory) 0 -1)
	   m))
	 :type 'user-error)))))

(ert-deftest tramp-test03-file-error ()
  "Check that Tramp signals an error in case of connection problems."
  (skip-unless (tramp-file-name-p tramp-test-vec))

  ;; Connect to a non-existing host.
  (let ((vec (copy-tramp-file-name tramp-test-vec))
	;; Don't poison it.
	(tramp-default-proxies-alist tramp-default-proxies-alist)
	(tramp-show-ad-hoc-proxies t))
    (cl-letf* (((symbol-function #'read-string) #'ignore) ; Suppress password.
	       ((symbol-function #'y-or-n-p) #'ignore) ; distrobox.
	       ((tramp-file-name-host vec) "example.com.invalid"))
      (should-error
       (file-exists-p (tramp-make-tramp-file-name vec))
       ;; `user-error' is raised if the host shall be local.
       ;; `remote-file-error' is raised if the host cannot be connected.
       :type (if (tramp--test-ange-ftp-p)
		 'ftp-error '(user-error remote-file-error)))
      (should-error
       (file-exists-p (tramp-make-tramp-file-name vec))
       ;; `ftp-error' and `remote-file-error' are subcategories of
       ;; `file-error'.  Let's check this as well.
       :type '(user-error file-error))
      ;; Check multi-hop.  `tramp-file-name-with-sudo' does not work
      ;; for the "kubernetes" method due to connection-local
      ;; `tramp-extra-expand-args'.
      (unless (tramp--test-kubernetes-p)
	(should-error
	 (file-exists-p
	  (tramp-file-name-with-sudo (tramp-make-tramp-file-name vec)))
	 :type '(user-error file-error))))))

(ert-deftest tramp-test04-substitute-in-file-name ()
  "Check `substitute-in-file-name'."
  (skip-unless (eq tramp-syntax 'default))

  ;; Suppress method name check.  We cannot use the string "foo" as
  ;; user name, because (substitute-in-string "/~foo") returns
  ;; different values depending on the existence of user "foo" (see
  ;; Bug#43052).
  (let ((tramp-methods (cons '("method") tramp-methods))
        (foo (downcase (md5 (current-time-string)))))
    (should
     (string-equal (substitute-in-file-name "/method:host:///foo") "/foo"))
    (should
     (string-equal
      (substitute-in-file-name "/method:host://foo") "/method:host:/foo"))
    (should
     (string-equal (substitute-in-file-name "/method:host:/path///foo") "/foo"))
    (should
     (string-equal
      (substitute-in-file-name "/method:host:/path//foo") "/method:host:/foo"))
    ;; Quoting local part.
    (should
     (string-equal
      (substitute-in-file-name "/method:host:/:///foo")
      "/method:host:/:///foo"))
    (should
     (string-equal
      (substitute-in-file-name "/method:host:/://foo") "/method:host:/://foo"))
    (should
     (string-equal
      (substitute-in-file-name "/method:host:/:/path///foo")
      "/method:host:/:/path///foo"))
    (should
     (string-equal
      (substitute-in-file-name "/method:host:/:/path//foo")
      "/method:host:/:/path//foo"))

    (should
     (string-equal
      (substitute-in-file-name (concat "/method:host://~" foo))
      (concat "/~" foo)))
    (should
     (string-equal
      (substitute-in-file-name (concat "/method:host:/~" foo))
      (concat "/method:host:/~" foo)))
    (should
     (string-equal
      (substitute-in-file-name (concat "/method:host:/path//~" foo))
      (concat "/~" foo)))
    ;; (substitute-in-file-name "/path/~foo") expands only for a local
    ;; user "foo" to "/~foo"".  Otherwise, it doesn't expand.
    (should
     (string-equal
      (substitute-in-file-name (concat "/method:host:/path/~" foo))
      (concat "/method:host:/path/~" foo)))
    ;; Quoting local part.
    (should
     (string-equal
      (substitute-in-file-name (concat "/method:host:/://~" foo))
      (concat "/method:host:/://~" foo)))
    (should
     (string-equal
      (substitute-in-file-name (concat "/method:host:/:/~" foo))
      (concat "/method:host:/:/~" foo)))
    (should
     (string-equal
      (substitute-in-file-name (concat "/method:host:/:/path//~" foo))
      (concat "/method:host:/:/path//~" foo)))
    (should
     (string-equal
      (substitute-in-file-name (concat "/method:host:/:/path/~" foo))
      (concat "/method:host:/:/path/~" foo)))

    (let (process-environment)
      (should
       (string-equal
	(substitute-in-file-name "/method:host:/path/$FOO")
	"/method:host:/path/$FOO"))
      (setenv "FOO" "bla")
      (should
       (string-equal
	(substitute-in-file-name "/method:host:/path/$FOO")
	"/method:host:/path/bla"))
      (should
       (string-equal
	(substitute-in-file-name "/method:host:/path/$$FOO")
	"/method:host:/path/$FOO"))
      ;; Quoting local part.
      (should
       (string-equal
	(substitute-in-file-name "/method:host:/:/path/$FOO")
	"/method:host:/:/path/$FOO"))
      (setenv "FOO" "bla")
      (should
       (string-equal
	(substitute-in-file-name "/method:host:/:/path/$FOO")
	"/method:host:/:/path/$FOO"))
      (should
       (string-equal
	(substitute-in-file-name "/method:host:/:/path/$$FOO")
	"/method:host:/:/path/$$FOO")))))

(ert-deftest tramp-test05-expand-file-name ()
  "Check `expand-file-name'."
  (skip-unless (eq tramp-syntax 'default))

  ;; Suppress method name check.
  (let ((tramp-methods (cons '("method") tramp-methods)))
    (should
     (string-equal
      (expand-file-name "/method:host:/path/./file") "/method:host:/path/file"))
    (should
     (string-equal
      (expand-file-name "/method:host:/path/../file") "/method:host:/file"))
    (should
     (string-equal
      (expand-file-name "/method:host:/path/.") "/method:host:/path"))
    (should
     (string-equal
      (expand-file-name "/method:host:/path/..") "/method:host:/"))
    (should
     (string-equal
      (expand-file-name "." "/method:host:/path/") "/method:host:/path"))
    (should
     (string-equal
      (expand-file-name "" "/method:host:/path/") "/method:host:/path"))
    ;; Quoting local part.
    (should
     (string-equal
      (expand-file-name "/method:host:/:/path/./file")
      "/method:host:/:/path/file"))
    (should
     (string-equal
      (expand-file-name "/method:host:/:/path/../file") "/method:host:/:/file"))
    (should
     (string-equal
      (expand-file-name "/method:host:/:/~/path/./file")
      "/method:host:/:/~/path/file"))))

;; The following test is inspired by Bug#26911 and Bug#34834.  They
;; were bugs in `expand-file-name'.
(ert-deftest tramp-test05-expand-file-name-relative ()
  "Check `expand-file-name'."
  (skip-unless (tramp--test-enabled))
  ;; Methods with a share do not expand "/path/..".
  (skip-unless (not (tramp--test-share-p)))

  (should
   (string-equal
    (let ((default-directory
	    (concat
	     (file-remote-p ert-remote-temporary-file-directory) "/path")))
      (expand-file-name ".." "./"))
    (concat (file-remote-p ert-remote-temporary-file-directory) "/"))))

(ert-deftest tramp-test05-expand-file-name-top ()
  "Check `expand-file-name'."
  (skip-unless (tramp--test-enabled))
  (skip-unless (not (tramp--test-ange-ftp-p)))

  (let ((dir (concat (file-remote-p ert-remote-temporary-file-directory) "/")))
    (dolist (local '("." ".."))
      (should (string-equal (expand-file-name local dir) dir))
      (should (string-equal (expand-file-name (concat dir local)) dir)))))

;; The following test is inspired by Bug#65685.
(ert-deftest tramp-test05-expand-file-name-tilde ()
  "Check `expand-file-name'."
  (skip-unless (tramp--test-enabled))
  (skip-unless (not (tramp--test-ange-ftp-p)))

  (let ((dir (file-remote-p ert-remote-temporary-file-directory))
	(tramp-tolerate-tilde t))
    (should (string-equal (expand-file-name (concat dir "~"))
			  (expand-file-name (concat dir "/:~"))))))

(ert-deftest tramp-test06-directory-file-name ()
  "Check `directory-file-name'.
This checks also `file-name-as-directory', `file-name-directory',
`file-name-nondirectory' and `unhandled-file-name-directory'."
  (skip-unless (eq tramp-syntax 'default))

  ;; Suppress method name check.
  (let ((tramp-methods (cons '("method") tramp-methods)))
    (should
     (string-equal
      (directory-file-name "/method:host:/path/to/file")
      "/method:host:/path/to/file"))
    (should
     (string-equal
      (directory-file-name "/method:host:/path/to/file/")
      "/method:host:/path/to/file"))
    (should
     (string-equal
      (directory-file-name "/method:host:/path/to/file//")
      "/method:host:/path/to/file"))
    (should
     (string-equal
      (file-name-as-directory "/method:host:/path/to/file")
      "/method:host:/path/to/file/"))
    (should
     (string-equal
      (file-name-as-directory "/method:host:/path/to/file/")
      "/method:host:/path/to/file/"))
    (should
     (string-equal
      (file-name-directory "/method:host:/path/to/file")
      "/method:host:/path/to/"))
    (should
     (string-equal
      (file-name-directory "/method:host:/path/to/file/")
      "/method:host:/path/to/file/"))
    (should
     (string-equal (file-name-directory "/method:host:file") "/method:host:"))
    (should
     (string-equal
      (file-name-directory "/method:host:path/") "/method:host:path/"))
    (should
     (string-equal
      (file-name-directory "/method:host:path/to") "/method:host:path/"))
    (should
     (string-equal
      (file-name-nondirectory "/method:host:/path/to/file") "file"))
    (should
     (string-equal (file-name-nondirectory "/method:host:/path/to/file/") ""))
    (should-not
     (unhandled-file-name-directory "/method:host:/path/to/file")))

  ;; Bug#10085.
  (when (tramp--test-enabled) ;; Packages like tramp-gvfs.el might be disabled.
    (dolist (non-essential '(nil t))
      ;; We must clear `tramp-default-method'.
      (let ((tramp-default-method
	     (file-remote-p ert-remote-temporary-file-directory 'method))
	    (host-port
	     (file-remote-p ert-remote-temporary-file-directory 'host)))
	(dolist
	    (file
	     `(,(format "/%s::" tramp-default-method)
	       ,(format
		 "/-:%s:"
		 ;; `(file-remote-p ... 'host)' eliminates IPv6
		 ;; delimiters.  Add them.
		 (if (string-match tramp-ipv6-regexp host-port)
		     (replace-match
		      (format
		       "%s\\&%s"
		       tramp-prefix-ipv6-format tramp-postfix-ipv6-format)
		      nil nil host-port)
		   host-port))))
	  (should (string-equal (directory-file-name file) file))
	  (should
	   (string-equal
	    (file-name-as-directory file)
	    (if non-essential
		file (concat file (if (tramp--test-ange-ftp-p) "/" "./")))))
	  (should (string-equal (file-name-directory file) file))
	  (should (string-equal (file-name-nondirectory file) "")))))))

(ert-deftest tramp-test07-abbreviate-file-name ()
  "Check that Tramp abbreviates file names correctly."
  (skip-unless (tramp--test-enabled))
  (skip-unless (not (tramp--test-ange-ftp-p)))
  ;; `abbreviate-file-name' is supported since Emacs 29.1.
  (skip-unless (tramp--test-emacs29-p))

  ;; We must refill the cache.  `file-truename' does it.
  (file-truename ert-remote-temporary-file-directory)
  (let* ((remote-host (file-remote-p ert-remote-temporary-file-directory))
	 (remote-host-nohop
	  (tramp-make-tramp-file-name (tramp-dissect-file-name remote-host)))
	 ;; Not all methods can expand "~".
         (home-dir (ignore-errors (expand-file-name (concat remote-host "~"))))
	 home-dir-nohop)
    (skip-unless home-dir)

    ;; Check home-dir abbreviation.
    (unless (string-suffix-p "~" home-dir)
      (should (equal (abbreviate-file-name (concat home-dir "/foo/bar"))
                     (concat remote-host-nohop "~/foo/bar")))
      (should (equal (abbreviate-file-name
		      (concat remote-host "/nowhere/special"))
                     (concat remote-host-nohop "/nowhere/special"))))

    ;; Check `directory-abbrev-alist' abbreviation.
    (let ((directory-abbrev-alist
           `((,(rx bos (literal home-dir) "/foo") . ,(concat home-dir "/f"))
             (,(rx bos (literal remote-host) "/nowhere")
	      . ,(concat remote-host "/nw")))))
      (should (equal (abbreviate-file-name (concat home-dir "/foo/bar"))
                     (concat remote-host-nohop "~/f/bar")))
      (should (equal (abbreviate-file-name
		      (concat remote-host "/nowhere/special"))
                     (concat remote-host-nohop "/nw/special"))))

    ;; Check that home-dir abbreviation doesn't occur when home-dir is just "/".
    (setq home-dir (concat remote-host "/")
	  home-dir-nohop
	  (tramp-make-tramp-file-name (tramp-dissect-file-name home-dir)))
    ;; The remote home directory is kept in the connection property "~".
    ;; We fake this setting.
    (tramp-set-connection-property tramp-test-vec "~" (file-local-name home-dir))
    (should (equal (abbreviate-file-name (concat home-dir "foo/bar"))
		   (concat home-dir-nohop "foo/bar")))
    (tramp-flush-connection-property tramp-test-vec "~")))

(ert-deftest tramp-test07-file-exists-p ()
  "Check `file-exist-p', `write-region' and `delete-file'."
  (skip-unless (tramp--test-enabled))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let ((tmp-name (tramp--test-make-temp-name nil quoted)))
      (should-not (file-exists-p tmp-name))
      (write-region "foo" nil tmp-name)
      (should (file-exists-p tmp-name))
      (delete-file tmp-name)
      (should-not (file-exists-p tmp-name))

      ;; Trashing files doesn't work when `system-move-file-to-trash'
      ;; is defined (on MS-Windows and macOS), and for encrypted
      ;; remote files.
      (unless (or (fboundp 'system-move-file-to-trash) (tramp--test-crypt-p))
	(let ((trash-directory (tramp--test-make-temp-name 'local quoted))
	      (delete-by-moving-to-trash t))
	  (make-directory trash-directory)
	  (should-not (file-exists-p tmp-name))
	  (write-region "foo" nil tmp-name)
	  (should (file-exists-p tmp-name))
	  (delete-file tmp-name 'trash)
	  (should-not (file-exists-p tmp-name))
	  (should
	   (or (file-exists-p
		(expand-file-name
		 (file-name-nondirectory tmp-name) trash-directory))
	       ;; Gdrive.
	       (file-symlink-p
		(expand-file-name
		 (file-name-nondirectory tmp-name) trash-directory))))
	  (delete-directory trash-directory 'recursive)
	  (should-not (file-exists-p trash-directory))))

      ;; Setting `remote-file-name-inhibit-delete-by-moving-to-trash'
      ;; prevents trashing remote files.
      (let ((trash-directory (tramp--test-make-temp-name 'local quoted))
	    (delete-by-moving-to-trash t)
	    (remote-file-name-inhibit-delete-by-moving-to-trash t))
	(make-directory trash-directory)
	(should-not (file-exists-p tmp-name))
	(write-region "foo" nil tmp-name)
	(should (file-exists-p tmp-name))
	(delete-file tmp-name 'trash)
	(should-not (file-exists-p tmp-name))
	(should-not
	 (file-exists-p
	  (expand-file-name (file-name-nondirectory tmp-name) trash-directory)))
	(delete-directory trash-directory 'recursive)
	(should-not (file-exists-p trash-directory))))))

(ert-deftest tramp-test08-file-local-copy ()
  "Check `file-local-copy'."
  (skip-unless (tramp--test-enabled))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let ((tmp-name1 (tramp--test-make-temp-name nil quoted))
	  tmp-name2)
      (unwind-protect
	  (progn
	    (write-region "foo" nil tmp-name1)
	    (should (setq tmp-name2 (file-local-copy tmp-name1)))
	    (with-temp-buffer
	      (insert-file-contents tmp-name2)
	      (should (string-equal (buffer-string) "foo")))
	    ;; Check also that a file transfer with compression works.
	    (let ((default-directory ert-remote-temporary-file-directory)
		  (tramp-copy-size-limit 4)
		  (tramp-inline-compress-start-size 2))
	      (delete-file tmp-name2)
	      (should (setq tmp-name2 (file-local-copy tmp-name1))))
	    ;; Error case.
	    (delete-file tmp-name1)
	    (delete-file tmp-name2)
	    (should-error
	     (setq tmp-name2 (file-local-copy tmp-name1))
	     :type 'file-missing))

	;; Cleanup.
	(ignore-errors
	  (delete-file tmp-name1)
	  (delete-file tmp-name2))))))

(ert-deftest tramp-test09-insert-file-contents ()
  "Check `insert-file-contents'."
  (skip-unless (tramp--test-enabled))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let ((tmp-name (tramp--test-make-temp-name nil quoted)))
      (unwind-protect
	  (with-temp-buffer
	    (write-region "foo" nil tmp-name)
	    (let ((point (point)))
	      (should
	       (equal
		(insert-file-contents tmp-name)
		`(,(expand-file-name tmp-name) 3)))
	      (should (string-equal (buffer-string) "foo"))
	      (should (= point (point))))
	    (goto-char (1+ (point)))
	    (let ((point (point)))
	      (should
	       (equal
		(insert-file-contents tmp-name)
		`(,(expand-file-name tmp-name) 3)))
	      (should (string-equal (buffer-string) "ffoooo"))
	      (should (= point (point))))
	    ;; Insert partly.
	    (let ((point (point)))
	      (should
	       (equal
		(insert-file-contents tmp-name nil 1 3)
		`(,(expand-file-name tmp-name) 2)))
	      (should (string-equal (buffer-string) "foofoooo"))
	      (should (= point (point))))
	    (let ((point (point)))
	      (should
	       (equal
		(insert-file-contents tmp-name nil 2 5)
		`(,(expand-file-name tmp-name) 1)))
	      (should (string-equal (buffer-string) "fooofoooo"))
	      (should (= point (point))))
	    ;; Replace.
	    (let ((point (point)))
	      ;; 0 characters replaced, because "foo" is already there.
	      (should
	       (equal
		(insert-file-contents tmp-name nil nil nil 'replace)
		`(,(expand-file-name tmp-name) 0)))
	      (should (string-equal (buffer-string) "foo"))
	      (should (= point (point))))
	    ;; Insert another string.
	    (let ((point (point)))
	      (replace-string-in-region "foo" "bar" (point-min) (point-max))
	      (goto-char point)
	      (should
	       (equal
		(insert-file-contents tmp-name nil nil nil 'replace)
		`(,(expand-file-name tmp-name) 3)))
	      (should (string-equal (buffer-string) "foo"))
	      (should (= point (point))))
	    ;; Error case.
	    (delete-file tmp-name)
	    (should-error
	     (insert-file-contents tmp-name)
	     :type 'file-missing))

	;; Cleanup.
	(ignore-errors (delete-file tmp-name))))))

(ert-deftest tramp-test10-write-region ()
  "Check `write-region'."
  (skip-unless (tramp--test-enabled))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let ((tmp-name (tramp--test-make-temp-name nil quoted))
          (inhibit-message (not (ignore-errors (edebug-mode)))))
      (unwind-protect
	  (progn
            ;; Write buffer.  Use absolute and relative file name.
	    (with-temp-buffer
	      (insert "foo")
	      (write-region nil nil tmp-name))
	    (with-temp-buffer
	      (insert-file-contents tmp-name)
	      (should (string-equal (buffer-string) "foo")))
	    (delete-file tmp-name)
	    (with-temp-buffer
	      (insert "foo")
	      (should-not (file-exists-p tmp-name))
	      (let ((default-directory (file-name-directory tmp-name)))
		(should-not (file-exists-p (file-name-nondirectory tmp-name)))
		(write-region nil nil (file-name-nondirectory tmp-name))
		(should (file-exists-p (file-name-nondirectory tmp-name))))
	      (should (file-exists-p tmp-name)))
	    (with-temp-buffer
	      (insert-file-contents tmp-name)
	      (should (string-equal (buffer-string) "foo")))

	    ;; Append.
	    (unless (tramp--test-ange-ftp-p)
	      (with-temp-buffer
		(insert "bla")
		(write-region nil nil tmp-name 'append))
	      (with-temp-buffer
		(insert-file-contents tmp-name)
		(should (string-equal (buffer-string) "foobla")))
	      (with-temp-buffer
		(insert "baz")
		(write-region nil nil tmp-name 3))
	      (with-temp-buffer
		(insert-file-contents tmp-name)
		(should (string-equal (buffer-string) "foobaz")))
	      (delete-file tmp-name)
	      (with-temp-buffer
		(insert "foo")
		(write-region nil nil tmp-name 'append))
	      (with-temp-buffer
		(insert-file-contents tmp-name)
		(should (string-equal (buffer-string) "foo"))))

	    ;; Write string.
	    (write-region "foo" nil tmp-name)
	    (with-temp-buffer
	      (insert-file-contents tmp-name)
	      (should (string-equal (buffer-string) "foo")))

	    ;; Write empty string.  Used for creation of temporary files.
	    (should-error
	     (make-empty-file tmp-name)
	     :type 'file-already-exists)
	    (delete-file tmp-name)
	    (make-empty-file tmp-name)
	    (with-temp-buffer
	      (insert-file-contents tmp-name)
	      (should (string-equal (buffer-string) "")))

	    ;; Write partly.
	    (with-temp-buffer
	      (insert "123456789")
	      (write-region 3 5 tmp-name))
	    (with-temp-buffer
	      (insert-file-contents tmp-name)
	      (should (string-equal (buffer-string) "34")))

	    ;; Check message.
	    (let (inhibit-message)
	      (dolist (noninteractive (unless (tramp--test-ange-ftp-p) '(nil t)))
		(dolist (visit '(nil t "string" no-message))
		  (ert-with-message-capture tramp--test-messages
		    (write-region "foo" nil tmp-name nil visit)
		    ;; We must check the last line.  There could be
		    ;; other messages from the progress reporter.
		    (should
		     (string-match-p
		      (if (and (null noninteractive)
			       (or (eq visit t) (null visit) (stringp visit)))
			  (rx bol "Wrote " (literal tmp-name) "\n" eos)
			(rx bos))
		      tramp--test-messages))))))

	    ;; We do not test the lock file here.  See
	    ;; `tramp-test39-make-lock-file-name'.

	    ;; Do not overwrite if excluded.
	    (cl-letf (((symbol-function #'y-or-n-p) #'always)
		      ;; Ange-FTP.
		      ((symbol-function #'yes-or-no-p) #'always))
	      (write-region "foo" nil tmp-name nil nil nil 'mustbenew))
	    (should-error
	     (cl-letf (((symbol-function #'y-or-n-p) #'ignore)
		       ;; Ange-FTP.
		       ((symbol-function #'yes-or-no-p) #'ignore))
	       (write-region "foo" nil tmp-name nil nil nil 'mustbenew))
             :type 'file-already-exists)
	    (should-error
	     (write-region "foo" nil tmp-name nil nil nil 'excl)
	     :type 'file-already-exists)
	    (delete-file tmp-name)

	    ;; Check `buffer-file-coding-system'.  Bug#65022.
	    (with-temp-buffer
	      (setq buffer-file-name tmp-name)
	      (insert "foo")
	      (set-buffer-file-coding-system 'cp1251)
	      (let ((bfcs buffer-file-coding-system))
		(should (buffer-modified-p))
		(should (null (save-buffer)))
		(should
                 (eq (coding-system-get buffer-file-coding-system :mime-charset)
                     (coding-system-get bfcs :mime-charset))))))

	;; Cleanup.
	(ignore-errors (delete-file tmp-name))))))

;; The following test is inspired by Bug#35497.
(ert-deftest tramp-test10-write-region-file-precious-flag ()
  "Check that `file-precious-flag' is respected with Tramp in use."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-sh-p))

  (let* ((tmp-name (tramp--test-make-temp-name))
         (inhibit-message (not (ignore-errors (edebug-mode))))
         written-files
         (advice (lambda (_start _end filename &rest _r)
                   (push filename written-files))))

    (unwind-protect
        (with-current-buffer (find-file-noselect tmp-name)
          ;; Write initial contents.  Adapt `visited-file-modtime'
          ;; in order to suppress confirmation.
          (insert "foo")
          (write-region nil nil tmp-name)
          (set-visited-file-modtime)
          ;; Run the test.
          (advice-add 'write-region :before advice)
          (setq-local file-precious-flag t)
          (setq-local backup-inhibited t)
          (insert "bar")
	  (should (buffer-modified-p))
          (should (null (save-buffer)))
	  (should (not (buffer-modified-p)))
          (should-not (cl-member tmp-name written-files :test #'string=)))

      ;; Cleanup.
      (ignore-errors (advice-remove 'write-region advice))
      (ignore-errors (delete-file tmp-name)))))

;; The following test is inspired by Bug#55166.
(ert-deftest tramp-test10-write-region-other-file-name-handler ()
  "Check that another file name handler in VISIT is acknowledged."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-enabled))
  (skip-unless (not (tramp--test-ange-ftp-p)))
  (skip-unless (executable-find "gzip"))

  (let* ((default-directory ert-remote-temporary-file-directory)
	 (archive (ert-resource-file "foo.tar.gz"))
	 (tmp-file (expand-file-name (file-name-nondirectory archive)))
	 (require-final-newline t)
	 (inhibit-message (not (ignore-errors (edebug-mode))))
         (backup-inhibited t)
	 create-lockfiles buffer1 buffer2)
    (unwind-protect
	(progn
	  (copy-file archive tmp-file 'ok)
	  ;; Read archive.  Check contents of foo.txt, and modify it.  Save.
	  (with-current-buffer (setq buffer1 (find-file-noselect tmp-file))
	    (should (tar-goto-file "foo.txt"))
	    (save-current-buffer
	      (setq buffer2 (tar-extract))
	      (should (string-equal (buffer-string) "foo\n"))
	      (goto-char (point-max))
	      (insert "bar")
	      (should (buffer-modified-p))
              (should (null (save-buffer)))
	      (should-not (buffer-modified-p)))
	    (should (buffer-modified-p))
            (should (null (save-buffer)))
	    (should-not (buffer-modified-p)))

	  (kill-buffer buffer1)
	  (kill-buffer buffer2)
	  ;; Read archive.  Check contents of modified foo.txt.
	  (with-current-buffer (setq buffer1 (find-file-noselect tmp-file))
	    (should (tar-goto-file "foo.txt"))
	    (save-current-buffer
	      (setq buffer2 (tar-extract))
	      (should (string-equal (buffer-string) "foo\nbar\n")))))

      ;; Cleanup.
      ;; Modifying `read-from-minibuffer' doesn't work on MS Windows.
      ;; `kill-buffer--possibly-save' exists since Emacs 29.1.
      (if (fboundp 'kill-buffer--possibly-save)
	  (cl-letf (((symbol-function #'kill-buffer--possibly-save) #'always))
	    (ignore-errors (kill-buffer buffer1))
	    (ignore-errors (kill-buffer buffer2)))
	(ignore-errors (kill-buffer buffer1))
	(ignore-errors (kill-buffer buffer2)))
      (ignore-errors (delete-file tmp-file)))))

(ert-deftest tramp-test11-copy-file ()
  "Check `copy-file'."
  (skip-unless (tramp--test-enabled))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let ((tmp-name1 (tramp--test-make-temp-name nil quoted))
	  (tmp-name2 (tramp--test-make-temp-name nil quoted))
	  (tmp-name3 (tramp--test-make-temp-name 'local quoted))
	  (tmp-name4
	   (file-name-nondirectory (tramp--test-make-temp-name 'local quoted))))
      (dolist (source-target
	       `(;; Copy on remote side.
		 (,tmp-name1 . ,tmp-name2)
		 ;; Copy from remote side to local side.
		 (,tmp-name1 . ,tmp-name3)
		 ;; Copy from local side to remote side.
		 (,tmp-name3 . ,tmp-name1)))
	(let* ((source (car source-target))
	       (source-link
		(expand-file-name tmp-name4 (file-name-directory source)))
	       (target (cdr source-target))
	       (target-link
		(expand-file-name tmp-name4 (file-name-directory target))))

	  ;; Copy simple file.
	  (unwind-protect
	      (progn
		(should-error
		 (copy-file source target)
		 :type 'file-missing)
		(write-region "foo" nil source)
		(should (file-exists-p source))
		(copy-file source target)
		(should (file-exists-p target))
		(with-temp-buffer
		  (insert-file-contents target)
		  (should (string-equal (buffer-string) "foo")))
		(when (tramp--test-expensive-test-p)
		  (should-error
		   (copy-file source target)
		   :type 'file-already-exists))
		(copy-file source target 'ok))

	    ;; Cleanup.
	    (ignore-errors (delete-file source))
	    (ignore-errors (delete-file target)))

	  ;; Copy symlinked file.
	  (unwind-protect
	      (tramp--test-ignore-make-symbolic-link-error
	       (write-region "foo" nil source-link)
	       (should (file-exists-p source-link))
	       (make-symbolic-link tmp-name4 source)
	       (should (file-exists-p source))
	       (should (string-equal (file-symlink-p source) tmp-name4))
	       (copy-file source target)
	       ;; Some backends like tramp-gvfs.el do not create the
	       ;; link on the target.
	       (when (file-symlink-p target)
		 (should (string-equal (file-symlink-p target) tmp-name4))))

	    ;; Cleanup.
	    (ignore-errors (delete-file source))
	    (ignore-errors (delete-file source-link))
	    (ignore-errors (delete-file target))
	    (ignore-errors (delete-file target-link)))

	  ;; Copy file to directory.
	  (unwind-protect
	      ;; This doesn't work on FTP.
	      (unless (tramp--test-ange-ftp-p)
		(write-region "foo" nil source)
		(should (file-exists-p source))
		(make-directory target)
		(should (file-directory-p target))
		(when (tramp--test-expensive-test-p)
		  (should-error
		   (copy-file source target)
		   :type 'file-already-exists)
		  (should-error
		   (copy-file source target 'ok)
		   :type 'file-error))
		(copy-file source (file-name-as-directory target))
		(should
		 (file-exists-p
		  (expand-file-name (file-name-nondirectory source) target))))

	    ;; Cleanup.
	    (ignore-errors (delete-file source))
	    (ignore-errors (delete-directory target 'recursive)))

	  ;; Copy directory to existing directory.
	  (unwind-protect
	      ;; This doesn't work on FTP.
	      (unless (tramp--test-ange-ftp-p)
		(make-directory source)
		(should (file-directory-p source))
		(write-region "foo" nil (expand-file-name "foo" source))
		(should (file-exists-p (expand-file-name "foo" source)))
		(make-directory target)
		(should (file-directory-p target))
		;; Directory `target' exists already, so we must use
		;; `file-name-as-directory'.
		(copy-file source (file-name-as-directory target))
		(should
		 (file-exists-p
		  (expand-file-name
		   (concat (file-name-nondirectory source) "/foo") target))))

	    ;; Cleanup.
	    (ignore-errors (delete-directory source 'recursive))
	    (ignore-errors (delete-directory target 'recursive)))

	  ;; Copy directory/file to non-existing directory.
	  (unwind-protect
	      ;; This doesn't work on FTP.
	      (unless (tramp--test-ange-ftp-p)
		(make-directory source)
		(should (file-directory-p source))
		(write-region "foo" nil (expand-file-name "foo" source))
		(should (file-exists-p (expand-file-name "foo" source)))
		(make-directory target)
		(should (file-directory-p target))
		(copy-file
		 source
		 (expand-file-name (file-name-nondirectory source) target))
		(should
		 (file-exists-p
		  (expand-file-name
		   (concat (file-name-nondirectory source) "/foo") target))))

	    ;; Cleanup.
	    (ignore-errors (delete-directory source 'recursive))
	    (ignore-errors (delete-directory target 'recursive))))))))

(ert-deftest tramp-test12-rename-file ()
  "Check `rename-file'."
  (skip-unless (tramp--test-enabled))

  (dolist (quoted  (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let ((tmp-name1 (tramp--test-make-temp-name nil quoted))
	  (tmp-name2 (tramp--test-make-temp-name nil quoted))
	  (tmp-name3 (tramp--test-make-temp-name 'local quoted))
	  (tmp-name4
	   (file-name-nondirectory (tramp--test-make-temp-name 'local quoted))))
      (dolist (source-target
	       `(;; Rename on remote side.
		 (,tmp-name1 . ,tmp-name2)
		 ;; Rename from remote side to local side.
		 (,tmp-name1 . ,tmp-name3)
		 ;; Rename from local side to remote side.
		 (,tmp-name3 . ,tmp-name1)))
	(let* ((source (car source-target))
	       (source-link
		(expand-file-name tmp-name4 (file-name-directory source)))
	       (target (cdr source-target))
	       (target-link
		(expand-file-name tmp-name4 (file-name-directory target))))

	  ;; Rename simple file.
	  (unwind-protect
	      (progn
		(should-error
		 (rename-file source target)
		 :type 'file-missing)
		(write-region "foo" nil source)
		(should (file-exists-p source))
		(rename-file source target)
		(should-not (file-exists-p source))
		(should (file-exists-p target))
		(with-temp-buffer
		  (insert-file-contents target)
		  (should (string-equal (buffer-string) "foo")))
		(write-region "foo" nil source)
		(should (file-exists-p source))
		(when (tramp--test-expensive-test-p)
		  (should-error
		   (rename-file source target)
		   :type 'file-already-exists))
		(rename-file source target 'ok)
		(should-not (file-exists-p source)))

	    ;; Cleanup.
	    (ignore-errors (delete-file source))
	    (ignore-errors (delete-file target)))

	  ;; Rename symlinked file.
	  (unwind-protect
	      (tramp--test-ignore-make-symbolic-link-error
	       (write-region "foo" nil source-link)
	       (should (file-exists-p source-link))
	       (make-symbolic-link tmp-name4 source)
	       (should (file-exists-p source))
	       (should (string-equal (file-symlink-p source) tmp-name4))
	       (rename-file source target)
	       (should-not (file-exists-p source))
	       ;; Some backends like tramp-gvfs.el do not create the
	       ;; link on the target.
	       (when (file-symlink-p target)
		 (should (string-equal (file-symlink-p target) tmp-name4))))

	    ;; Cleanup.
	    (ignore-errors (delete-file source))
	    (ignore-errors (delete-file source-link))
	    (ignore-errors (delete-file target))
	    (ignore-errors (delete-file target-link)))

	  ;; Rename file to directory.
	  (unwind-protect
	      (progn
		(write-region "foo" nil source)
		(should (file-exists-p source))
		(make-directory target)
		(should (file-directory-p target))
		(when (tramp--test-expensive-test-p)
		  (should-error
		   (rename-file source target)
		   :type 'file-already-exists)
		  (should-error
		   (rename-file source target 'ok)
		   :type 'file-error))
		(rename-file source (file-name-as-directory target))
		(should-not (file-exists-p source))
		(should
		 (file-exists-p
		  (expand-file-name (file-name-nondirectory source) target))))

	    ;; Cleanup.
	    (ignore-errors (delete-file source))
	    (ignore-errors (delete-directory target 'recursive)))

	  ;; Rename directory to existing directory.
	  (unwind-protect
	      ;; This doesn't work on FTP.
	      (unless (tramp--test-ange-ftp-p)
		(make-directory source)
		(should (file-directory-p source))
		(write-region "foo" nil (expand-file-name "foo" source))
		(should (file-exists-p (expand-file-name "foo" source)))
		(make-directory target)
		(should (file-directory-p target))
		;; Directory `target' exists already, so we must use
		;; `file-name-as-directory'.
		(rename-file source (file-name-as-directory target))
		(should-not (file-exists-p source))
		(should
		 (file-exists-p
		  (expand-file-name
		   (concat (file-name-nondirectory source) "/foo") target))))

	    ;; Cleanup.
	    (ignore-errors (delete-directory source 'recursive))
	    (ignore-errors (delete-directory target 'recursive)))

	  ;; Rename directory/file to non-existing directory.
	  (unwind-protect
	      ;; This doesn't work on FTP.
	      (unless (tramp--test-ange-ftp-p)
		(make-directory source)
		(should (file-directory-p source))
		(write-region "foo" nil (expand-file-name "foo" source))
		(should (file-exists-p (expand-file-name "foo" source)))
		(make-directory target)
		(should (file-directory-p target))
		(rename-file
		 source
		 (expand-file-name (file-name-nondirectory source) target))
		(should-not (file-exists-p source))
		(should
		 (file-exists-p
		  (expand-file-name
		   (concat (file-name-nondirectory source) "/foo") target))))

	    ;; Cleanup.
	    (ignore-errors (delete-directory source 'recursive))
	    (ignore-errors (delete-directory target 'recursive))))))))

(ert-deftest tramp-test13-make-directory ()
  "Check `make-directory'.
This tests also `file-directory-p' and `file-accessible-directory-p'."
  (skip-unless (tramp--test-enabled))

  ;; Since Emacs 29.1, `make-directory' has defined return values.
  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let* ((tmp-name1 (tramp--test-make-temp-name nil quoted))
	   (tmp-name2 (expand-file-name "foo/bar" tmp-name1))
	   (unusual-file-mode-1 #o740)
	   (unusual-file-mode-2 #o710))
      (unwind-protect
	  (progn
	    (with-file-modes unusual-file-mode-1
	      (if (tramp--test-emacs29-p)
		  (should-not (make-directory tmp-name1))
		(make-directory tmp-name1)))
	    (should-error
	     (make-directory tmp-name1)
	     :type 'file-already-exists)
	    (should (file-directory-p tmp-name1))
	    (should (file-accessible-directory-p tmp-name1))
	    (when (tramp--test-supports-set-file-modes-p)
	      (should (equal (format "%#o" unusual-file-mode-1)
			     (format "%#o" (file-modes tmp-name1)))))
	    (should-error
	     (make-directory tmp-name2)
	     :type 'file-error)
	    (with-file-modes unusual-file-mode-2
	      (if (tramp--test-emacs29-p)
		  (should-not (make-directory tmp-name2 'parents))
		(make-directory tmp-name2 'parents)))
	    (should (file-directory-p tmp-name2))
	    (should (file-accessible-directory-p tmp-name2))
	    (when (tramp--test-supports-set-file-modes-p)
	      (should (equal (format "%#o" unusual-file-mode-2)
			     (format "%#o" (file-modes tmp-name2)))))
	    ;; If PARENTS is non-nil, `make-directory' shall not
	    ;; signal an error when DIR exists already.  It returns t.
	    (if (tramp--test-emacs29-p)
		(should (make-directory tmp-name2 'parents))
	      (make-directory tmp-name2 'parents)))

	;; Cleanup.
	(ignore-errors (delete-directory tmp-name1 'recursive))))))

(ert-deftest tramp-test14-delete-directory ()
  "Check `delete-directory'."
  (skip-unless (tramp--test-enabled))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let* ((tmp-name1 (tramp--test-make-temp-name nil quoted))
	   (tmp-name2 (expand-file-name "foo" tmp-name1)))
      ;; Delete empty directory.
      (make-directory tmp-name1)
      (should (file-directory-p tmp-name1))
      (delete-directory tmp-name1)
      (should-not (file-directory-p tmp-name1))
      ;; Delete non-empty directory.
      (make-directory tmp-name1)
      (should (file-directory-p tmp-name1))
      (write-region "foo" nil (expand-file-name "bla" tmp-name1))
      (should (file-exists-p (expand-file-name "bla" tmp-name1)))
      (make-directory tmp-name2)
      (should (file-directory-p tmp-name2))
      (write-region "foo" nil (expand-file-name "bla" tmp-name2))
      (should (file-exists-p (expand-file-name "bla" tmp-name2)))
      (should-error
       (delete-directory tmp-name1)
       :type 'file-error)
      (delete-directory tmp-name1 'recursive)
      (should-not (file-directory-p tmp-name1))

      ;; Trashing directories doesn't work when
      ;; `system-move-file-to-trash' is defined (on MS Windows and
      ;; macOS), for encrypted remote directories and for ange-ftp.
      (when (and (not (fboundp 'system-move-file-to-trash))
		 (not (tramp--test-crypt-p)) (not (tramp--test-ftp-p)))
	(let ((trash-directory (tramp--test-make-temp-name 'local quoted))
	      (delete-by-moving-to-trash t))
	  (make-directory trash-directory)
	  ;; Delete empty directory.
	  (make-directory tmp-name1)
	  (should (file-directory-p tmp-name1))
	  (delete-directory tmp-name1 nil 'trash)
	  (should-not (file-directory-p tmp-name1))
	  (should
	   (file-exists-p
	    (expand-file-name
	     (file-name-nondirectory tmp-name1) trash-directory)))
	  (delete-directory trash-directory 'recursive)
	  (should-not (file-exists-p trash-directory))
	  ;; Delete non-empty directory.
	  (make-directory tmp-name1)
	  (should (file-directory-p tmp-name1))
	  (write-region "foo" nil (expand-file-name "bla" tmp-name1))
	  (should (file-exists-p (expand-file-name "bla" tmp-name1)))
	  (make-directory tmp-name2)
	  (should (file-directory-p tmp-name2))
	  (write-region "foo" nil (expand-file-name "bla" tmp-name2))
	  (should (file-exists-p (expand-file-name "bla" tmp-name2)))
	  (should-error
	   (delete-directory tmp-name1 nil 'trash)
	   ;; tramp-rclone.el and tramp-sshfs.el call the local
	   ;; `delete-directory'.  This raises another error.
	   :type (if (tramp--test-fuse-p) 'error 'file-error))
	  (delete-directory tmp-name1 'recursive 'trash)
	  (should-not (file-directory-p tmp-name1))
	  (should
	   (file-exists-p
	    (format
	     "%s/%s/bla" trash-directory (file-name-nondirectory tmp-name1))))
	  (should
	   (file-exists-p
	    (format
	     "%s/%s/%s/bla" trash-directory (file-name-nondirectory tmp-name1)
	     (file-name-nondirectory tmp-name2))))
	  (delete-directory trash-directory 'recursive)
	  (should-not (file-exists-p trash-directory))))

      ;; Setting `remote-file-name-inhibit-delete-by-moving-to-trash'
      ;; prevents trashing remote files.
      (let ((trash-directory (tramp--test-make-temp-name 'local quoted))
	    (delete-by-moving-to-trash t)
	    (remote-file-name-inhibit-delete-by-moving-to-trash t))
	(make-directory trash-directory)
	(make-directory tmp-name1)
	(should (file-directory-p tmp-name1))
	(delete-directory tmp-name1 nil 'trash)
	(should-not (file-exists-p tmp-name1))
	(should-not
	 (file-exists-p
	  (expand-file-name (file-name-nondirectory tmp-name1) trash-directory)))
	(delete-directory trash-directory 'recursive)
	(should-not (file-exists-p trash-directory))))))

(ert-deftest tramp-test15-copy-directory ()
  "Check `copy-directory'."
  (skip-unless (tramp--test-enabled))
  (skip-unless (not (tramp--test-rclone-p)))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let* ((tmp-name1 (tramp--test-make-temp-name nil quoted))
	   (tmp-name2 (tramp--test-make-temp-name nil quoted))
	   (tmp-name3 (expand-file-name
		       (file-name-nondirectory tmp-name1) tmp-name2))
	   (tmp-name4 (expand-file-name "foo" tmp-name1))
	   (tmp-name5 (expand-file-name "foo" tmp-name2))
	   (tmp-name6 (expand-file-name "foo" tmp-name3))
	   (tmp-name7 (tramp--test-make-temp-name nil quoted)))

      ;; Copy complete directory.
      (unwind-protect
	  (progn
	    (should-error
	     (copy-directory tmp-name1 tmp-name2)
	     :type 'file-missing)
	    ;; Copy empty directory.
	    (make-directory tmp-name1)
	    (write-region "foo" nil tmp-name4)
	    (should (file-directory-p tmp-name1))
	    (should (file-exists-p tmp-name4))
	    (copy-directory tmp-name1 tmp-name2)
	    (should (file-directory-p tmp-name2))
	    (should (file-exists-p tmp-name5))
	    ;; Target directory does exist already.
	    (should-error
	     (copy-directory tmp-name1 tmp-name2)
	     :type 'file-already-exists)
	    (copy-directory tmp-name1 (file-name-as-directory tmp-name2))
	    (should (file-directory-p tmp-name3))
	    (should (file-exists-p tmp-name6)))

	;; Cleanup.
	(ignore-errors
	  (delete-directory tmp-name1 'recursive)
	  (delete-directory tmp-name2 'recursive)))

      ;; Copy directory contents.
      (unwind-protect
	  (progn
	    ;; Copy empty directory.
	    (make-directory tmp-name1)
	    (write-region "foo" nil tmp-name4)
	    (should (file-directory-p tmp-name1))
	    (should (file-exists-p tmp-name4))
	    (copy-directory tmp-name1 tmp-name2 nil 'parents 'contents)
	    (should (file-directory-p tmp-name2))
	    (should (file-exists-p tmp-name5))
	    ;; Target directory does exist already.
	    (delete-file tmp-name5)
	    (should-not (file-exists-p tmp-name5))
	    (copy-directory
	     tmp-name1 (file-name-as-directory tmp-name2)
	     nil 'parents 'contents)
	    (should (file-directory-p tmp-name2))
	    (should (file-exists-p tmp-name5))
	    (should-not (file-directory-p tmp-name3))
	    (should-not (file-exists-p tmp-name6)))

	;; Cleanup.
	(ignore-errors
	  (delete-directory tmp-name1 'recursive)
	  (delete-directory tmp-name2 'recursive)))

      ;; Copy symlink to directory.
      (dolist (copy-directory-create-symlink '(nil t))
	(unwind-protect
	    (tramp--test-ignore-make-symbolic-link-error
	     ;; Copy to file name.
	     (make-directory tmp-name1)
	     (write-region "foo" nil tmp-name4)
	     (make-symbolic-link tmp-name1 tmp-name7)
	     (should (file-directory-p tmp-name1))
	     (should (file-exists-p tmp-name4))
	     (should (file-symlink-p tmp-name7))
	     (copy-directory tmp-name7 tmp-name2)
	     (if copy-directory-create-symlink
		 (should
		  (string-equal
		   (file-symlink-p tmp-name2) (file-symlink-p tmp-name7)))
	       (should (file-directory-p tmp-name2)))
	     ;; Copy to directory name.
	     (delete-directory tmp-name2 'recursive)
	     (make-directory tmp-name2)
	     (should (file-directory-p tmp-name2))
	     (copy-directory tmp-name7 (file-name-as-directory tmp-name2))
	     (if copy-directory-create-symlink
		 (should
		  (string-equal
		   (file-symlink-p
		    (expand-file-name
		     (file-name-nondirectory tmp-name7) tmp-name2))
		   (file-symlink-p tmp-name7)))
	       (should
		(file-directory-p
		 (expand-file-name
		  (file-name-nondirectory tmp-name7) tmp-name2)))))

	  ;; Cleanup.
	  (ignore-errors
	    (delete-directory tmp-name1 'recursive)
	    (delete-directory tmp-name2 'recursive)
	    (delete-directory tmp-name7 'recursive)))))))

(ert-deftest tramp-test16-directory-files ()
  "Check `directory-files'."
  (skip-unless (tramp--test-enabled))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let* ((tramp-fuse-remove-hidden-files t)
	   (tmp-name1 (tramp--test-make-temp-name nil quoted))
	   (tmp-name2 (expand-file-name "bla" tmp-name1))
	   (tmp-name3 (expand-file-name "foo" tmp-name1)))
      (unwind-protect
	  (progn
	    (should-error
	     (directory-files tmp-name1)
	     :type 'file-missing)
	    (make-directory tmp-name1)
	    (write-region "foo" nil tmp-name2)
	    (write-region "bla" nil tmp-name3)
	    (should (file-directory-p tmp-name1))
	    (should (file-exists-p tmp-name2))
	    (should (file-exists-p tmp-name3))
	    (should (equal (directory-files tmp-name1) '("." ".." "bla" "foo")))
	    (should (equal (directory-files tmp-name1 'full)
			   `(,(concat tmp-name1 "/.")
			     ,(concat tmp-name1 "/..")
			     ,tmp-name2 ,tmp-name3)))
	    (should (equal (directory-files
			    tmp-name1 nil directory-files-no-dot-files-regexp)
			   '("bla" "foo")))
	    (should (equal (directory-files
			    tmp-name1 'full directory-files-no-dot-files-regexp)
			   `(,tmp-name2 ,tmp-name3)))
	    ;; Check the COUNT arg.
	    (should
	     (equal
	      (directory-files
	       tmp-name1 nil directory-files-no-dot-files-regexp nil 1)
	      '("bla"))))

	;; Cleanup.
	(ignore-errors (delete-directory tmp-name1 'recursive))))))

;; This is not a file name handler test.  But Tramp needed to apply an
;; advice for older Emacs versions, so we check that this has been fixed.
(ert-deftest tramp-test16-file-expand-wildcards ()
  "Check `file-expand-wildcards'."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-enabled))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let* ((tramp-fuse-remove-hidden-files t)
	   (tmp-name1 (tramp--test-make-temp-name nil quoted))
	   (tmp-name2 (expand-file-name "foo" tmp-name1))
	   (tmp-name3 (expand-file-name "bar" tmp-name1))
	   (tmp-name4 (expand-file-name "baz" tmp-name1))
	   (default-directory tmp-name1))
      (unwind-protect
	  (progn
	    (make-directory tmp-name1)
	    (write-region "foo" nil tmp-name2)
	    (write-region "bar" nil tmp-name3)
	    (write-region "baz" nil tmp-name4)
	    (should (file-directory-p tmp-name1))
	    (should (file-exists-p tmp-name2))
	    (should (file-exists-p tmp-name3))
	    (should (file-exists-p tmp-name4))

	    ;; `sort' works destructive.
	    (should
	     (equal (file-expand-wildcards "*")
		    (sort (copy-sequence '("foo" "bar" "baz")) #'string-lessp)))
	    (should
	     (equal (file-expand-wildcards "ba?")
		    (sort (copy-sequence '("bar" "baz")) #'string-lessp)))
	    (should
	     (equal (file-expand-wildcards "ba[rz]")
		    (sort (copy-sequence '("bar" "baz")) #'string-lessp)))

	    (should
	     (equal
	      (file-expand-wildcards "*" 'full)
	      (sort
	       (copy-sequence `(,tmp-name2 ,tmp-name3 ,tmp-name4))
	       #'string-lessp)))
	    (should
	     (equal
	      (file-expand-wildcards "ba?" 'full)
	      (sort (copy-sequence `(,tmp-name3 ,tmp-name4)) #'string-lessp)))
	    (should
	     (equal
	      (file-expand-wildcards "ba[rz]" 'full)
	      (sort (copy-sequence `(,tmp-name3 ,tmp-name4)) #'string-lessp)))

	    (should
	     (equal
	      (file-expand-wildcards (concat tmp-name1 "/" "*"))
	      (sort
	       (copy-sequence `(,tmp-name2 ,tmp-name3 ,tmp-name4))
	       #'string-lessp)))
	    (should
	     (equal
	      (file-expand-wildcards (concat tmp-name1 "/" "ba?"))
	      (sort (copy-sequence `(,tmp-name3 ,tmp-name4)) #'string-lessp)))
	    (should
	     (equal
	      (file-expand-wildcards (concat tmp-name1 "/" "ba[rz]"))
	      (sort (copy-sequence `(,tmp-name3 ,tmp-name4)) #'string-lessp))))

	;; Cleanup.
	(ignore-errors (delete-directory tmp-name1 'recursive))))))

(ert-deftest tramp-test17-insert-directory ()
  "Check `insert-directory'."
  (skip-unless (tramp--test-enabled))
  ;; Ange-FTP is very special.  It does not include the header line
  ;; (this is performed by `dired').  If FULL is nil, it shows just
  ;; one file.  So we refrain from testing.
  (skip-unless (not (tramp--test-ange-ftp-p)))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let* ((tmp-name1
            (expand-file-name (tramp--test-make-temp-name nil quoted)))
	   (tmp-name2 (expand-file-name "foo" tmp-name1))
	   ;; We test for the summary line.  Keyword "total" could be localized.
	   (process-environment
	    (seq-union '("LANG=C" "LANGUAGE=C" "LC_ALL=C") process-environment)))
      (unwind-protect
	  (progn
	    (make-directory tmp-name1)
	    (write-region "foo" nil tmp-name2)
	    (should (file-directory-p tmp-name1))
	    (should (file-exists-p tmp-name2))
	    (with-temp-buffer
	      (insert-directory tmp-name1 nil)
	      (goto-char (point-min))
	      (should (looking-at-p (rx (literal tmp-name1)))))
	    (with-temp-buffer
	      (insert-directory (file-name-as-directory tmp-name1) nil)
	      (goto-char (point-min))
	      (should
               (looking-at-p (rx (literal (file-name-as-directory tmp-name1))))))
	    (with-temp-buffer
	      (insert-directory tmp-name1 "-al")
	      (goto-char (point-min))
	      (should
	       (looking-at-p (rx bol (+ nonl) blank (literal tmp-name1) eol))))
	    (with-temp-buffer
	      (insert-directory (file-name-as-directory tmp-name1) "-al")
	      (goto-char (point-min))
	      (should
	       (looking-at-p
		(rx bol (+ nonl) blank (literal tmp-name1) "/" eol))))
	    (with-temp-buffer
	      (insert-directory
	       (file-name-as-directory tmp-name1) "-al" nil 'full-directory-p)
	      (goto-char (point-min))
	      (should
	       (looking-at-p
		(rx-to-string
		 `(:
		   ;; There might be a summary line.
		   (? (* blank) "total" (+ nonl) (+ digit) (? blank)
		      (? (any "EGKMPTYZk")) (? "i") (? "B") "\n")
		   ;; We don't know in which order ".", ".." and "foo" appear.
		   (= ,(length (directory-files tmp-name1))
		      (+ nonl) blank
		      (| . ,(directory-files tmp-name1))
		      (? " ->" (+ nonl)) "\n"))))))

	    ;; Check error cases.
	    (when (and (tramp--test-supports-set-file-modes-p)
		       ;; With "sshfs", directories with zero file
		       ;; modes are still "accessible".
		       (not (tramp--test-sshfs-p))
		       ;; A directory is always accessible for user "root".
		       (not (zerop (file-attribute-user-id
				    (file-attributes tmp-name1)))))
	      (set-file-modes tmp-name1 0)
	      (with-temp-buffer
		(should-error
		 (insert-directory tmp-name1 nil)
		 :type 'file-error))
	      (set-file-modes tmp-name1 #o777))
	    (delete-directory tmp-name1 'recursive)
	    (with-temp-buffer
	      (should-error
	       (insert-directory tmp-name1 nil)
	       :type 'file-missing)))

	;; Cleanup.
	(ignore-errors (delete-directory tmp-name1 'recursive))))))

(ert-deftest tramp-test17-dired-with-wildcards ()
  "Check `dired' with wildcards."
  :tags '(:expensive-test)
  ;; `separate' syntax and IPv6 host name syntax do not work.
  (skip-unless
   (not (string-match-p (rx "[") ert-remote-temporary-file-directory)))
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-sh-p))
  (skip-unless (not (tramp--test-rsync-p)))
  ;; Wildcards are not supported in tramp-crypt.el.
  (skip-unless (not (tramp--test-crypt-p)))
  ;; Wildcards are not supported with "docker cp ..." or "podman cp ...".
  (skip-unless (not (tramp--test-container-oob-p)))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let* ((tmp-name1
	    (expand-file-name (tramp--test-make-temp-name nil quoted)))
	   (tmp-name2
            (expand-file-name (tramp--test-make-temp-name nil quoted)))
	   (tmp-name3 (expand-file-name "foo" tmp-name1))
	   (tmp-name4 (expand-file-name "bar" tmp-name2))
	   (ert-remote-temporary-file-directory
	    (funcall
	     (if quoted #'file-name-quote #'identity)
	     ert-remote-temporary-file-directory))
	   buffer)
      (unwind-protect
	  (progn
	    (make-directory tmp-name1)
	    (write-region "foo" nil tmp-name3)
	    (should (file-directory-p tmp-name1))
	    (should (file-exists-p tmp-name3))
	    (make-directory tmp-name2)
	    (write-region "foo" nil tmp-name4)
	    (should (file-directory-p tmp-name2))
	    (should (file-exists-p tmp-name4))

	    ;; Check for expanded directory names.
	    (with-current-buffer
		(setq buffer
		      (dired-noselect
		       (expand-file-name
			"tramp-test*" ert-remote-temporary-file-directory)))
	      (goto-char (point-min))
	      (should
	       (search-forward-regexp
		(rx
		 (literal
		  (file-relative-name
		   tmp-name1 ert-remote-temporary-file-directory)))))
	      (goto-char (point-min))
	      (should
	       (search-forward-regexp
		(rx
		 (literal
		  (file-relative-name
		   tmp-name2 ert-remote-temporary-file-directory))))))
	    (kill-buffer buffer)

	    ;; Check for expanded directory and file names.
	    (with-current-buffer
		(setq buffer
		      (dired-noselect
		       (expand-file-name
			"tramp-test*/*" ert-remote-temporary-file-directory)))
	      (goto-char (point-min))
	      (should
	       (search-forward-regexp
		(rx
		 (literal
		  (file-relative-name
		   tmp-name3 ert-remote-temporary-file-directory)))))
	      (goto-char (point-min))
	      (should
	       (search-forward-regexp
		(rx
		 (literal
		  (file-relative-name
		   tmp-name4
		   ert-remote-temporary-file-directory))))))
	    (kill-buffer buffer)

	    ;; Check for special characters.
	    (setq tmp-name3 (expand-file-name "*?" tmp-name1))
	    (setq tmp-name4 (expand-file-name "[a-z0-9]" tmp-name2))
	    (write-region "foo" nil tmp-name3)
	    (should (file-exists-p tmp-name3))
	    (write-region "foo" nil tmp-name4)
	    (should (file-exists-p tmp-name4))

	    (with-current-buffer
		(setq buffer
		      (dired-noselect
		       (expand-file-name
			"tramp-test*/*" ert-remote-temporary-file-directory)))
	      (goto-char (point-min))
	      (should
	       (search-forward-regexp
		(rx
		 (literal
		  (file-relative-name
		   tmp-name3 ert-remote-temporary-file-directory)))))
	      (goto-char (point-min))
	      (should
	       (search-forward-regexp
		(rx
		 (literal
		  (file-relative-name
		   tmp-name4
		   ert-remote-temporary-file-directory))))))
	    (kill-buffer buffer))

	;; Cleanup.
	(ignore-errors (kill-buffer buffer))
	(ignore-errors (delete-directory tmp-name1 'recursive))
	(ignore-errors (delete-directory tmp-name2 'recursive))))))

;; The following test is inspired by Bug#45691.
(ert-deftest tramp-test17-insert-directory-one-file ()
  "Check `insert-directory' inside directory listing."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-enabled))
  ;; Relative file names in dired are not supported in tramp-crypt.el.
  (skip-unless (not (tramp--test-crypt-p)))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let* ((tmp-name1
	    (expand-file-name (tramp--test-make-temp-name nil quoted)))
	   (tmp-name2 (expand-file-name "foo" tmp-name1))
	   (tmp-name3 (expand-file-name "bar" tmp-name1))
	   (dired-copy-preserve-time t)
	   (dired-recursive-copies 'top)
	   dired-copy-dereference
	   buffer)
      (unwind-protect
	  (progn
	    (make-directory tmp-name1)
	    (write-region "foo" nil tmp-name2)
	    (should (file-directory-p tmp-name1))
	    (should (file-exists-p tmp-name2))

	    ;; Check, that `insert-directory' works properly.
	    (with-current-buffer
		(setq buffer (dired-noselect tmp-name1 "--dired -al"))
	      (read-only-mode -1)
	      (goto-char (point-min))
	      (while (not (or (eobp)
			      (string-equal
			       (dired-get-filename 'no-dir 'no-error)
			       (file-name-nondirectory tmp-name2))))
		(forward-line 1))
	      (should-not (eobp))
	      (copy-file tmp-name2 tmp-name3)
	      (insert-directory
	       (file-name-nondirectory tmp-name3) "--dired -al -d")
	      ;; Point shall still be the recent file.
	      (should
	       (string-equal
		(dired-get-filename 'no-dir 'no-error)
		(file-name-nondirectory tmp-name2)))
	      (should-not (search-forward "dired" nil t))
	      ;; The copied file has been inserted the line before.
	      (forward-line -1)
	      (should
	       (string-equal
		(dired-get-filename 'no-dir 'no-error)
		(file-name-nondirectory tmp-name3))))
	    (kill-buffer buffer))

	;; Cleanup.
	(ignore-errors (kill-buffer buffer))
	(ignore-errors (delete-directory tmp-name1 'recursive))))))

(ert-deftest tramp-test18-file-attributes ()
  "Check `file-attributes'.
This tests also `access-file', `file-readable-p',
`file-regular-p' and `file-ownership-preserved-p'."
  (skip-unless (tramp--test-enabled))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    ;; We must use `file-truename' for the temporary directory,
    ;; because it could be located on a symlinked directory.  This
    ;; would let the test fail.
    (let* ((ert-remote-temporary-file-directory
	    (file-truename ert-remote-temporary-file-directory))
	   (tmp-name1 (tramp--test-make-temp-name nil quoted))
	   (tmp-name2 (tramp--test-make-temp-name nil quoted))
	   ;; File name with "//".
	   (tmp-name3
	    (replace-regexp-in-string "/" "//" (file-local-name tmp-name1)))
	   ;; `file-ownership-preserved-p' is implemented only in tramp-sh.el.
	   (test-file-ownership-preserved-p (tramp--test-sh-p))
	   attr)
      (unwind-protect
	  (progn
	    (write-region "foo" nil tmp-name1)
	    ;; `access-file' returns nil in case of success.
	    (should-not (access-file tmp-name1 "error"))
	    ;; `access-file' could use a timeout.
	    (let ((remote-file-name-access-timeout 1)
		  comp-warn-primitives)
	      (cl-letf (((symbol-function #'file-exists-p)
			 (lambda (_filename) (sleep-for 5))))
		(should-error
		 (access-file tmp-name1 "error")
		 :type 'file-error)))
	    (delete-file tmp-name1)

	    ;; A sticky bit could damage the `file-ownership-preserved-p' test.
	    (when
		(and test-file-ownership-preserved-p
		     (zerop (logand
			     #o1000
			     (file-modes ert-remote-temporary-file-directory))))
	      (write-region "foo" nil tmp-name1)
	      (setq test-file-ownership-preserved-p
		    (= (file-attribute-group-id (file-attributes tmp-name1))
		       (tramp-get-remote-gid tramp-test-vec 'integer)))
	      (delete-file tmp-name1))

	    (when (tramp--test-supports-set-file-modes-p)
	      (write-region "foo" nil tmp-name1)
	      ;; A file is always accessible for user "root".
	      (unless
		  (zerop (file-attribute-user-id (file-attributes tmp-name1)))
		(set-file-modes tmp-name1 0)
		(should-error
		 (access-file tmp-name1 "error")
		 :type tramp-permission-denied)
		(set-file-modes tmp-name1 #o777))
	      (delete-file tmp-name1))
	    (should-error
	     (access-file tmp-name1 "error")
	     :type 'file-missing)

	    (should-not (file-exists-p tmp-name1))
	    (should-not (file-readable-p tmp-name1))
	    (should-not (file-regular-p tmp-name1))
	    ;; `file-ownership-preserved-p' should return t for
	    ;; non-existing files.
	    (when test-file-ownership-preserved-p
	      (should (file-ownership-preserved-p tmp-name1 'group)))
	    (write-region "foo" nil tmp-name1)
	    (should (file-exists-p tmp-name1))
	    (should (file-readable-p tmp-name1))
	    (should (file-regular-p tmp-name1))
	    (should-not (access-file tmp-name1 "error"))
	    (when test-file-ownership-preserved-p
	      (should (file-ownership-preserved-p tmp-name1 'group)))

	    ;; We do not test inodes and device numbers.
	    (setq attr (file-attributes tmp-name1))
	    (should (consp attr))
	    (should (null (file-attribute-type attr)))
	    (should (numberp (file-attribute-link-number attr)))
	    (should (numberp (file-attribute-user-id attr)))
	    (should (numberp (file-attribute-group-id attr)))
	    (should
	     (stringp (current-time-string (file-attribute-access-time attr))))
	    (should
	     (stringp
	      (current-time-string (file-attribute-modification-time attr))))
	    (should
	     (stringp
	      (current-time-string (file-attribute-status-change-time attr))))
	    (should (numberp (file-attribute-size attr)))
	    (should (stringp (file-attribute-modes attr)))

	    (setq attr (file-attributes tmp-name1 'string))
	    (should (stringp (file-attribute-user-id attr)))
	    (should (stringp (file-attribute-group-id attr)))

	    ;; Symbolic links.
	    (tramp--test-ignore-make-symbolic-link-error
	      (should-error
	       (access-file tmp-name2 "error")
	       :type 'file-missing)
	      (when test-file-ownership-preserved-p
		(should (file-ownership-preserved-p tmp-name2 'group)))
	      (make-symbolic-link tmp-name1 tmp-name2)
	      (should (file-exists-p tmp-name2))
	      (should (file-symlink-p tmp-name2))
	      (should-not (access-file tmp-name2 "error"))
	      (when test-file-ownership-preserved-p
		(should (file-ownership-preserved-p tmp-name2 'group)))
	      (setq attr (file-attributes tmp-name2))
	      (should
	       (string-equal
		(funcall
		 (if quoted #'file-name-quote #'identity)
		 (file-attribute-type attr))
		(file-remote-p (file-truename tmp-name1) 'localname)))
	      (delete-file tmp-name2)

	      ;; A non-existent or cyclic link target makes the file
	      ;; inaccessible.
	      (dolist (target
		       `("does-not-exist" ,(file-name-nondirectory tmp-name2)))
		(make-symbolic-link target tmp-name2)
		(should (file-symlink-p tmp-name2))
		(should-not (file-exists-p tmp-name2))
		(should-not (file-directory-p tmp-name2))
		(should-error
		 (access-file tmp-name2 "error")
		 :type
		 (if (string-equal target "does-not-exist")
		     'file-missing 'file-error))
		;; `file-ownership-preserved-p' should return t for
		;; symlinked files to a non-existing or cyclic target.
		(when test-file-ownership-preserved-p
		  (should (file-ownership-preserved-p tmp-name2 'group)))
		(delete-file tmp-name2))

	      ;; Check, that "//" in symlinks are handled properly.
	      (make-symbolic-link tmp-name3 tmp-name2)
	      (should (file-symlink-p tmp-name2))
	      (setq attr (file-attributes tmp-name2))
	      (should (string-equal (file-attribute-type attr) tmp-name3))
	      (delete-file tmp-name2))

	    (when test-file-ownership-preserved-p
	      (should (file-ownership-preserved-p tmp-name1 'group)))
	    (delete-file tmp-name1)
	    (make-directory tmp-name1)
	    (should (file-exists-p tmp-name1))
	    (should (file-readable-p tmp-name1))
	    (should-not (file-regular-p tmp-name1))
	    (should-not (access-file tmp-name1 "error"))
	    (when test-file-ownership-preserved-p
	      (should (file-ownership-preserved-p tmp-name1 'group)))
	    (setq attr (file-attributes tmp-name1))
	    (should (eq (file-attribute-type attr) t))
	    ;; A trailing slash shouldn't harm.
	    (should
	     (equal (file-attributes tmp-name1)
		    (file-attributes (file-name-as-directory tmp-name1)))))

	;; Cleanup.
	(ignore-errors (delete-directory tmp-name1 'recursive))
	(ignore-errors (delete-file tmp-name1))
	(ignore-errors (delete-file tmp-name2))))))

(defun tramp--test-set-ert-test-documentation (test command)
  "Set the documentation string for a derived test.
The test is derived from TEST and COMMAND."
  (let ((test-doc
	 (split-string (ert-test-documentation (get test 'ert--test)) "\n")))
    ;; The first line must be extended.
    (setcar
     test-doc (format "%s  Use the \"%s\" command." (car test-doc) command))
    (setf (ert-test-documentation
	   (get (intern (format "%s-with-%s" test command)) 'ert--test))
	  (string-join test-doc "\n"))))

(defmacro tramp--test-deftest-with-stat (test)
  "Define ert `TEST-with-stat'."
  (declare (indent 1))
  `(ert-deftest ,(intern (concat (symbol-name test) "-with-stat")) ()
     :tags '(:expensive-test)
     (tramp--test-set-ert-test-documentation ',test "stat")
     (skip-unless (tramp--test-enabled))
     (skip-unless (tramp--test-sh-p))
     (skip-unless (tramp-get-remote-stat tramp-test-vec))
     (if-let* ((default-directory ert-remote-temporary-file-directory)
	       (ert-test (ert-get-test ',test))
	       (result (ert-test-most-recent-result ert-test))
	       (tramp-connection-properties
		(cons '(nil "perl" nil)
		      tramp-connection-properties)))
	 (progn
	   (skip-unless (< (ert-test-result-duration result) 300))
	   (funcall (ert-test-body ert-test)))
       (ert-skip (format "Test `%s' must run before" ',test)))))

(defmacro tramp--test-deftest-with-perl (test)
  "Define ert `TEST-with-perl'."
  (declare (indent 1))
  `(ert-deftest ,(intern (concat (symbol-name test) "-with-perl")) ()
     :tags '(:expensive-test)
     (tramp--test-set-ert-test-documentation ',test "perl")
     (skip-unless (tramp--test-enabled))
     (skip-unless (tramp--test-sh-p))
     (skip-unless (tramp-get-remote-perl tramp-test-vec))
     (if-let* ((default-directory ert-remote-temporary-file-directory)
	       (ert-test (ert-get-test ',test))
	       (result (ert-test-most-recent-result ert-test))
	       (tramp-connection-properties
		(append
		 '((nil "stat" nil)
		   ;; See `tramp-sh-handle-file-truename'.
		   (nil "readlink" nil)
		   ;; See `tramp-sh-handle-get-remote-*'.
		   (nil "id" nil))
		 tramp-connection-properties)))
	 (progn
	   (skip-unless (< (ert-test-result-duration result) 300))
	   (funcall (ert-test-body ert-test)))
       (ert-skip (format "Test `%s' must run before" ',test)))))

(defmacro tramp--test-deftest-with-ls (test)
  "Define ert `TEST-with-ls'."
  (declare (indent 1))
  `(ert-deftest ,(intern (concat (symbol-name test) "-with-ls")) ()
     :tags '(:expensive-test)
     (tramp--test-set-ert-test-documentation ',test "ls")
     (skip-unless (tramp--test-enabled))
     (skip-unless (tramp--test-sh-p))
     (if-let* ((default-directory ert-remote-temporary-file-directory)
	       (ert-test (ert-get-test ',test))
	       (result (ert-test-most-recent-result ert-test))
	       (tramp-connection-properties
		(append
		 '((nil "perl" nil)
		   (nil "stat" nil)
		   ;; See `tramp-sh-handle-file-truename'.
		   (nil "readlink" nil))
		 tramp-connection-properties)))
	 (progn
	   (skip-unless (< (ert-test-result-duration result) 300))
	   (funcall (ert-test-body ert-test)))
       (ert-skip (format "Test `%s' must run before" ',test)))))

(defmacro tramp--test-deftest-without-file-attributes (test)
  "Define ert `TEST-without-file-attributes'."
  (declare (indent 1))
  `(ert-deftest
       ,(intern (concat (symbol-name test) "-without-file-attributes")) ()
     :tags '(:expensive-test)
     (let ((test-doc
	    (split-string
	     (ert-test-documentation (get ',test 'ert--test)) "\n")))
       ;; The first line must be extended.
       (setcar
	test-doc
	(format "%s  Don't Use the \"file-attributes\" cache." (car test-doc)))
       (setf (ert-test-documentation
	      (get
	       (intern (format "%s-without-file-attributes" ',test))
	       'ert--test))
	     (string-join test-doc "\n")))
     (skip-unless (tramp--test-enabled))
     (skip-unless
      (or (tramp--test-adb-p) (tramp--test-sh-p) (tramp--test-sudoedit-p)))
     (if-let* ((default-directory ert-remote-temporary-file-directory)
	       (ert-test (ert-get-test ',test))
	       (result (ert-test-most-recent-result ert-test)))
	 (progn
	   (skip-unless (< (ert-test-result-duration result) 300))
	   (let (tramp-use-file-attributes)
	     (funcall (ert-test-body ert-test))))
       (ert-skip (format "Test `%s' must run before" ',test)))))

(tramp--test-deftest-with-stat tramp-test18-file-attributes)

(tramp--test-deftest-with-perl tramp-test18-file-attributes)

(tramp--test-deftest-with-ls tramp-test18-file-attributes)

(tramp--test-deftest-without-file-attributes tramp-test18-file-attributes)

(defvar tramp--test-start-time nil
  "Keep the start time of the current test, a float number.")

(defsubst tramp--test-file-attributes-equal-p (attr1 attr2)
  "Check, whether file attributes ATTR1 and ATTR2 are equal.
They might differ only in time attributes or directory size."
  (let ((attr1 (copy-sequence attr1))
	(attr2 (copy-sequence attr2))
	(start-time (- tramp--test-start-time 10)))
    ;; Link number.  For directories, it includes the number of
    ;; subdirectories.  Set it to 1.
    (when (eq (file-attribute-type attr1) t)
      (setcar (nthcdr 1 attr1) 1))
    (when (eq (file-attribute-type attr2) t)
      (setcar (nthcdr 1 attr2) 1))
    ;; Access time.
    (setcar (nthcdr 4 attr1) tramp-time-dont-know)
    (setcar (nthcdr 4 attr2) tramp-time-dont-know)
    ;; Modification time.  If any of the time values is "don't know",
    ;; we cannot compare, and we normalize the time stamps.  If the
    ;; time value is newer than the test start time, normalize it,
    ;; because due to caching the time stamps could differ slightly (a
    ;; few seconds).  We use a test start time minus 10 seconds, in
    ;; order to compensate a possible timestamp resolution higher than
    ;; a second on the remote machine.
    (when (or (time-equal-p
	       (file-attribute-modification-time attr1) tramp-time-dont-know)
	      (time-equal-p
	       (file-attribute-modification-time attr2) tramp-time-dont-know))
      (setcar (nthcdr 5 attr1) tramp-time-dont-know)
      (setcar (nthcdr 5 attr2) tramp-time-dont-know))
    (when (< start-time
	     (float-time (file-attribute-modification-time attr1)))
      (setcar (nthcdr 5 attr1) tramp-time-dont-know))
    (when (< start-time
	     (float-time (file-attribute-modification-time attr2)))
      (setcar (nthcdr 5 attr2) tramp-time-dont-know))
    ;; Status change time.  Ditto.
    (when (or (time-equal-p
	       (file-attribute-status-change-time attr1) tramp-time-dont-know)
	      (time-equal-p
	       (file-attribute-status-change-time attr2) tramp-time-dont-know))
      (setcar (nthcdr 6 attr1) tramp-time-dont-know)
      (setcar (nthcdr 6 attr2) tramp-time-dont-know))
    (when (< start-time (float-time (file-attribute-status-change-time attr1)))
      (setcar (nthcdr 6 attr1) tramp-time-dont-know))
    (when (< start-time (float-time (file-attribute-status-change-time attr2)))
      (setcar (nthcdr 6 attr2) tramp-time-dont-know))
    ;; Size.  Set it to 0 for directories, because it might have
    ;; changed.  For example the upper directory "../".
    (when (eq (file-attribute-type attr1) t)
      (setcar (nthcdr 7 attr1) 0))
    (when (eq (file-attribute-type attr2) t)
      (setcar (nthcdr 7 attr2) 0))
    ;; The check.
    (unless (equal attr1 attr2) (tramp--test-message "%S\n%S" attr1 attr2))
    (equal attr1 attr2)))

;; This isn't 100% correct, but better than no explainer at all.
(put #'tramp--test-file-attributes-equal-p 'ert-explainer #'ert--explain-equal)

(ert-deftest tramp-test19-directory-files-and-attributes ()
  "Check `directory-files-and-attributes'."
  (skip-unless (tramp--test-enabled))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    ;; `directory-files-and-attributes' contains also values for
    ;; "../".  Ensure that this doesn't change during tests, for
    ;; example due to handling temporary files.
    (let* ((tmp-name1 (tramp--test-make-temp-name nil quoted))
	   (tmp-name2 (expand-file-name "bla" tmp-name1))
	   attr)
      (unwind-protect
	  (progn
	    (should-error
	     (directory-files-and-attributes tmp-name1)
	     :type 'file-missing)
	    (make-directory tmp-name1)
	    (should (file-directory-p tmp-name1))
	    (setq tramp--test-start-time
		  (float-time
		   (file-attribute-modification-time
		    (file-attributes tmp-name1))))
	    (make-directory tmp-name2)
	    (should (file-directory-p tmp-name2))
	    (write-region "foo" nil (expand-file-name "foo" tmp-name2))
	    (write-region "bar" nil (expand-file-name "bar" tmp-name2))
	    (write-region "boz" nil (expand-file-name "boz" tmp-name2))

	    (setq attr (directory-files-and-attributes tmp-name2))
	    (should (consp attr))
	    (dolist (elt attr)
	      (should
	       (tramp--test-file-attributes-equal-p
		(file-attributes (expand-file-name (car elt) tmp-name2))
		(cdr elt))))

	    (setq attr (directory-files-and-attributes tmp-name2 'full))
	    (should (consp attr))
	    (dolist (elt attr)
	      (should
	       (tramp--test-file-attributes-equal-p
		(file-attributes (car elt)) (cdr elt))))

	    (setq attr (directory-files-and-attributes
			tmp-name2 nil (rx bos "b")))
	    (should (equal (mapcar #'car attr) '("bar" "boz")))

	    ;; Check the COUNT arg.
	    (setq attr (directory-files-and-attributes
			tmp-name2 nil (rx bos "b") nil nil 1))
	    (should (equal (mapcar #'car attr) '("bar")))

	    ;; A trailing slash shouldn't harm.
	    (should
	     (equal (file-attributes tmp-name2)
		    (file-attributes (file-name-as-directory tmp-name2)))))

	;; Cleanup.
	(ignore-errors (delete-directory tmp-name1 'recursive))))))

(tramp--test-deftest-with-stat tramp-test19-directory-files-and-attributes)

(tramp--test-deftest-with-perl tramp-test19-directory-files-and-attributes)

(tramp--test-deftest-with-ls tramp-test19-directory-files-and-attributes)

(tramp--test-deftest-without-file-attributes
 tramp-test19-directory-files-and-attributes)

(ert-deftest tramp-test20-file-modes ()
  "Check `file-modes'.
This tests also `file-executable-p', `file-writable-p' and `set-file-modes'."
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-supports-set-file-modes-p))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let ((tmp-name1 (tramp--test-make-temp-name nil quoted))
	  (tmp-name2 (tramp--test-make-temp-name nil quoted)))

      (unwind-protect
	  (progn
	    ;; Setting the mode for not existing files shall fail.
	    (should-error
	     (set-file-modes tmp-name1 #o000)
	     :type 'file-missing)
	    (write-region "foo" nil tmp-name1)
	    (should (file-exists-p tmp-name1))
	    (set-file-modes tmp-name1 #o777)
	    (should (= (file-modes tmp-name1) #o777))
	    (should (file-executable-p tmp-name1))
	    (should (file-writable-p tmp-name1))
	    (set-file-modes tmp-name1 #o444)
	    (should (= (file-modes tmp-name1) #o444))
	    (should-not (file-executable-p tmp-name1))
	    ;; A file is always writable for user "root".
	    (unless
		(or (zerop (file-attribute-user-id (file-attributes tmp-name1)))
		    (tramp--test-sshfs-p))
	      (should-not (file-writable-p tmp-name1)))
	    ;; Check the NOFOLLOW arg.  For regular files, there
	    ;; shouldn't be a difference.
	    (set-file-modes tmp-name1 #o222 'nofollow)
	    (should (= (file-modes tmp-name1 'nofollow) #o222))
	    ;; Setting the mode for not existing files shall fail.
	    (should-error
	     (set-file-modes tmp-name2 #o777)
	     :type 'file-missing))

	;; Cleanup.
	(ignore-errors (delete-file tmp-name1)))

      ;; Check the NOFOLLOW arg.  It is implemented for tramp-gvfs.el
      ;; and tramp-sh.el.  However, tramp-gvfs,el does not support
      ;; creating symbolic links.  And in tramp-sh.el, we must ensure
      ;; that the remote chmod command supports the "-h" argument.
      (when (and (tramp--test-sh-p) (tramp-get-remote-chmod-h tramp-test-vec))
	(unwind-protect
	    (progn
	      (write-region "foo" nil tmp-name1)
	      (should (file-exists-p tmp-name1))
	      (make-symbolic-link tmp-name1 tmp-name2)
	      (should
	       (string-equal
		(funcall
		 (if quoted #'file-name-unquote #'identity)
		 (file-remote-p tmp-name1 'localname))
		(file-symlink-p tmp-name2)))
	      ;; Both report the modes of `tmp-name1'.
	      (should
	       (= (file-modes tmp-name1) (file-modes tmp-name2)))
	      ;; `tmp-name1' is a regular file.  NOFOLLOW doesn't matter.
	      (should
	       (= (file-modes tmp-name1) (file-modes tmp-name1 'nofollow)))
	      ;; `tmp-name2' is a symbolic link.  It has different permissions.
	      (should-not
	       (= (file-modes tmp-name2) (file-modes tmp-name2 'nofollow)))
	      (should-not
	       (= (file-modes tmp-name1 'nofollow)
		  (file-modes tmp-name2 'nofollow)))
	      ;; Change permissions.
	      (set-file-modes tmp-name1 #o200)
	      (set-file-modes tmp-name2 #o200)
	      (should
	       (= (file-modes tmp-name1) (file-modes tmp-name2) #o200))
	      ;; Change permissions with NOFOLLOW.
	      (set-file-modes tmp-name1 #o300 'nofollow)
	      (set-file-modes tmp-name2 #o300 'nofollow)
	      (should
	       (= (file-modes tmp-name1 'nofollow)
		  (file-modes tmp-name2 'nofollow)))
	      (should-not (= (file-modes tmp-name1) (file-modes tmp-name2))))

	  ;; Cleanup.
	  (ignore-errors (delete-file tmp-name1))
	  (ignore-errors (delete-file tmp-name2)))))))

(tramp--test-deftest-without-file-attributes tramp-test20-file-modes)

;; Method "smb" could run into "NT_STATUS_REVISION_MISMATCH" error.
(defmacro tramp--test-ignore-add-name-to-file-error (&rest body)
  "Run BODY, ignoring \"error with add-name-to-file\" file error."
  (declare (indent defun) (debug (body)))
  `(condition-case err
       (progn ,@body)
     (file-error
      (unless (string-prefix-p "error with add-name-to-file"
			       (error-message-string err))
	(signal (car err) (cdr err))))))

(ert-deftest tramp-test21-file-links ()
  "Check `file-symlink-p'.
This tests also `make-symbolic-link', `file-truename' and `add-name-to-file'."
  (skip-unless (tramp--test-enabled))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    ;; We must use `file-truename' for the temporary directory,
    ;; because it could be located on a symlinked directory.  This
    ;; would let the test fail.
    (let* ((ert-remote-temporary-file-directory
	    (file-truename ert-remote-temporary-file-directory))
	   (tmp-name1 (tramp--test-make-temp-name nil quoted))
	   (tmp-name2 (tramp--test-make-temp-name nil quoted))
	   (tmp-name3 (tramp--test-make-temp-name 'local quoted))
	   (tmp-name4 (tramp--test-make-temp-name nil quoted))
	   (tmp-name5
	    (expand-file-name (file-name-nondirectory tmp-name1) tmp-name4))
	   (tmp-name6 (tramp--test-make-temp-name nil quoted)))
      ;; Check `make-symbolic-link'.
      (unwind-protect
	  (tramp--test-ignore-make-symbolic-link-error
	    (write-region "foo" nil tmp-name1)
	    (should (file-exists-p tmp-name1))
	    (should (file-regular-p tmp-name1))
	    (make-symbolic-link tmp-name1 tmp-name2)
	    (should (file-exists-p tmp-name2))
	    (should (file-regular-p tmp-name2))
	    (should
	     (string-equal
	      (funcall
	       (if quoted #'file-name-unquote #'identity)
	       (file-remote-p tmp-name1 'localname))
	      (file-symlink-p tmp-name2)))
	    (when (tramp--test-expensive-test-p)
	      (should-error
	       (make-symbolic-link tmp-name1 tmp-name2)
	       :type 'file-already-exists))
	    (when (tramp--test-expensive-test-p)
	      ;; A number means interactive case.
	      (cl-letf (((symbol-function #'yes-or-no-p) #'ignore))
		(should-error
		 (make-symbolic-link tmp-name1 tmp-name2 0)
		 :type 'file-already-exists)))
	    (cl-letf (((symbol-function #'yes-or-no-p) #'always))
	      (make-symbolic-link tmp-name1 tmp-name2 0)
	      (should
	       (string-equal
		(funcall
		 (if quoted #'file-name-unquote #'identity)
		 (file-remote-p tmp-name1 'localname))
		(file-symlink-p tmp-name2))))
	    (make-symbolic-link tmp-name1 tmp-name2 'ok-if-already-exists)
	    (should
	     (string-equal
	      (funcall
	       (if quoted #'file-name-unquote #'identity)
	       (file-remote-p tmp-name1 'localname))
	      (file-symlink-p tmp-name2)))
	    ;; If we use the local part of `tmp-name1', it shall still work.
	    (make-symbolic-link
	     (file-remote-p tmp-name1 'localname)
	     tmp-name2 'ok-if-already-exists)
	    (should
	     (string-equal
	      (funcall
	       (if quoted #'file-name-unquote #'identity)
	       (file-remote-p tmp-name1 'localname))
	      (file-symlink-p tmp-name2)))
	    ;; `tmp-name3' is a local file name.  Therefore, the link
	    ;; target remains unchanged, even if quoted.
	    ;; `make-symbolic-link' might not be permitted on w32 systems.
	    (unless (tramp--test-windows-nt-p)
	      (make-symbolic-link tmp-name1 tmp-name3)
	      (should
	       (string-equal tmp-name1 (file-symlink-p tmp-name3))))
	    ;; Check directory as newname.
	    (make-directory tmp-name4)
	    (should (file-directory-p tmp-name4))
	    (should-not (file-regular-p tmp-name4))
	    (when (tramp--test-expensive-test-p)
	      (should-error
	       (make-symbolic-link tmp-name1 tmp-name4)
	       :type 'file-already-exists))
	    (make-symbolic-link tmp-name1 (file-name-as-directory tmp-name4))
	    (should
	     (string-equal
	      (funcall
	       (if quoted #'file-name-unquote #'identity)
	       (file-remote-p tmp-name1 'localname))
	      (file-symlink-p tmp-name5)))
	    ;; Check, that files in symlinked directories still work.
	    (make-symbolic-link tmp-name4 tmp-name6)
	    (should (file-symlink-p tmp-name6))
	    (should (file-directory-p tmp-name6))
	    (should-not (file-regular-p tmp-name6))
	    (write-region "foo" nil (expand-file-name "foo" tmp-name6))
	    (delete-file (expand-file-name "foo" tmp-name6))
	    (should-not (file-exists-p (expand-file-name "foo" tmp-name4)))
	    (should-not (file-exists-p (expand-file-name "foo" tmp-name6))))

	;; Cleanup.
	(ignore-errors (delete-file tmp-name1))
	(ignore-errors (delete-file tmp-name2))
	(ignore-errors (delete-file tmp-name3))
	(ignore-errors (delete-file tmp-name5))
	(ignore-errors (delete-file tmp-name6))
	(ignore-errors (delete-directory tmp-name4 'recursive)))

      ;; Check `add-name-to-file'.
      (unwind-protect
	  (when (tramp--test-expensive-test-p)
	    (tramp--test-ignore-add-name-to-file-error
	     (write-region "foo" nil tmp-name1)
	     (should (file-exists-p tmp-name1))
	     (add-name-to-file tmp-name1 tmp-name2)
	     (should (file-regular-p tmp-name2))
	     (should-error
	      (add-name-to-file tmp-name1 tmp-name2)
	      :type 'file-already-exists)
	     ;; A number means interactive case.
	     (cl-letf (((symbol-function #'yes-or-no-p) #'ignore))
	       (should-error
		(add-name-to-file tmp-name1 tmp-name2 0)
		:type 'file-already-exists))
	     (cl-letf (((symbol-function #'yes-or-no-p) #'always))
	       (add-name-to-file tmp-name1 tmp-name2 0)
	       (should (file-regular-p tmp-name2)))
	     (add-name-to-file tmp-name1 tmp-name2 'ok-if-already-exists)
	     (should-not (file-symlink-p tmp-name2))
	     (should (file-regular-p tmp-name2))
	     ;; `tmp-name3' is a local file name.
	     (should-error
	      (add-name-to-file tmp-name1 tmp-name3)
	      :type 'file-error)
	     ;; Check directory as newname.
	     (make-directory tmp-name4)
	     (should-error
	      (add-name-to-file tmp-name1 tmp-name4)
	      :type 'file-already-exists)
	     (add-name-to-file tmp-name1 (file-name-as-directory tmp-name4))
	     (should
	      (file-regular-p
	       (expand-file-name
		(file-name-nondirectory tmp-name1) tmp-name4)))))

	;; Cleanup.
	(ignore-errors
	  (delete-file tmp-name1)
	  (delete-file tmp-name2)
	  (delete-directory tmp-name4 'recursive)))

      ;; Check `file-truename'.
      (unwind-protect
	  (tramp--test-ignore-make-symbolic-link-error
	    (write-region "foo" nil tmp-name1)
	    (should (file-exists-p tmp-name1))
	    (should (file-regular-p tmp-name1))
	    (should (string-equal tmp-name1 (file-truename tmp-name1)))
	    (make-symbolic-link tmp-name1 tmp-name2)
	    (should (file-symlink-p tmp-name2))
	    (should (file-regular-p tmp-name2))
	    (should-not (string-equal tmp-name2 (file-truename tmp-name2)))
	    (should
	     (string-equal (file-truename tmp-name1) (file-truename tmp-name2)))
	    (should (file-equal-p tmp-name1 tmp-name2))
	    ;; Check relative symlink file name.
	    (delete-file tmp-name2)
	    (let ((default-directory ert-remote-temporary-file-directory))
	      (make-symbolic-link (file-name-nondirectory tmp-name1) tmp-name2))
	    (should (file-symlink-p tmp-name2))
	    (should (file-regular-p tmp-name2))
	    (should-not (string-equal tmp-name2 (file-truename tmp-name2)))
	    (should
	     (string-equal (file-truename tmp-name1) (file-truename tmp-name2)))
	    (should (file-equal-p tmp-name1 tmp-name2))
	    ;; Symbolic links could look like a remote file name.
	    ;; They must be quoted then.
	    (let ((penguin
		   (if (eq tramp-syntax 'separate)
		       "/[penguin/motd]" "/penguin:motd:")))
	      (delete-file tmp-name2)
	      (make-symbolic-link
	       (funcall (if quoted #'file-name-unquote #'identity) penguin)
	       tmp-name2)
	      (should (file-symlink-p tmp-name2))
	      (should-not (file-regular-p tmp-name2))
	      (should
	       (string-equal
		(file-truename tmp-name2)
		(file-name-quote (concat (file-remote-p tmp-name2) penguin)))))
	    ;; `tmp-name3' is a local file name.
	    ;; `make-symbolic-link' might not be permitted on w32 systems.
	    (unless (tramp--test-windows-nt-p)
	      (make-symbolic-link tmp-name1 tmp-name3)
	      (should (file-symlink-p tmp-name3))
	      (should-not (file-regular-p tmp-name3))
              (should-not (string-equal tmp-name3 (file-truename tmp-name3)))
	      ;; `file-truename' returns a quoted file name for `tmp-name3'.
	      ;; We must unquote it.
	      (should
	       (string-equal
		(file-truename tmp-name1)
		(file-name-unquote (file-truename tmp-name3))))))

	;; Cleanup.
	(ignore-errors
	  (delete-file tmp-name1)
	  (delete-file tmp-name2)
	  (delete-file tmp-name3)))

      ;; Symbolic links could be nested.
      (unwind-protect
	  (tramp--test-ignore-make-symbolic-link-error
	    (make-directory tmp-name1)
	    (should (file-directory-p tmp-name1))
	    (let* ((ert-remote-temporary-file-directory
		    (file-truename tmp-name1))
		   (tmp-name2 (tramp--test-make-temp-name nil quoted))
		   (tmp-name3 tmp-name2)
		   (number-nesting 15))
	      (dotimes (_ number-nesting)
		(make-symbolic-link
		 tmp-name3
		 (setq tmp-name3 (tramp--test-make-temp-name nil quoted))))
	      (should-not (file-regular-p tmp-name2))
	      (should-not (file-regular-p tmp-name3))
	      (should
	       (string-equal
		(file-truename tmp-name2)
		(file-truename tmp-name3)))
	      (when (tramp--test-expensive-test-p)
		(should-error
		 (with-temp-buffer (insert-file-contents tmp-name2))
		 :type 'file-missing))
	      (when (tramp--test-expensive-test-p)
		(should-error
		 (with-temp-buffer (insert-file-contents tmp-name3))
		 :type 'file-missing))
	      ;; `directory-files' does not show symlinks to
	      ;; non-existing targets in the "smb" case.  So we remove
	      ;; the symlinks manually.
	      (while (stringp (setq tmp-name2 (file-symlink-p tmp-name3)))
		(delete-file tmp-name3)
		(setq tmp-name3 (concat (file-remote-p tmp-name3) tmp-name2)))))

	;; Cleanup.
	(ignore-errors (delete-file tmp-name2))
	(ignore-errors (delete-file tmp-name3))
	(ignore-errors (delete-directory tmp-name1 'recursive)))

      ;; Detect cyclic symbolic links.
      (unwind-protect
	  (when (tramp--test-expensive-test-p)
	    (tramp--test-ignore-make-symbolic-link-error
	     (make-symbolic-link tmp-name2 tmp-name1)
	     (should (file-symlink-p tmp-name1))
	     (should-not (file-regular-p tmp-name1))
	     (should-not (file-regular-p tmp-name2))
	     (should
	      (string-equal
	       (file-truename tmp-name1)
	       (file-truename tmp-name2)))
	     (if (tramp--test-smb-p)
		 ;; The symlink command of "smbclient" detects the
		 ;; cycle already.
		 (should-error
		  (make-symbolic-link tmp-name1 tmp-name2)
		  :type 'file-error)
	       (make-symbolic-link tmp-name1 tmp-name2)
	       (should (file-symlink-p tmp-name1))
	       (should (file-symlink-p tmp-name2))
	       (should-not (file-regular-p tmp-name1))
	       (should-not (file-regular-p tmp-name2)))))

	;; Cleanup.
	(ignore-errors
	  (delete-file tmp-name1)
	  (delete-file tmp-name2)))

      ;; `file-truename' shall preserve trailing slash of directories.
      (let* ((dir1
	      (directory-file-name
	       (funcall
		(if quoted #'file-name-quote #'identity)
		ert-remote-temporary-file-directory)))
	     (dir2 (file-name-as-directory dir1)))
	(should (string-equal (file-truename dir1) (expand-file-name dir1)))
	(should (string-equal (file-truename dir2) (expand-file-name dir2)))))))

(tramp--test-deftest-without-file-attributes tramp-test21-file-links)

(ert-deftest tramp-test22-file-times ()
  "Check `set-file-times' and `file-newer-than-file-p'."
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-supports-set-file-times-p))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let ((tmp-name1 (tramp--test-make-temp-name nil quoted))
	  (tmp-name2 (tramp--test-make-temp-name nil quoted))
	  (tmp-name3 (tramp--test-make-temp-name nil quoted)))
      (unwind-protect
	  (progn
	    (write-region "foo" nil tmp-name1)
	    (should (file-exists-p tmp-name1))
	    (should (consp (file-attribute-modification-time
			    (file-attributes tmp-name1))))
	    ;; Skip the test, if the remote handler is not able to set
	    ;; the correct time.  Some remote machines cannot resolve
	    ;; seconds.  tramp-adb.el needs at least a day.
	    (skip-unless
	     (set-file-times tmp-name1 (seconds-to-time (* 24 60 60))))
	    ;; Dumb remote shells without perl(1) or stat(1) are not
	    ;; able to return the date correctly.  They say "don't know".
	    (unless (time-equal-p
		     (file-attribute-modification-time
		      (file-attributes tmp-name1))
		     tramp-time-dont-know)
	      (should
	       (time-equal-p
                (file-attribute-modification-time (file-attributes tmp-name1))
		(seconds-to-time (* 24 60 60))))
	      ;; Setting the time for not existing files shall fail.
	      (should-error
	       (set-file-times tmp-name2)
	       :type 'file-missing)
	      (write-region "bla" nil tmp-name2)
	      (should (file-exists-p tmp-name2))
	      (should (file-newer-than-file-p tmp-name2 tmp-name1))
	      ;; `tmp-name3' does not exist.
	      (should (file-newer-than-file-p tmp-name2 tmp-name3))
	      (should-not (file-newer-than-file-p tmp-name3 tmp-name1))
	      ;; Check the NOFOLLOW arg.  For regular files, there
	      ;; shouldn't be a difference.
	      (set-file-times tmp-name1 (seconds-to-time 60) 'nofollow)
	      (should
	       (time-equal-p
                (file-attribute-modification-time (file-attributes tmp-name1))
		(seconds-to-time 60)))))

	;; Cleanup.
	(ignore-errors
	  (delete-file tmp-name1)
	  (delete-file tmp-name2))))))

(ert-deftest tramp-test23-visited-file-modtime ()
  "Check `set-visited-file-modtime' and `verify-visited-file-modtime'."
  (skip-unless (tramp--test-enabled))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let ((tmp-name (tramp--test-make-temp-name nil quoted)))
      (unwind-protect
	  (progn
	    (write-region "foo" nil tmp-name)
	    (should (file-exists-p tmp-name))
	    (with-temp-buffer
	      (insert-file-contents tmp-name)
	      (should (verify-visited-file-modtime))
              (set-visited-file-modtime (seconds-to-time 1))
	      (should (verify-visited-file-modtime))
	      (should (= 1 (float-time (visited-file-modtime))))

	      ;; Checks with deleted file.
	      (delete-file tmp-name)
	      (dired-uncache tmp-name)
	      (should (verify-visited-file-modtime))
              (set-visited-file-modtime (seconds-to-time 1))
	      (should (verify-visited-file-modtime))
	      (should (= 1 (float-time (visited-file-modtime))))))

	;; Cleanup.
	(ignore-errors (delete-file tmp-name))))))

;; This test is inspired by Bug#29149.
(ert-deftest tramp-test24-file-acl ()
  "Check that `file-acl' and `set-file-acl' work proper."
  (skip-unless (tramp--test-enabled))
  ;; The following test checks also whether `set-file-modes' will work.
  (skip-unless (file-acl ert-remote-temporary-file-directory))
  (skip-unless (not (tramp--test-crypt-p)))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let ((tmp-name1 (tramp--test-make-temp-name nil quoted))
	  (tmp-name2 (tramp--test-make-temp-name nil quoted))
	  (tmp-name3 (tramp--test-make-temp-name 'local quoted)))
      ;; Both files are remote.
      (unwind-protect
	  (progn
	    ;; Two files with same ACLs.
	    (write-region "foo" nil tmp-name1)
	    (should (file-exists-p tmp-name1))
	    (should (file-acl tmp-name1))
	    (copy-file tmp-name1 tmp-name2 nil nil nil 'preserve-permissions)
	    (should (file-acl tmp-name2))
	    (should (string-equal (file-acl tmp-name1) (file-acl tmp-name2)))
	    ;; Different permissions mean different ACLs.
	    (unless (tramp--test-windows-nt-or-smb-p)
	      (set-file-modes tmp-name1 #o777)
	      (set-file-modes tmp-name2 #o444)
	      (should-not
	       (string-equal (file-acl tmp-name1) (file-acl tmp-name2))))
	    ;; Copy ACL.  Not all remote handlers support it, so we test.
	    (when (set-file-acl tmp-name2 (file-acl tmp-name1))
	      (should (string-equal (file-acl tmp-name1) (file-acl tmp-name2))))
	    ;; An invalid ACL does not harm.
	    (should-not (set-file-acl tmp-name2 "foo")))

	;; Cleanup.
	(ignore-errors (delete-file tmp-name1))
	(ignore-errors (delete-file tmp-name2)))

      ;; Remote and local file.
      (unwind-protect
	  (when (and (file-acl temporary-file-directory)
		     (not (tramp--test-windows-nt-or-smb-p)))
	    ;; Two files with same ACLs.
	    (write-region "foo" nil tmp-name1)
	    (should (file-exists-p tmp-name1))
	    (should (file-acl tmp-name1))
	    (copy-file tmp-name1 tmp-name3 nil nil nil 'preserve-permissions)
	    (should (file-acl tmp-name3))
	    (should (string-equal (file-acl tmp-name1) (file-acl tmp-name3)))
	    ;; Different permissions mean different ACLs.
	    (set-file-modes tmp-name1 #o777)
	    (set-file-modes tmp-name3 #o444)
	    (should-not
	     (string-equal (file-acl tmp-name1) (file-acl tmp-name3)))
	    ;; Copy ACL.  Since we don't know whether Emacs is built
	    ;; with local ACL support, we must check it.
	    (when (set-file-acl tmp-name3 (file-acl tmp-name1))
	      (should (string-equal (file-acl tmp-name1) (file-acl tmp-name3))))

	    ;; Two files with same ACLs.
	    (delete-file tmp-name1)
	    (copy-file tmp-name3 tmp-name1 nil nil nil 'preserve-permissions)
	    (should (file-acl tmp-name1))
	    (should (string-equal (file-acl tmp-name1) (file-acl tmp-name3)))
	    ;; Different permissions mean different ACLs.
	    (set-file-modes tmp-name1 #o777)
	    (set-file-modes tmp-name3 #o444)
	    (should-not
	     (string-equal (file-acl tmp-name1) (file-acl tmp-name3)))
	    ;; Copy ACL.
	    (set-file-acl tmp-name1 (file-acl tmp-name3))
	    (should (string-equal (file-acl tmp-name1) (file-acl tmp-name3))))

	;; Cleanup.
	(ignore-errors (delete-file tmp-name1))
	(ignore-errors (delete-file tmp-name3))))))

(ert-deftest tramp-test25-file-selinux ()
  "Check `file-selinux-context' and `set-file-selinux-context'."
  (skip-unless (tramp--test-enabled))
  (skip-unless
   (not (equal (file-selinux-context ert-remote-temporary-file-directory)
	       '(nil nil nil nil))))
  (skip-unless (not (tramp--test-crypt-p)))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let ((tmp-name1 (tramp--test-make-temp-name nil quoted))
	  (tmp-name2 (tramp--test-make-temp-name nil quoted))
	  (tmp-name3 (tramp--test-make-temp-name 'local quoted)))
      ;; Both files are remote.
      (unwind-protect
	  (progn
	    ;; Two files with same SELinux context.
	    (write-region "foo" nil tmp-name1)
	    (should (file-exists-p tmp-name1))
	    (should (file-selinux-context tmp-name1))
	    (copy-file tmp-name1 tmp-name2 nil nil nil 'preserve-permissions)
	    (should (file-selinux-context tmp-name2))
	    (should
	     (equal
	      (file-selinux-context tmp-name1)
	      (file-selinux-context tmp-name2)))
	    ;; Check different SELinux context.  We cannot support
	    ;; different ranges in this test; let's assume the most
	    ;; likely one.
	    (let ((context (file-selinux-context tmp-name1)))
	      (when (and (string-equal (nth 3 context) "s0")
			 (setcar (nthcdr 3 context) "s0:c0")
			 (set-file-selinux-context tmp-name1 context))
		(should-not
		 (equal
		  (file-selinux-context tmp-name1)
		  (file-selinux-context tmp-name2)))))
	    ;; Copy SELinux context.
	    (should
	     (set-file-selinux-context
	      tmp-name2 (file-selinux-context tmp-name1)))
	    (should
	     (equal
	      (file-selinux-context tmp-name1)
	      (file-selinux-context tmp-name2)))
	    ;; An invalid SELinux context does not harm.
	    (should-not (set-file-selinux-context tmp-name2 "foo")))

	;; Cleanup.
	(ignore-errors (delete-file tmp-name1))
	(ignore-errors (delete-file tmp-name2)))

      ;; Remote and local file.
      (unwind-protect
	  (when (and (not
		      (or (equal (file-selinux-context temporary-file-directory)
				 '(nil nil nil nil))
			  (tramp--test-windows-nt-or-smb-p)))
		     ;; Both users shall use the same SELinux context.
		     (string-equal
		      (let ((default-directory temporary-file-directory))
			(shell-command-to-string "id -Z"))
		      (let ((default-directory
			      ert-remote-temporary-file-directory))
			(shell-command-to-string "id -Z"))))

	    ;; Two files with same SELinux context.
	    (write-region "foo" nil tmp-name1)
	    (should (file-exists-p tmp-name1))
	    (should (file-selinux-context tmp-name1))
	    (copy-file tmp-name1 tmp-name3)
	    (should (file-selinux-context tmp-name3))
	    ;; We cannot expect that copying over file system
	    ;; boundaries keeps SELinux context.  So we copy it
	    ;; explicitly.
	    (should
	     (set-file-selinux-context
	      tmp-name3 (file-selinux-context tmp-name1)))
	    (should
	     (equal
	      (file-selinux-context tmp-name1)
	      (file-selinux-context tmp-name3)))
	    ;; Check different SELinux context.  We cannot support
	    ;; different ranges in this test; let's assume the most
	    ;; likely one.
	    (let ((context (file-selinux-context tmp-name1)))
	      (when (and (string-equal (nth 3 context) "s0")
			 (setcar (nthcdr 3 context) "s0:c0")
			 (set-file-selinux-context tmp-name1 context))
		(should-not
		 (equal
		  (file-selinux-context tmp-name1)
		  (file-selinux-context tmp-name3)))))
	    ;; Copy SELinux context.
	    (should
	     (set-file-selinux-context
	      tmp-name3 (file-selinux-context tmp-name1)))
	    (should
	     (equal
	      (file-selinux-context tmp-name1)
	      (file-selinux-context tmp-name3)))

	    ;; Two files with same SELinux context.
	    (delete-file tmp-name1)
	    (copy-file tmp-name3 tmp-name1)
	    (should (file-selinux-context tmp-name1))
	    ;; We cannot expect that copying over file system
	    ;; boundaries keeps SELinux context.  So we copy it
	    ;; explicitly.
	    (should
	     (set-file-selinux-context
	      tmp-name1 (file-selinux-context tmp-name3)))
	    (should
	     (equal
	      (file-selinux-context tmp-name1)
	      (file-selinux-context tmp-name3)))
	    ;; Check different SELinux context.  We cannot support
	    ;; different ranges in this test; let's assume the most
	    ;; likely one.
	    (let ((context (file-selinux-context tmp-name3)))
	      (when (and (string-equal (nth 3 context) "s0")
			 (setcar (nthcdr 3 context) "s0:c0")
			 (set-file-selinux-context tmp-name3 context))
		(should-not
		 (equal
		  (file-selinux-context tmp-name1)
		  (file-selinux-context tmp-name3)))))
	    ;; Copy SELinux context.
	    (should
	     (set-file-selinux-context
	      tmp-name1 (file-selinux-context tmp-name3)))
	    (should
	     (equal
	      (file-selinux-context tmp-name1)
	      (file-selinux-context tmp-name3))))

	;; Cleanup.
	(ignore-errors (delete-file tmp-name1))
	(ignore-errors (delete-file tmp-name3))))))

(ert-deftest tramp-test26-file-name-completion ()
  "Check `file-name-completion' and `file-name-all-completions'."
  (skip-unless (tramp--test-enabled))

  ;; Method and host name in completion mode.
  (let ((tramp-fuse-remove-hidden-files t)
	(method (file-remote-p ert-remote-temporary-file-directory 'method))
	(host (file-remote-p ert-remote-temporary-file-directory 'host))
        (orig-syntax tramp-syntax)
        (minibuffer-completing-file-name t))
    ;; `file-remote-p' returns as host the string "host#port", which
    ;; isn't useful.
    (when (and (stringp host)
	       (string-match
		(rx (regexp tramp-prefix-port-regexp) (regexp tramp-port-regexp))
		host))
      (setq host (replace-match "" nil nil host)))

    (unwind-protect
        (dolist (syntax (if (tramp--test-expensive-test-p)
		            (tramp-syntax-values) `(,orig-syntax)))
          (tramp-change-syntax syntax)
	  ;; This has cleaned up all connection data, which are used
	  ;; for completion.  We must refill the cache in order to get
	  ;; at least one completion candidate.
	  (tramp-set-connection-property tramp-test-vec "completion-use-cache" t)

          (let (;; This is needed for the `separate' syntax.
                (prefix-format (substring tramp-prefix-format 1))
		;; This is needed for the IPv6 host name syntax.
		(ipv6-prefix
		 (and (string-match-p tramp-ipv6-regexp host)
		      tramp-prefix-ipv6-format))
		(ipv6-postfix
		 (and (string-match-p tramp-ipv6-regexp host)
		      tramp-postfix-ipv6-format)))
            ;; Complete method name.
	    (unless (or (tramp-string-empty-or-nil-p method)
                        (string-empty-p tramp-method-regexp))
	      ;; `read-directory-name' uses `file-directory-p'.
	      ;; `file-directory-p' works since Emacs 31.
	      ;; (Bug#79236)
	      (when (tramp--test-emacs31-p)
		(should
		 (file-name-completion
		  (concat prefix-format (substring method 0 1))
		  "/" #'file-directory-p)))
	      (should
	       (member
		(concat prefix-format method tramp-postfix-method-format)
		(file-name-all-completions
                 (concat prefix-format (substring method 0 1)) "/"))))
            ;; Complete host name.
	    (unless (or (tramp-string-empty-or-nil-p method)
                        (string-empty-p tramp-method-regexp)
                        (tramp-string-empty-or-nil-p host))
	      ;; `read-directory-name' uses `file-directory-p'.
	      ;; `file-directory-p' works since Emacs 31.
	      ;; (Bug#79236)
	      (when (tramp--test-emacs31-p)
		(should
		 (file-name-completion
		  (concat prefix-format method tramp-postfix-method-format)
		  "/" #'file-directory-p)))
	      (should
	       (member
		(concat
                 prefix-format method tramp-postfix-method-format
		 ipv6-prefix host ipv6-postfix tramp-postfix-host-format)
		(file-name-all-completions
		 (concat prefix-format method tramp-postfix-method-format)
                 "/"))))))

      ;; Cleanup.
      (tramp-change-syntax orig-syntax)))

  (dolist (non-essential '(nil t))
    (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
      (ert-with-temp-directory tmp-name
	:prefix (tramp--test-make-temp-prefix nil quoted) :suffix ""
	(let ((tramp-fuse-remove-hidden-files t))
	      ;; Local files.
	      (write-region "foo" nil (expand-file-name "foo" tmp-name))
	      (should (file-exists-p (expand-file-name "foo" tmp-name)))
	      (write-region "bar" nil (expand-file-name "bold" tmp-name))
	      (should (file-exists-p (expand-file-name "bold" tmp-name)))
	      (make-directory (expand-file-name "boz" tmp-name))
	      (should (file-directory-p (expand-file-name "boz" tmp-name)))
	      (should (equal (file-name-completion "fo" tmp-name) "foo"))
	      (should (equal (file-name-completion "foo" tmp-name) t))
	      (should (equal (file-name-completion "b" tmp-name) "bo"))
	      (should-not (file-name-completion "a" tmp-name))
	      ;; `file-name-completion' should not err out if
	      ;; directory does not exist.  (Bug#61890)
	      ;; Ange-FTP does not support this.
	      (unless (tramp--test-ange-ftp-p)
		(should-not
		 (file-name-completion "a" (file-name-concat tmp-name "fuzz"))))
	      ;; Ange-FTP does not support predicates.
	      (unless (tramp--test-ange-ftp-p)
		(should
		 (equal
		  (file-name-completion "b" tmp-name #'file-directory-p)
		  "boz/")))
	      (should
	       (equal (file-name-all-completions "fo" tmp-name) '("foo")))
	      (should
	       (equal
		(sort (file-name-all-completions "b" tmp-name) #'string-lessp)
		'("bold" "boz/")))
	      (should-not (file-name-all-completions "a" tmp-name))
	      ;; Symbolic links.
	      (tramp--test-ignore-make-symbolic-link-error
	       (make-symbolic-link
		(file-name-concat tmp-name "foo")
		(file-name-concat tmp-name "link1"))
	       (should (file-exists-p (expand-file-name "link1" tmp-name)))
	       (make-symbolic-link
		(file-name-concat tmp-name "boz")
		(file-name-concat tmp-name "link2"))
	       (should (file-exists-p (expand-file-name "link2" tmp-name)))
	       (should (equal (file-name-completion "li" tmp-name) "link"))
	       (should (member "link1" (file-name-all-completions "" tmp-name)))
	       (should (member "link2/" (file-name-all-completions "" tmp-name)))
	       (delete-file (file-name-concat tmp-name "link1"))
	       (delete-file (file-name-concat tmp-name "link2")))
	      ;; `completion-regexp-list' restricts the completion to
	      ;; files which match all expressions in this list.
	      ;; Ange-FTP does not complete "".
	      (unless (tramp--test-ange-ftp-p)
		(let ((completion-regexp-list
		       `(,directory-files-no-dot-files-regexp "b")))
		  (should
		   (equal (file-name-completion "" tmp-name) "bo"))
		  (should
		   (equal
		    (sort
		     (file-name-all-completions "" tmp-name) #'string-lessp)
		    '("bold" "boz/")))))
	      ;; `file-name-completion' ignores file names that end in
	      ;; any string in `completion-ignored-extensions'.
	      (let ((completion-ignored-extensions '(".ext")))
		(write-region "foo" nil (expand-file-name "foo.ext" tmp-name))
		(should (file-exists-p (expand-file-name "foo.ext" tmp-name)))
		(should (equal (file-name-completion "fo" tmp-name) "foo"))
		(should (equal (file-name-completion "foo" tmp-name) t))
		(should
		 (equal (file-name-completion "foo." tmp-name) "foo.ext"))
		(should (equal (file-name-completion "foo.ext" tmp-name) t))
		;; `file-name-all-completions' is not affected.
		(should
		 (equal
		  (sort (file-name-all-completions "" tmp-name) #'string-lessp)
		  '("../" "./" "bold" "boz/" "foo" "foo.ext")))))))))

(tramp--test-deftest-with-perl tramp-test26-file-name-completion)

(tramp--test-deftest-with-ls tramp-test26-file-name-completion)

;; This test is inspired by Bug#51386, Bug#52758, Bug#53513,
;; Bug#54042, Bug#60505 and Bug#79236.
(ert-deftest tramp-test26-interactive-file-name-completion ()
  "Check interactive completion with different `completion-styles'."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-enabled))

  ;; Method, user and host name in completion mode.
  (let* (;; Set this to `t' if you want to run all tests.
	 (expensive nil) ;(tramp--test-expensive-test-p))
	 ;; Set this to `t' if you want to see the traces.
	 (tramp-trace nil)
	 (method (file-remote-p ert-remote-temporary-file-directory 'method))
	 (user (file-remote-p ert-remote-temporary-file-directory 'user))
	 (host (file-remote-p ert-remote-temporary-file-directory 'host))
	 (hop (and expensive
		   (file-remote-p ert-remote-temporary-file-directory 'hop)))
	 ;; All multi-hop capable methods.
	 (method-list
	  (and hop (sort (mapcar
			  (lambda (x)
			    (substring x (length tramp-prefix-format)))
			  (tramp-get-completion-methods "" t)))))
         (orig-syntax tramp-syntax)
         (non-essential t)
	 (inhibit-message
	  (and (not tramp-trace) (not (ignore-errors (edebug-mode))))))
    ;; `file-remote-p' returns as host the string "host#port", which
    ;; isn't useful.
    (when (and (stringp host)
	       (string-match
		(rx (regexp tramp-prefix-port-regexp) (regexp tramp-port-regexp))
		host))
      (setq host (replace-match "" nil nil host)))

    (when tramp-trace
      (when (get-buffer trace-buffer) (kill-buffer trace-buffer))
      (dolist
	  (elt
	   (append
	    (apropos-internal (rx bos "tramp-") #'functionp)
	    '(completion-file-name-table read-file-name)))
	(unless (get elt 'tramp-suppress-trace)
	  (trace-function-background elt))))

    (unwind-protect
        (dolist (syntax (if (tramp--test-expensive-test-p)
		            (tramp-syntax-values) `(,orig-syntax)))
          (tramp-change-syntax syntax)
	  ;; This has cleaned up all connection data, which are used
	  ;; for completion.  We must refill the cache in order to get
	  ;; at least one completion candidate.
	  (tramp-set-connection-property tramp-test-vec "completion-use-cache" t)

          (dolist
              (style
               (if expensive
                   ;; `initials' uses "/" as separator, it doesn't apply here.
		   ;; `shorthand' is about symbols, it doesn't apply here.
                   `(emacs21 emacs22 basic partial-completion substring
		     ;; FIXME: `flex' is not compatible with IPv6 hosts.
		     ,@(unless (string-match-p tramp-ipv6-regexp host) '(flex))
		     ;; `orderless' is an ELPA package.
		     ;; What about `company' backends, `consult',
		     ;; `cider', `helm'?
		     orderless)
		 '(basic)))

	    (when (assoc style completion-styles-alist)
	      (let* (;; Force the real minibuffer in batch mode.
                     (executing-kbd-macro noninteractive)
		     (confirm-nonexistent-file-or-buffer nil)
                     (completion-styles `(,style))
                     completion-category-defaults
                     completion-category-overrides
		     ;; FIXME: Is this TRT for test?
		     (completion-pcm--delim-wild-regex
		      ;; "::1" is a complete word.  ":" isn't a
		      ;; delimiter, therefore.
		      (rx-to-string
		       `(any
			 ,(string-replace
			   ":" "" completion-pcm-word-delimiters))))
		     ;; Don't truncate in *Completions* buffer.
		     (completions-max-height most-positive-fixnum)
	             ;; This is needed for the `simplified' syntax.
                     (tramp-default-method method)
                     (method-string
		      (unless (string-empty-p tramp-method-regexp)
			(concat method tramp-postfix-method-format)))
		     (user-string
		      (unless (tramp-string-empty-or-nil-p user)
			(concat user tramp-postfix-user-format)))
		     ;; This is needed for the IPv6 host name syntax.
		     (ipv6-prefix
		      (and (string-match-p tramp-ipv6-regexp host)
		           tramp-prefix-ipv6-format))
		     (ipv6-postfix
		      (and (string-match-p tramp-ipv6-regexp host)
		           tramp-postfix-ipv6-format))
		     (host-string
		      (unless (tramp-string-empty-or-nil-p host)
			(concat
			 ipv6-prefix host
			 ipv6-postfix tramp-postfix-host-format)))
		     ;; The hop string fits only the initial syntax.
		     (hop (and (eq tramp-syntax orig-syntax) hop))
		     ;; Needed for host name completion.
		     (default-user
		      (file-remote-p
		       (concat
			tramp-prefix-format hop method-string host-string)
		       'user))
		     (default-user-string
		      (unless (tramp-string-empty-or-nil-p default-user)
			(concat default-user tramp-postfix-user-format)))
                     test result completions)

		(dolist
		    (test-and-result
		     ;; These are triples of strings (TEST-STRING
		     ;; RESULT-CHECK COMPLETION-CHECK).  If
		     ;; COMPLETION-CHECK is a list, it is the complete
		     ;; result the contents of *Completions* shall be
		     ;; checked with.
		     (append
		      ;; Complete hop.
		      (unless (tramp-string-empty-or-nil-p hop)
			`((,(concat tramp-prefix-format hop)
			   ,(concat tramp-prefix-format hop)
			   ,(if (string-empty-p tramp-method-regexp)
				(or default-user-string host-string)
			      method-list))))
		      ;; Complete method name.
		      (unless (string-empty-p tramp-method-regexp)
			`((,(concat
                             tramp-prefix-format hop
                             (substring-no-properties
			      method 0 (min 2 (length method))))
			   ,(concat tramp-prefix-format hop method-string)
			   ,method-string)))
		      ;; Complete user name.
		      (unless (tramp-string-empty-or-nil-p user)
			`((,(concat
                             tramp-prefix-format hop method-string
                             (substring-no-properties
			      user 0 (min 2 (length user))))
			   ,(concat
                             tramp-prefix-format hop method-string user-string)
			   ,user-string)))
		      ;; Complete host name.
		      (unless (tramp-string-empty-or-nil-p host-string)
			`((,(concat
                             tramp-prefix-format hop method-string
			     ipv6-prefix
			     (substring-no-properties
			      host 0 (min 2 (length host))))
			   ,(concat
			     tramp-prefix-format hop method-string
			     default-user-string host-string)
			   ,host-string)))
		      ;; Complete user and host name.
		      (unless (or (tramp-string-empty-or-nil-p user-string)
				  (tramp-string-empty-or-nil-p host-string))
			`((,(concat
                             tramp-prefix-format hop method-string user-string
			     ipv6-prefix
			     (substring-no-properties
			      host 0 (min 2 (length host))))
			   ,(concat
                             tramp-prefix-format hop method-string
	                     user-string host-string)
			   ,host-string)))))

		  (dolist
		      (predicate
		       (if (and expensive (tramp--test-emacs31-p))
			   ;; `nil' will be expanded to `file-exists-p'.
			   ;; `read-directory-name' uses `file-directory-p'.
			   ;; `file-directory-p' works since Emacs 31.
			   ;; (Bug#79236)
			   '(file-exists-p file-directory-p) '(nil)))

                    (ignore-errors (kill-buffer "*Completions*"))
		    (when tramp-trace
		      (when (get-buffer trace-buffer)
			(kill-buffer trace-buffer)))
                    (discard-input)
                    (setq test (car test-and-result)
                          unread-command-events
			  (append test (if noninteractive '(tab tab)
                                         '(tab tab return)))
                          completions nil
                          result (read-file-name ": " nil nil nil nil predicate))

                    (if (or (not (get-buffer "*Completions*"))
			    (string-match-p
			     (if (string-empty-p tramp-method-regexp)
				 (rx
				  (| (regexp tramp-postfix-user-regexp)
				     (regexp tramp-postfix-host-regexp))
				  eos)
			       (rx
				(| (regexp tramp-postfix-method-regexp)
				   (regexp tramp-postfix-user-regexp)
				   (regexp tramp-postfix-host-regexp))
				eos))
			     result))
			(progn
			  (when tramp-trace
                            (tramp--test-message
			     (concat
                              "syntax: %s style: %s predicate: %s "
			      "test: %s result: %s")
                             syntax style predicate test result))
			  (should
			   (string-prefix-p (cadr test-and-result) result)))

                      (with-current-buffer "*Completions*"
			;; We must remove leading `default-directory'.
			(goto-char (point-min))
			(let ((inhibit-read-only t))
			  (while (search-forward-regexp "//" nil 'noerror)
			    (delete-region (line-beginning-position) (point))))
			(goto-char (point-min))
			(search-forward-regexp
			 (rx bol (0+ nonl)
			     (any "Pp") "ossible completions"
			     (0+ nonl) eol))
			(forward-line 1)
			(setq completions
                              (split-string
                               (buffer-substring-no-properties
				(point) (point-max))
                               (rx (any "\r\n\t ")) 'omit)))

		      (when tramp-trace
			(tramp--test-message
			 (concat
                          "syntax: %s style: %s predicate: %s test: %s "
		          "result: %s completions: %S")
			 syntax style predicate test result completions))
		      (if (stringp (caddr test-and-result))
			  (should
			   (member (caddr test-and-result) completions))
			(should
			 (equal
			  (caddr test-and-result)
			  (sort completions #'string-lessp)))))))))))

      ;; Cleanup.
      (when tramp-trace
	(untrace-all))
      (tramp-change-syntax orig-syntax)
      (tramp-cleanup-connection tramp-test-vec 'keep-debug 'keep-password))))

(defun tramp--test-split-on-boundary (s)
  "Return completion boundaries for string S."
  (let ((bounds (completion-boundaries s #'completion--file-name-table nil "")))
    (cons (substring s nil (car bounds)) (substring s (car bounds)))))

(ert-deftest tramp-test26-file-name-completion-boundaries ()
  "Test file name completion boundaries are correct in various cases."
  ;; `completion--file-name-table' was reworked in Emacs 31, the
  ;; boundaries are always incorrect before that.
  (skip-unless (tramp--test-emacs31-p))

  (when-let* ((remote (file-remote-p ert-remote-temporary-file-directory)))
    (dolist
	(file `(,remote ,(concat remote "/~/")
		,(concat remote "/usr//usr/") ,(concat remote remote "//usr/")))
      (should (equal (tramp--test-split-on-boundary (concat file "foo"))
                     `(,file . "foo"))))))

(ert-deftest tramp-test27-load ()
  "Check `load'."
  (skip-unless (tramp--test-enabled))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let ((tmp-name (tramp--test-make-temp-name nil quoted)))
      (unwind-protect
	  (progn
	    ;; Ange-FTP does not tolerate a missing file, even with `noerror'.
	    (unless (tramp--test-ange-ftp-p)
	      (load tmp-name 'noerror 'nomessage))
	    (should-not (featurep 'tramp-test-load))
	    (write-region
	     ";;; -*- lexical-binding: t; -*-\n(provide 'tramp-test-load)"
	     nil tmp-name)
	    ;; `load' in lread.c passes `must-suffix' since Emacs 29.
	    ;; In Ange-FTP, `must-suffix' is ignored.
	    (when (and (tramp--test-emacs29-p)
                       (not (tramp--test-ange-ftp-p)))
	      (should-error
	       (load tmp-name nil 'nomessage 'nosuffix 'must-suffix)
	       :type 'file-error))
	    (load tmp-name nil 'nomessage 'nosuffix)
	    (should (featurep 'tramp-test-load)))

	;; Cleanup.
	(ignore-errors
	  (and (featurep 'tramp-test-load) (unload-feature 'tramp-test-load))
	  (delete-file tmp-name))))))

(defun tramp--test-shell-file-name ()
  "Return default remote shell."
  (let ((default-directory ert-remote-temporary-file-directory))
    (tramp-compat-connection-local-value shell-file-name)))

(defun tramp--test-shell-command-switch ()
  "Return default remote shell command switch."
  (let ((default-directory ert-remote-temporary-file-directory))
    (tramp-compat-connection-local-value shell-command-switch)))

(ert-deftest tramp-test28-process-file ()
  "Check `process-file'."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-supports-processes-p))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let* ((tmp-name (tramp--test-make-temp-name nil quoted))
	   (fnnd (file-name-nondirectory tmp-name))
	   (default-directory ert-remote-temporary-file-directory)
	   (buffer (get-buffer-create "*tramp-tests*"))
	   kill-buffer-query-functions)
      (unwind-protect
	  (progn
	    ;; In the "smb" case, default-directory must have a share.
	    (when (tramp--test-smb-p)
	      (let ((default-directory
		     (concat (file-remote-p default-directory) "/")))
		(should-not
		 (zerop (process-file "exit" nil nil nil "-not" "$true")))))
	    (should
	     (zerop
	      (if (tramp--test-smb-p)
		  ;; $true is converted to 1 in Powershell.
		  (process-file "exit" nil nil nil "-not" "$true")
		(process-file "true"))))
	    (should-not
	     (zerop
	      (if (tramp--test-smb-p)
		  ;; $false is converted to 0 in Powershell.
		  (process-file "exit" nil nil nil "-not" "$false")
		(process-file "false"))))
	    (should-not (zerop (process-file "binary-does-not-exist")))
	    ;; Return exit code.
	    ;; FIXME: Make it work with the shell also in the "smb" case.
	    (should
	     (= 42 (if (tramp--test-smb-p)
		       (process-file "exit" nil nil nil "42")
		     (process-file
		      (tramp--test-shell-file-name) nil nil nil
		      (tramp--test-shell-command-switch) "exit 42"))))
	    ;; Return exit code in case the process is interrupted,
	    ;; and there's no indication for a signal describing string.
	    (unless (or (tramp--test-sshfs-p) (tramp--test-smb-p))
	      (let (process-file-return-signal-string)
		(should
		 (= (+ 128 2)
		    (process-file
		     (tramp--test-shell-file-name) nil nil nil
		     (tramp--test-shell-command-switch) "kill -2 $$")))))
	    ;; Return string in case the process is interrupted and
	    ;; there's an indication for a signal describing string.
	    (unless (or (tramp--test-sshfs-p) (tramp--test-smb-p))
	      (let ((process-file-return-signal-string t))
		(should
		 (string-match-p
		  (rx (| "Interrupt" "Signal 2"))
		  (process-file
		   (tramp--test-shell-file-name) nil nil nil
		   (tramp--test-shell-command-switch) "kill -2 $$")))))

	    ;; Check DESTINATION.
	    (dolist (destination `(nil t ,buffer))
	      (when (bufferp destination)
		(with-current-buffer destination
		  (delete-region (point-min) (point-max))))
	      (with-temp-buffer
		(write-region "foo" nil tmp-name)
		(should (file-exists-p tmp-name))
		(should
		 (zerop
		  (if (tramp--test-smb-p)
		      (process-file
		       (format "(ls %s).Name" fnnd) nil destination)
		    (process-file "ls" nil destination nil fnnd))))
		(with-current-buffer
		    (if (bufferp destination) destination (current-buffer))
		  ;; "ls" could produce colorized output.
		  (goto-char (point-min))
		  (while (search-forward-regexp
			  ansi-color-control-seq-regexp nil t)
		    (replace-match ""))
		  (should
		   (string-equal (if destination (format "%s\n" fnnd) "")
				 (buffer-string)))
		  (should-not (get-buffer-window (current-buffer) t))
		  (goto-char (point-max)))

		;; Second run.  The output must be appended.
		(should
		 (zerop
		  (if (tramp--test-smb-p)
		      (process-file
		       (format "(ls %s).Name" fnnd) nil destination t)
		    (process-file "ls" nil destination t fnnd))))
		(with-current-buffer
		    (if (bufferp destination) destination (current-buffer))
		  ;; "ls" could produce colorized output.
		  (goto-char (point-min))
		  (while (search-forward-regexp
			  ansi-color-control-seq-regexp nil t)
		    (replace-match ""))
		  (should
		   (string-equal
		    (if destination (format "%s\n%s\n" fnnd fnnd) "")
		    (buffer-string))))

		(unless (eq destination t)
		  (should (string-empty-p (buffer-string))))
		;; A non-nil DISPLAY must not raise the buffer.
		(should-not (get-buffer-window (current-buffer) t))
		(delete-file tmp-name)))

	    ;; Check remote and local INFILE.
	    (dolist (local '(nil t))
	      (with-temp-buffer
		(setq tmp-name (tramp--test-make-temp-name local quoted))
		(write-region "foo" nil tmp-name)
		(should (file-exists-p tmp-name))
		(should (zerop (process-file "cat" tmp-name t)))
		(should
		 (string-equal
		  (if (tramp--test-smb-p) "foo\n" "foo") (buffer-string)))
		(should-not (get-buffer-window (current-buffer) t))
		(delete-file tmp-name)))

	    ;; Check remote and local DESTINATION file.  This isn't
	    ;; implemented yet in all file name handler backends.
	    ;; (dolist (local '(nil t))
	    ;;   (setq tmp-name (tramp--test-make-temp-name local quoted))
	    ;;   (should
	    ;;    (zerop (process-file "echo" nil `(:file ,tmp-name) nil "foo")))
	    ;;   (with-temp-buffer
	    ;; 	(insert-file-contents tmp-name)
	    ;; 	(should (string-equal "foo" (buffer-string)))
	    ;; 	(should-not (get-buffer-window (current-buffer) t))
	    ;; 	(delete-file tmp-name)))

	    ;; Check remote and local STDERR.
	    (unless (tramp--test-sshfs-p)
	      (dolist (local '(nil t))
		(setq tmp-name (tramp--test-make-temp-name local quoted))
		(should-not
		 (zerop
		  (process-file "cat" nil `(t ,tmp-name) nil "/does-not-exist")))
		(with-temp-buffer
		  (insert-file-contents tmp-name)
		  (should
		   (string-match-p
		    (rx (| (: "cat:" (* nonl) "No such file or directory")
			   ;; MS Windows.
			   (: "cat : Cannot find path")))
		    (buffer-string)))
		  (should-not (get-buffer-window (current-buffer) t))
		  (delete-file tmp-name)))))

	;; Cleanup.
	(ignore-errors (kill-buffer buffer))
	(ignore-errors (delete-file tmp-name))))))

;; Must be a command, because used as `sigusr1' handler.
(defun tramp--test-timeout-handler (&rest _ignore)
  "Timeout handler, reporting a failed test."
  (interactive)
  (tramp--test-message "proc: %s" (get-buffer-process (current-buffer)))
  (when-let* ((proc (get-buffer-process (current-buffer)))
	      ((processp proc)))
    (tramp--test-message "cmd: %s" (process-command proc)))
  (tramp--test-message "buf: %s\n%s\n---" (current-buffer) (buffer-string))
  (ert-fail (format "`%s' timed out" (ert-test-name (ert-running-test)))))

(ert-deftest tramp-test29-start-file-process ()
  "Check `start-file-process'."
  :tags '(:expensive-test :tramp-asynchronous-processes)
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-supports-processes-p))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let ((default-directory ert-remote-temporary-file-directory)
	  (tmp-name (tramp--test-make-temp-name nil quoted))
	  kill-buffer-query-functions command proc)

      ;; Simple process.
      ;; The "smb" method does not support stdin redirection.
      (unless (tramp--test-smb-p)
	(unwind-protect
	    (with-temp-buffer
	      (setq command '("cat")
		    proc
		    (apply
		     #'start-file-process "test1" (current-buffer) command))
	      (should (processp proc))
	      (should (equal (process-status proc) 'run))
	      (should (equal (process-get proc 'remote-command) command))
	      (process-send-string proc "foo\n")
	      (process-send-eof proc)
	      ;; Read output.
	      (with-timeout (10 (tramp--test-timeout-handler))
		(while (< (- (point-max) (point-min)) (length "foo"))
		  (while (accept-process-output proc 0 nil t))))
              ;; Some `cat' implementations do not support the `cat -'
              ;; call.  We skip then.
              (skip-unless
               (not
		(string-match-p (rx "cat: -: input file is output file\n")
				(buffer-string))))
	      (should (string-match-p "foo" (buffer-string))))

	  ;; Cleanup.
	  (ignore-errors (delete-process proc))))

      ;; Simple process using a file.
      (unwind-protect
	  (with-temp-buffer
	    (write-region "foo" nil tmp-name)
	    (should (file-exists-p tmp-name))
	    (setq command `("cat" ,(file-name-nondirectory tmp-name))
		  proc
		  (apply #'start-file-process "test2" (current-buffer) command))
	    (should (processp proc))
	    (should (equal (process-get proc 'remote-command) command))
	    ;; Read output.
	    (with-timeout (10 (tramp--test-timeout-handler))
	      (while (not (string-match-p "foo" (buffer-string)))
		(while (accept-process-output proc 0 nil t))))
	    (should (string-match-p "foo" (buffer-string))))

	;; Cleanup.
	(ignore-errors
	  (delete-process proc)
	  (delete-file tmp-name)))

      ;; Process filter.
      ;; The "smb" method does not support the "cat" stdin
      ;; redirection.  The "adb" method does not support late process
      ;; filter setting for the "echo" command.
      (unwind-protect
	  (with-temp-buffer
	    (setq command (if (tramp--test-smb-p) '("echo" "foo") '("cat"))
		  proc
		  (apply #'start-file-process "test3" (current-buffer) command))
	    (should (processp proc))
	    ;(should (equal (process-status proc) 'run))
	    (should (equal (process-get proc 'remote-command) command))
	    (set-process-filter
	     proc
	     (lambda (p s)
	       (with-current-buffer
		   (process-buffer p)
		 (insert
		  (replace-regexp-in-string
		   (rx bol "foo" (? "\r") eol) "foobar" s)))))
	    (unless (tramp--test-smb-p)
	      (process-send-string proc "foo\n")
	      (process-send-eof proc))
	    ;; Read output.
	    (with-timeout (10 (tramp--test-timeout-handler))
	      (while (not (string-match-p "foobar" (buffer-string)))
		(while (accept-process-output proc 0 nil t))))
	    (should (string-match-p "foobar" (buffer-string))))

	;; Cleanup.
	(ignore-errors (delete-process proc)))

      ;; Disabled process filter.  It doesn't work reliable.
      (unless t
	(unwind-protect
	    (with-temp-buffer
	      (setq command '("cat")
		    proc
		    (apply
		     #'start-file-process "test4" (current-buffer) command))
	      (should (processp proc))
	      (should (equal (process-status proc) 'run))
	      (should (equal (process-get proc 'remote-command) command))
	      (set-process-filter proc t)
	      (process-send-string proc "foo\n")
	      (process-send-eof proc)
	      ;; Read output.  There shouldn't be any.
	      (with-timeout (10)
		(while (process-live-p proc)
		  (while (accept-process-output proc 0 nil t))))
	      ;; No output due to process filter.
	      (should (= (point-min) (point-max))))

	  ;; Cleanup.
	  (ignore-errors (delete-process proc))))

      ;; Process connection type.
      (when (and (tramp--test-sh-p)
		 (not (tramp-direct-async-process-p))
		 (executable-find "hexdump" 'remote))
	(dolist (process-connection-type '(nil pipe t pty))
	  (unwind-protect
	      (with-temp-buffer
		(setq command '("hexdump" "-v" "-e" "/1 \"%02X\n\"")
		      proc
		      (apply #'start-file-process
			     (format "test5-%s" process-connection-type)
			     (current-buffer) command))
		(should (processp proc))
		(should (equal (process-status proc) 'run))
		(should (equal (process-get proc 'remote-command) command))
		;; Give the pipe process a chance to start.
		(when (memq process-connection-type '(nil pipe))
		  (sit-for 0.1 'nodisp))
		(process-send-string proc "foo\r\n")
		(process-send-eof proc)
		;; Read output.  On macOS, there is always newline
                ;; conversion.  "telnet" converts \r to <CR><NUL> if
                ;; `crlf' flag is FALSE.  See telnet(1) man page.
		(let ((expected
		       (rx "66\n" "6F\n" "6F\n"
			   (| "0D\n" "0A\n") (? "00\n") "0A\n")))
		  (with-timeout (10 (tramp--test-timeout-handler))
		    (while (not (string-match-p expected (buffer-string)))
		      (while (accept-process-output proc 0 nil t))))
		  (should (string-match-p expected (buffer-string)))))

	    ;; Cleanup.
	    (ignore-errors (delete-process proc)))))

      ;; PTY.
      (unwind-protect
	  (with-temp-buffer
	    ;; It works only for tramp-sh.el, and not direct async processes.
	    (if (or (not (tramp--test-sh-p)) (tramp-direct-async-process-p))
		(should-error
		 (start-file-process "test6" (current-buffer) nil)
		 :type 'wrong-type-argument)

	      (setq proc (start-file-process "test6" (current-buffer) nil))
	      (should (processp proc))
	      (should (equal (process-status proc) 'run))
	      (should-not (process-get proc 'remote-command))
	      ;; On MS Windows, `process-tty-name' returns nil.
	      (unless (tramp--test-windows-nt-p)
		(should (stringp (process-tty-name proc))))))

	;; Cleanup.
	(ignore-errors (delete-process proc))))))

(defmacro tramp--test-deftest-direct-async-process (test &optional unstable)
  "Define ert test `TEST-direct-async' for direct async processes.
If UNSTABLE is non-nil, the test is tagged as `:unstable'."
  (declare (indent 1))
  `(ert-deftest ,(intern (concat (symbol-name test) "-direct-async")) ()
     ;; This is the docstring.  However, it must be expanded to a
     ;; string inside the macro.  No idea.
     ;; (concat (ert-test-documentation (get ',test 'ert--test))
     ;;         "\nUse direct async process.")
     :tags (append '(:expensive-test :tramp-asynchronous-processes)
		   (and ,unstable '(:unstable)))
     (skip-unless (tramp--test-enabled))
     (let* ((default-directory ert-remote-temporary-file-directory)
	    (ert-test (ert-get-test ',test))
	    (connection-local-profile-alist
	     (cons
	      '(direct-async-process-profile (tramp-direct-async-process . t))
	      connection-local-profile-alist))
	    (connection-local-criteria-alist
	     (cons
	      `((:application tramp
		 :machine ,(file-remote-p default-directory 'host))
		direct-async-process-profile)
	      connection-local-criteria-alist)))
       (skip-unless (tramp-direct-async-process-p))
       (when-let* ((result (ert-test-most-recent-result ert-test)))
	 (skip-unless (< (ert-test-result-duration result) 300)))
       ;; We do expect an established connection already,
       ;; `file-truename' does it by side-effect.  Suppress
       ;; `tramp--test-enabled', in order to keep the connection.
       (cl-letf (((symbol-function #'tramp--test-enabled) #'always))
	 (file-truename ert-remote-temporary-file-directory)
	 (funcall (ert-test-body ert-test))))))

(tramp--test-deftest-direct-async-process tramp-test29-start-file-process)

(ert-deftest tramp-test30-make-process ()
  "Check `make-process'."
  :tags (append '(:expensive-test :tramp-asynchronous-processes)
		(and (getenv "EMACS_EMBA_CI") '(:unstable)))
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-supports-processes-p))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let ((default-directory ert-remote-temporary-file-directory)
	  (tmp-name (tramp--test-make-temp-name nil quoted))
	  (inhibit-message (not (ignore-errors (edebug-mode))))
	  kill-buffer-query-functions command proc)
      (should-not (apply #'make-process nil)) ; Use `apply' to avoid warnings.

      ;; Simple process.
      ;; The "smb" method does not support stdin redirection.
      (unless (tramp--test-smb-p)
	(unwind-protect
	    (with-temp-buffer
	      (setq command '("cat")
		    proc
		    (make-process
		     :name "test1" :buffer (current-buffer) :command command
		     :file-handler t))
	      (should (processp proc))
	      (should (equal (process-status proc) 'run))
	      (should (equal (process-get proc 'remote-command) command))
	      (process-send-string proc "foo\n")
	      (process-send-eof proc)
	      ;; Read output.
	      (with-timeout (10 (tramp--test-timeout-handler))
		(while (< (- (point-max) (point-min)) (length "foo"))
		  (while (accept-process-output proc 0 nil t))))
              ;; Some `cat' implementations do not support the `cat -'
              ;; call.  We skip then.
              (skip-unless
               (not
		(string-match-p (rx "cat: -: input file is output file\n")
				(buffer-string))))
	      (should (string-match-p "foo" (buffer-string))))

	  ;; Cleanup.
	  (ignore-errors (delete-process proc))))

      ;; Simple process using a file.
      (unwind-protect
	  (with-temp-buffer
	    (write-region "foo" nil tmp-name)
	    (should (file-exists-p tmp-name))
	    (setq command `("cat" ,(file-name-nondirectory tmp-name))
		  proc
		  (make-process
		   :name "test2" :buffer (current-buffer) :command command
		   :file-handler t))
	    (should (processp proc))
	    (should (equal (process-get proc 'remote-command) command))
	    ;; Read output.
	    (with-timeout (10 (tramp--test-timeout-handler))
	      (while (not (string-match-p "foo" (buffer-string)))
		(while (accept-process-output proc 0 nil t))))
	    (should (string-match-p "foo" (buffer-string))))

	;; Cleanup.
	(ignore-errors
	  (delete-process proc)
	  (delete-file tmp-name)))

      ;; Process filter.
      (unwind-protect
	  (with-temp-buffer
	    (setq command '("echo" "foo")
		  proc
		  (make-process
		   :name "test3" :buffer (current-buffer) :command command
		   :filter
		   (lambda (p s)
		     (with-current-buffer
			 (process-buffer p)
		       (insert
			(replace-regexp-in-string
			 (rx bol "foo" (? "\r") eol) "foobar" s))))
		   :file-handler t))
	    (should (processp proc))
	    ;(should (equal (process-status proc) 'run))
	    (should (equal (process-get proc 'remote-command) command))
	    ;; Read output.
	    (with-timeout (10 (tramp--test-timeout-handler))
	      (while (not (string-match-p "foobar" (buffer-string)))
		(while (accept-process-output proc 0 nil t))))
	    (should (string-match-p "foobar" (buffer-string))))

	;; Cleanup.
	(ignore-errors (delete-process proc)))

      ;; Disabled process filter.  It doesn't work reliable.
      (unless t
	(unwind-protect
	    (with-temp-buffer
	      (setq command '("cat")
		    proc
		    (make-process
		     :name "test4" :buffer (current-buffer) :command command
		     :filter t :file-handler t))
	      (should (processp proc))
	      (should (equal (process-status proc) 'run))
	      (should (equal (process-get proc 'remote-command) command))
	      (process-send-string proc "foo\n")
	      (process-send-eof proc)
	      ;; Read output.  There shouldn't be any.
	      (with-timeout (10)
		(while (process-live-p proc)
		  (while (accept-process-output proc 0 nil t))))
	      ;; No output due to process filter.
	      (should (= (point-min) (point-max))))

	  ;; Cleanup.
	  (ignore-errors (delete-process proc))))

      ;; Process sentinel.
      (unwind-protect
	  (with-temp-buffer
	    (setq command '("cat")
		  proc
		  (make-process
		   :name "test5" :buffer (current-buffer) :command command
		   :sentinel
		   (lambda (p s)
		     (with-current-buffer (process-buffer p) (insert s)))
		   :file-handler t))
	    (should (processp proc))
	    (should (equal (process-status proc) 'run))
	    (should (equal (process-get proc 'remote-command) command))
	    (process-send-string proc "foo\n")
	    (process-send-eof proc)
	    (delete-process proc)
	    ;; Read output.
	    (with-timeout (10 (tramp--test-timeout-handler))
	      (while (accept-process-output proc 0 nil t)))
	    ;; On some MS Windows systems, it returns "unknown signal".
	    (should
	     (string-match-p
	      (rx (| "unknown signal" "killed")) (buffer-string))))

	;; Cleanup.
	(ignore-errors (delete-process proc)))

      ;; Process with stderr buffer.  "telnet" does not cooperate with
      ;; three processes.
      (unless (or (tramp--test-telnet-p) (tramp-direct-async-process-p))
	(let ((stderr (generate-new-buffer "*stderr*")))
	  (unwind-protect
	      (with-temp-buffer
		(setq command '("cat" "/does-not-exist")
		      proc
		      (make-process
		       :name "test6" :buffer (current-buffer) :command  command
		       :stderr stderr :file-handler t))
		(should (processp proc))
		(should (equal (process-get proc 'remote-command) command))
		;; Read output.
		(with-timeout (10 (tramp--test-timeout-handler))
		  (while (accept-process-output proc 0 nil t)))
		;; Read stderr.
		(with-current-buffer stderr
		  (with-timeout (10 (tramp--test-timeout-handler))
		    (while (not (string-match-p
				 (rx (| "No such file or directory"
					"Cannot find path"))
				 (buffer-string)))
		      (when-let* ((p (or (get-buffer-process stderr)
					 auto-revert-notify-watch-descriptor))
				  ((processp p)))
			(while (accept-process-output p 0 nil t)))))
		  (delete-process proc)
		  (should
		   (string-match-p
		    (rx (| (: "cat:" (* nonl) "No such file or directory")
			   ;; MS Windows.
			   (: "cat : Cannot find path")))
		    (buffer-string)))))

	    ;; Cleanup.
	    (ignore-errors (delete-process proc))
	    (ignore-errors (kill-buffer stderr)))))

      ;; Process with stderr file.
      (unless (tramp-direct-async-process-p)
	(unwind-protect
	    (with-temp-buffer
	      (setq command '("cat" "/does-not-exist")
		    proc
		    (make-process
		     :name "test7" :buffer (current-buffer) :command command
		     :stderr tmp-name :file-handler t))
	      (should (processp proc))
	      (should (equal (process-get proc 'remote-command) command))
	      ;; Read stderr.
	      (with-timeout (10 (tramp--test-timeout-handler))
		(let ((remote-file-name-inhibit-cache t))
		  (while
		      (or (not (file-exists-p tmp-name))
			  (zerop
			   (file-attribute-size (file-attributes tmp-name)))))))
	      (delete-process proc)
	      (with-temp-buffer
		(insert-file-contents tmp-name)
		(should
		 (string-match-p
		  (rx (| (: "cat:" (* nonl) "No such file or directory")
			 ;; MS Windows.
			 (: "cat : Cannot find path")))
		  (buffer-string)))))

	  ;; Cleanup.
	  (ignore-errors (delete-process proc))
	  (ignore-errors (delete-file tmp-name))))

      ;; Process connection type.
      (when (and (tramp--test-sh-p)
		 (not (tramp-direct-async-process-p))
		 (executable-find "hexdump" 'remote))
	(dolist (connection-type '(nil pipe t pty))
	  ;; `process-connection-type' is taken when
	  ;; `:connection-type' is nil.
	  (dolist (process-connection-type
		   (if connection-type '(nil pipe t pty) '(nil)))
	    (unwind-protect
		(with-temp-buffer
		  (setq command '("hexdump" "-v" "-e" "/1 \"%02X\n\"")
			proc
			(make-process
			 :name
			 (format "test8-%s-%s"
				 connection-type process-connection-type)
			 :buffer (current-buffer)
			 :connection-type connection-type
			 :command command
			 :file-handler t))
		  (should (processp proc))
		  (should (equal (process-status proc) 'run))
		  (should (equal (process-get proc 'remote-command) command))
		  ;; Give the pipe process a chance to start.
		  (when (or (eq connection-type 'pipe)
			    (memq process-connection-type '(nil pipe)))
		    (sit-for 0.1 'nodisp))
		  (process-send-string proc "foo\r\n")
		  (process-send-eof proc)
		  ;; Read output.  On macOS, there is always newline
                  ;; conversion.  "telnet" converts \r to <CR><NUL> if
                  ;; `crlf' flag is FALSE.  See telnet(1) man page.
		  (let ((expected
			 (rx "66\n" "6F\n" "6F\n"
			     (| "0D\n" "0A\n") (? "00\n") "0A\n")))
		    (with-timeout (10 (tramp--test-timeout-handler))
		      (while (not (string-match-p expected (buffer-string)))
			(while (accept-process-output proc 0 nil t))))
		    (should (string-match-p expected (buffer-string)))))

	      ;; Cleanup.
	      (ignore-errors (delete-process proc)))))))))

(tramp--test-deftest-direct-async-process tramp-test30-make-process)

(ert-deftest tramp-test31-interrupt-process ()
  "Check `interrupt-process'."
  ;; The final `process-live-p' check does not run sufficiently.
  :tags '(:expensive-test :tramp-asynchronous-processes :unstable)
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-sh-p))
  (skip-unless (not (tramp--test-windows-nt-p)))
  (skip-unless (not (tramp--test-crypt-p)))

  ;; We must use `file-truename' for the temporary directory, in
  ;; order to establish the connection prior running an asynchronous
  ;; process.
  (let ((default-directory (file-truename ert-remote-temporary-file-directory))
	(delete-exited-processes t)
	kill-buffer-query-functions command proc)
    (unwind-protect
	(with-temp-buffer
	  (setq command "trap 'echo boom; exit 1' 2; sleep 100"
		proc (start-file-process-shell-command
		      "test" (current-buffer) command))
	  (should (processp proc))
	  (should (process-live-p proc))
	  (should (equal (process-status proc) 'run))
	  (should (numberp (process-get proc 'remote-pid)))
	  (should (equal (process-get proc 'remote-command)
			 (with-connection-local-variables
			  `(,shell-file-name ,shell-command-switch ,command))))
	  (should (interrupt-process proc))
	  ;; Let the process accept the interrupt.
	  (with-timeout (10 (tramp--test-timeout-handler))
	    (while (process-live-p proc)
	      (while (accept-process-output proc 0 nil t))))
	  (should-not (process-live-p proc))
	  ;; An interrupted process cannot be interrupted, again.
	  (should-error
	   (interrupt-process proc)
	   :type 'error))

      ;; Cleanup.
      (ignore-errors (delete-process proc)))))

(ert-deftest tramp-test31-signal-process ()
  "Check `signal-process'."
  ;; The final `process-live-p' check does not run sufficiently.
  :tags '(:expensive-test :tramp-asynchronous-processes :unstable)
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-sh-p))
  (skip-unless (not (tramp--test-windows-nt-p)))
  (skip-unless (not (tramp--test-crypt-p)))
  ;; Since Emacs 29.1.
  (skip-unless (boundp 'signal-process-functions))

  ;; We must use `file-truename' for the temporary directory, in
  ;; order to establish the connection prior running an asynchronous
  ;; process.
  (let ((default-directory (file-truename ert-remote-temporary-file-directory))
	(delete-exited-processes t)
	kill-buffer-query-functions command proc)

    ;; If PROCESS is a string, it must be a process name or a process
    ;; number.  Check error handling.
    (should-error
     (signal-process (md5 (current-time-string)) 0)
     :type 'wrong-type-argument)

    ;; The PROCESS argument of `signal-process' can be a string.  Test
    ;; this as well.
    (dolist
	(func '(identity
		(lambda (x) (format "%s" (if (processp x) (process-name x) x)))))
      (dolist (sigcode '(2 INT))
	(unwind-protect
	    (with-temp-buffer
	      (setq command "trap 'echo boom; exit 1' 2; sleep 100"
		    proc (start-file-process-shell-command
		          (format "test1-%s" sigcode) (current-buffer) command))
	      (should (processp proc))
	      (should (process-live-p proc))
	      (should (equal (process-status proc) 'run))
	      (should (numberp (process-get proc 'remote-pid)))
	      (should
	       (equal (process-get proc 'remote-command)
		      (with-connection-local-variables
		       `(,shell-file-name ,shell-command-switch ,command))))
	      (should (zerop (signal-process (funcall func proc) sigcode)))
	      ;; Let the process accept the signal.
	      (with-timeout (10 (tramp--test-timeout-handler))
		(while (accept-process-output proc 0 nil t)))
              (should-not (process-live-p proc)))

          ;; Cleanup.
          (ignore-errors (kill-process proc))
          (ignore-errors (delete-process proc)))

	(unwind-protect
	    (with-temp-buffer
	      (setq command "trap 'echo boom; exit 1' 2; sleep 100"
		    proc (start-file-process-shell-command
		          (format "test2-%s" sigcode) (current-buffer) command))
	      (should (processp proc))
	      (should (process-live-p proc))
	      (should (equal (process-status proc) 'run))
	      (should (numberp (process-get proc 'remote-pid)))
	      (should
	       (equal (process-get proc 'remote-command)
		      (with-connection-local-variables
		       `(,shell-file-name ,shell-command-switch ,command))))
	      ;; `signal-process' has argument REMOTE since Emacs 29.
	      (with-no-warnings
		(should
		 (zerop
		  (signal-process
		   (funcall func (process-get proc 'remote-pid))
		   sigcode default-directory))))
	      ;; Let the process accept the signal.
	      (with-timeout (10 (tramp--test-timeout-handler))
		(while (accept-process-output proc 0 nil t)))
              (should-not (process-live-p proc)))

          ;; Cleanup.
          (ignore-errors (kill-process proc))
          (ignore-errors (delete-process proc)))))))

(ert-deftest tramp-test31-list-system-processes ()
  "Check `list-system-processes'."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-supports-processes-p))
  ;; `list-system-processes' is supported since Emacs 29.1.
  (skip-unless (tramp--test-emacs29-p))

  (let ((default-directory ert-remote-temporary-file-directory))
    (skip-unless (consp (list-system-processes)))
    (should (not (equal (list-system-processes)
			(let ((default-directory temporary-file-directory))
			  (list-system-processes)))))))

(ert-deftest tramp-test31-process-attributes ()
  "Check `process-attributes'."
  :tags '(:expensive-test :tramp-asynchronous-processes)
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-supports-processes-p))
  ;; `process-attributes' is supported since Emacs 29.1.
  (skip-unless (tramp--test-emacs29-p))

  ;; We must use `file-truename' for the temporary directory, in
  ;; order to establish the connection prior running an asynchronous
  ;; process.
  (let ((default-directory (file-truename ert-remote-temporary-file-directory))
	(delete-exited-processes t)
	kill-buffer-query-functions command proc)
    (skip-unless (consp (list-system-processes)))

    (unwind-protect
	(progn
	  (setq command '("sleep" "100")
		proc (apply #'start-file-process "test" nil command))
	  (while (accept-process-output proc 0))
	  (when-let* ((pid (process-get proc 'remote-pid))
		      (attributes (process-attributes pid)))
	    ;; (tramp--test-message "%s" attributes)
	    (should (equal (cdr (assq 'comm attributes)) (car command)))
	    (should (equal (cdr (assq 'args attributes))
			   (string-join command " ")))))

      ;; Cleanup.
      (ignore-errors (delete-process proc)))))

(ert-deftest tramp-test31-memory-info ()
  "Check `memory-info'."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-supports-processes-p))
  ;; `memory-info' is supported since Emacs 29.1.
  (skip-unless (tramp--test-emacs29-p))

  (when-let* ((default-directory ert-remote-temporary-file-directory)
              (mi (memory-info)))
    (should (consp mi))
    (should (length= mi 4))
    (dotimes (i (length mi))
      (should (natnump (nth i mi))))))

(defun tramp--test-async-shell-command
    (command output-buffer &optional error-buffer input)
  "Like `async-shell-command', reading the output.
INPUT, if non-nil, is a string sent to the process."
  (let ((proc (async-shell-command command output-buffer error-buffer))
	(delete-exited-processes t))
    ;; `tramp-smb-handle-shell-command' modifies the command.
    (unless (tramp--test-smb-p)
      (should (equal (process-get proc 'remote-command)
		     (with-connection-local-variables
		      `(,shell-file-name ,shell-command-switch ,command)))))
    (cl-letf (((symbol-function #'shell-command-sentinel) #'ignore))
      (when (stringp input)
	(process-send-string proc input))
      (with-timeout
	  ((if (getenv "EMACS_EMBA_CI") 30 10) (tramp--test-timeout-handler))
	(while
	    (or (accept-process-output proc nil nil t) (process-live-p proc))))
      (accept-process-output proc nil nil t))))

(defun tramp--test-shell-command-to-string-asynchronously (command)
  "Like `shell-command-to-string', but for asynchronous processes."
  (with-temp-buffer
    (tramp--test-async-shell-command command (current-buffer))
    (buffer-substring-no-properties (point-min) (point-max))))

(ert-deftest tramp-test32-shell-command ()
  "Check `shell-command'."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-supports-processes-p))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let ((tmp-name (tramp--test-make-temp-name nil quoted))
	  (default-directory ert-remote-temporary-file-directory)
	  ;; Suppress nasty messages.
	  (inhibit-message (not (ignore-errors (edebug-mode))))
	  kill-buffer-query-functions)

      (dolist (this-shell-command
	       (append
		;; Synchronously.
		'(shell-command)
		;; Asynchronously.
		(and (tramp--test-asynchronous-processes-p)
		     '(tramp--test-async-shell-command))))

	;; Test ordinary `{async-}shell-command'.
	(unwind-protect
	    (with-temp-buffer
	      (write-region "foo" nil tmp-name)
	      (should (file-exists-p tmp-name))
	      (funcall
	       this-shell-command
	       (format
		(if (tramp--test-smb-p) "(ls %s).Name" "ls %s")
		(file-name-nondirectory tmp-name))
	       (current-buffer))
	      ;; "ls" could produce colorized output.
	      (goto-char (point-min))
	      (while (search-forward-regexp ansi-color-control-seq-regexp nil t)
		(replace-match ""))
	      (should
	       (string-equal
		(format "%s\n" (file-name-nondirectory tmp-name))
		(buffer-string))))

	  ;; Cleanup.
	  (ignore-errors (delete-file tmp-name)))

	;; Test `{async-}shell-command' with error buffer.
	;; Method "smb" does not support ">&2" construct.
	(unless (or (tramp--test-smb-p) (tramp-direct-async-process-p))
	  (let ((stderr (generate-new-buffer "*stderr*")))
	    (unwind-protect
		(with-temp-buffer
		  (funcall
		   this-shell-command
		   "echo foo >&2; echo bar" (current-buffer) stderr)
		  (should (string-equal "bar\n" (buffer-string)))
		  ;; Check stderr.  Some shells echo, for example the
		  ;; "adb" or container methods.
		  (should
		   (string-match-p
		    (rx bol (** 1 2 "foo\n") eol)
		    (tramp-get-buffer-string stderr))))

	      ;; Cleanup.
	      (ignore-errors (kill-buffer stderr))))))

      ;; Test sending string to `async-shell-command'.
      (when (and (not (tramp--test-smb-p))
		 (tramp--test-asynchronous-processes-p))
	(unwind-protect
	    (with-temp-buffer
	      (write-region "foo" nil tmp-name)
	      (should (file-exists-p tmp-name))
	      (tramp--test-async-shell-command
	       "read line; ls $line" (current-buffer) nil
	       ;; String to be sent.
	       (format "%s\n" (file-name-nondirectory tmp-name)))
	      (should
	       (string-match-p
		;; Some shells echo, for example the "adb" or container methods.
		(rx
		 bos (** 1 2 (literal (file-name-nondirectory tmp-name)) "\n")
		 eos)
		(buffer-string))))

	  ;; Cleanup.
	  (ignore-errors (delete-file tmp-name))))))

  ;; Test `async-shell-command-width'.
  (when (and (tramp--test-asynchronous-processes-p) (tramp--test-sh-p))
    (let* (;; Since Fedora 41, this seems to be the upper limit.  Used
	   ;; to be 1024 before.
	   (async-shell-command-width 512)
	   (default-directory ert-remote-temporary-file-directory)
	   (cols (ignore-errors
		   (read (tramp--test-shell-command-to-string-asynchronously
			  "tput cols")))))
      (when (natnump cols)
	(should (= cols async-shell-command-width))))))

(tramp--test-deftest-direct-async-process tramp-test32-shell-command)

;; This test is inspired by Bug#39067.
(ert-deftest tramp-test32-shell-command-dont-erase-buffer ()
  "Check `shell-command-dont-erase-buffer'."
  ;; As long as Bug#40896 is not solved both in simple.el and Tramp,
  ;; this test cannot run properly.
  :tags '(:expensive-test :unstable)
  (skip-unless (tramp--test-enabled))
  (skip-unless nil)
  (skip-unless (tramp--test-supports-processes-p))

  ;; (message "   s-c-d-e-b current-buffer buffer-string point")
  ;; (message "===============================================")

  ;;    s-c-d-e-b current-buffer buffer-string point
  ;; ===============================================
  ;;          nil              t    foobazzbar     4 x
  ;;          nil            nil          bazz     5
  ;; -----------------------------------------------
  ;;        erase              t          bazz     1 x
  ;;        erase            nil          bazz     5
  ;; -----------------------------------------------
  ;; beg-last-out              t    foobazzbar     4 x
  ;; beg-last-out            nil    foobarbazz     7
  ;; -----------------------------------------------
  ;; end-last-out              t    foobazzbar     4
  ;; end-last-out            nil    foobazzbar    11
  ;; -----------------------------------------------
  ;;   save-point              t    foobazzbar     4 x
  ;;   save-point            nil    foobarbazz     4 x
  ;; -----------------------------------------------
  ;;       random              t    foobazzbar     4
  ;;       random            nil    foobazzbar    11
  ;; -----------------------------------------------

  (let (;; Suppress nasty messages.
	(inhibit-message (not (ignore-errors (edebug-mode))))
	buffer kill-buffer-query-functions)
    ;; We check both the local and remote case, in order to guarantee
    ;; that they behave similar.
    (dolist (default-directory
	      `(,temporary-file-directory ,ert-remote-temporary-file-directory))
      ;; These are the possible values of `shell-command-dont-erase-buffer'.
      ;; `random' is taken as non-nil value without special meaning.
      (dolist (shell-command-dont-erase-buffer
	       '(nil erase beg-last-out end-last-out save-point random))
	;; `shell-command' might work over the current buffer, or not.
	(dolist (current '(t nil))
	  (with-temp-buffer
	    ;; We insert the string "foobar" into an empty buffer.
	    ;; Point is set between "foo" and "bar".
	    (setq buffer (current-buffer))
	    (insert "foobar")
	    (goto-char (- (point) 3))
	    (should (string-equal "foobar" (buffer-string)))
	    (should (string-equal "foo" (buffer-substring (point-min) (point))))
	    (should (string-equal "bar" (buffer-substring (point) (point-max))))

	    ;; Apply `shell-command'.  It shall output the string
	    ;; "bazz".  Messages in the *Messages* buffer are
	    ;; suppressed.
            (let (message-log-max)
	      (if current
		  (shell-command "echo -n bazz" (current-buffer))
		(with-temp-buffer (shell-command "echo -n bazz" buffer))))

	  ;;   (message
	  ;;    "%12s %14s %13s %5d"
	  ;;    shell-command-dont-erase-buffer current (buffer-string) (point))))
	  ;; (message "-----------------------------------------------")))))

	    ;; Check result.
	    (cond
	     (current
	      ;; String is inserted at point, and point is preserved
	      ;; unless dictated otherwise.
	      (cond
	       ((null shell-command-dont-erase-buffer)
		(should (string-equal "foobazzbar" (buffer-string)))
		(should (= 4 (point))))
	       ((eq shell-command-dont-erase-buffer 'erase)
		(should (string-equal "bazz" (buffer-string)))
		(should (= 1 (point))))
	       ((eq shell-command-dont-erase-buffer 'beg-last-out)
		(should (string-equal "foobazzbar" (buffer-string)))
		(should (= 4 (point))))
	       ;; Bug#40896
	       ;; ((eq shell-command-dont-erase-buffer 'end-last-out)
	       ;; 	(should (string-equal "foobazzbar" (buffer-string)))
	       ;; 	(should (= 7 (point))))
	       ((eq shell-command-dont-erase-buffer 'save-point)
		(should (string-equal "foobazzbar" (buffer-string)))
		(should (= 4 (point))))
	       ;; Bug#40896
	       ;; ((eq shell-command-dont-erase-buffer 'random)
	       ;; 	(should (string-equal "foobazzbar" (buffer-string)))
	       ;; 	(should (= 7 (point))))))
	       ))

	     (t ;; not current buffer
	      ;; String is appended, and point is at point-max unless
	      ;; dictated otherwise.
	      (cond
	       ((null shell-command-dont-erase-buffer)
		(should (string-equal "bazz" (buffer-string)))
		(should (= 5 (point))))
	       ((eq shell-command-dont-erase-buffer 'erase)
		(should (string-equal "bazz" (buffer-string)))
		(should (= 5 (point))))
	       ((eq shell-command-dont-erase-buffer 'beg-last-out)
		(should (string-equal "foobarbazz" (buffer-string)))
		(should (= 7 (point))))
	       ;; ;; Bug#40896
	       ;; ((eq shell-command-dont-erase-buffer 'end-last-out)
	       ;; 	(should (string-equal "foobarbazz" (buffer-string)))
	       ;; 	(should (= 11 (point))))
	       ((eq shell-command-dont-erase-buffer 'save-point)
		(should (string-equal "foobarbazz" (buffer-string)))
		(should (= 4 (point))))
	       ;; ;; Bug#40896
	       ;; ((eq shell-command-dont-erase-buffer 'random)
	       ;; 	(should (string-equal "foobarbazz" (buffer-string)))
	       ;; 	(should (= 11 (point)))))))))))))
	       )))))))))

;; This test is inspired by Bug#23952 and Bug#80783.
(ert-deftest tramp-test33-environment-variables ()
  "Check that remote processes set / unset environment variables properly."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-supports-environment-variables-p))

  (dolist (this-shell-command-to-string
	   (append
	    ;; Synchronously.
	    '(shell-command-to-string)
	    ;; Asynchronously.
	    (and (tramp--test-asynchronous-processes-p)
		 '(tramp--test-shell-command-to-string-asynchronously))))

    (let ((default-directory ert-remote-temporary-file-directory)
	  (shell-file-name "/bin/sh")
	  (envvar (concat "VAR_" (upcase (md5 (current-time-string)))))
	  kill-buffer-query-functions)

      ;; Check INSIDE_EMACS.
      (setenv "INSIDE_EMACS")
      (should
       (string-equal
	(format "%s,tramp:%s\n" emacs-version tramp-version)
	(funcall this-shell-command-to-string "echo \"${INSIDE_EMACS:-bla}\"")))
      (let ((process-environment
	     (cons (format "INSIDE_EMACS=%s,foo" emacs-version)
		   process-environment)))
	(should
	 (string-equal
	  (format "%s,foo,tramp:%s\n" emacs-version tramp-version)
	  (funcall
	   this-shell-command-to-string "echo \"${INSIDE_EMACS:-bla}\""))))

      ;; Check EMACSCLIENT_TRAMP.
      (setenv "EMACSCLIENT_TRAMP")
      (let ((tramp-propagate-emacsclient-tramp t))
	(should
	 (string-equal
	  (format "%s\n" (tramp-make-tramp-file-name tramp-test-vec 'noloc))
	  (funcall
	   this-shell-command-to-string "echo \"${EMACSCLIENT_TRAMP:-bla}\""))))
      (let (tramp-propagate-emacsclient-tramp)
	(should
	 (string-equal
	  "bla\n"
	  (funcall
	   this-shell-command-to-string "echo \"${EMACSCLIENT_TRAMP:-bla}\""))))

      ;; Set a value.
      (let ((process-environment
	     (cons (concat envvar "=foo") process-environment)))
	;; Default value.
	(should
	 (string-match-p
	  "foo"
	  (funcall
	   this-shell-command-to-string
	   (format "echo \"${%s:-bla}\"" envvar)))))

      ;; Set the empty value.
      (let ((process-environment
	     (cons (concat envvar "=") process-environment)))
	;; Value is null.
	(should
	 (string-match-p
	  "bla"
	  (funcall
	   this-shell-command-to-string (format "echo \"${%s:-bla}\"" envvar))))
	;; Variable is set.
	(should
	 (string-match-p
	  (rx (literal envvar)) (funcall this-shell-command-to-string "set"))))

      (unless (tramp-direct-async-process-p)
	;; We force a reconnect, in order to have a clean environment.
	(tramp-cleanup-connection tramp-test-vec 'keep-debug 'keep-password)
	;; Unset the variable.
	(let ((tramp-remote-process-environment
	       (cons (concat envvar "=foo") tramp-remote-process-environment)))
	  ;; Refill the cache; we don't want to run into timeouts.
	  (file-truename default-directory)
	  ;; Check the initial value, we want to unset below.
	  (should
	   (string-match-p
	    "foo"
	    (funcall
	     this-shell-command-to-string
	     (format "echo \"${%s:-bla}\"" envvar))))
	  (let ((process-environment (cons envvar process-environment)))
	    ;; Variable is unset.
	    (should
	     (string-match-p
	      "bla"
	      (funcall
	       this-shell-command-to-string
	       (format "echo \"${%s:-bla}\"" envvar))))
	    ;; Variable is unset.
	    (should-not
	     (string-match-p
	      (rx (literal envvar))
	      ;; We must remove PS1, the output is truncated otherwise.
	      ;; We must suppress "_=VAR...".
	      (funcall
	       this-shell-command-to-string
	       "printenv | grep -v PS1 | grep -v _="))))))

      ;; Handle looooong environment variables.  Bug#80783.
      ;; FIXME: Make it also work in the synchronous case.
      (unless (or (eq this-shell-command-to-string 'shell-command-to-string)
		  (tramp-direct-async-process-p))
	(let* ((bad (concat envvar "=" (make-string 2024 ?x)))
	       (process-environment
		(cl-list* bad bad bad bad process-environment)))
	  (should
	   (string-match-p
	    "foo" (funcall this-shell-command-to-string "echo foo"))))))))

(tramp--test-deftest-direct-async-process tramp-test33-environment-variables)

;; This test is inspired by Bug#27009.
(ert-deftest tramp-test33-environment-variables-and-port-numbers ()
  "Check that two connections with separate ports are different."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-enabled))
  ;; We test it only for the mock-up connection; otherwise there might
  ;; be problems with the used ports.
  (skip-unless (and (eq tramp-syntax 'default) (tramp--test-mock-p)))
  (skip-unless (not (tramp--test-crypt-p)))

  ;; We force a reconnect, in order to have a clean environment.
  (dolist (dir `(,ert-remote-temporary-file-directory
		 "/mock:localhost#11111:" "/mock:localhost#22222:"))
    (tramp-cleanup-connection
     (tramp-dissect-file-name dir) 'keep-debug 'keep-password))

  (unwind-protect
      (dolist (port '(11111 22222))
	(let* ((default-directory
		 (format "/mock:localhost#%d:%s" port temporary-file-directory))
	       (shell-file-name "/bin/sh")
	       (envvar (concat "VAR_" (upcase (md5 (current-time-string)))))
	       ;; We cannot use `process-environment', because this
	       ;; would be applied in `process-file'.
	       (tramp-remote-process-environment
		(cons
		 (format "%s=%d" envvar port)
		 tramp-remote-process-environment)))
	  (should
	   (string-match-p
	    (number-to-string port)
	    (shell-command-to-string (format "echo $%s" envvar))))))

    ;; Cleanup.
    (dolist (dir '("/mock:localhost#11111:" "/mock:localhost#22222:"))
      (tramp-cleanup-connection (tramp-dissect-file-name dir)))))

(ert-deftest tramp-test34-connection-local-variables ()
  "Check that connection-local variables are enabled."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-enabled))

  (let* ((default-directory ert-remote-temporary-file-directory)
	 (tmp-name1 (tramp--test-make-temp-name))
	 (tmp-name2 (expand-file-name "foo" tmp-name1))
	 (enable-local-variables :all)
	 (enable-remote-dir-locals t)
         (inhibit-message (not (ignore-errors (edebug-mode))))
	 kill-buffer-query-functions
	 (clpa connection-local-profile-alist)
	 (clca connection-local-criteria-alist))
    (unwind-protect
	(progn
	  (make-directory tmp-name1)
          (should (file-directory-p tmp-name1))

	  ;; `local-variable' is buffer-local due to explicit setting.
	  ;; We need `with-no-warnings', because `defvar-local' is not
	  ;; called at toplevel.
	  (with-no-warnings (defvar-local local-variable 'buffer))
	  (with-temp-buffer
	    (should (eq local-variable 'buffer)))

	  ;; `local-variable' is connection-local due to Tramp.
	  (write-region "foo" nil tmp-name2)
	  (should (file-exists-p tmp-name2))
	  (connection-local-set-profile-variables
	   'local-variable-profile
	   '((local-variable . connect)))
	  (connection-local-set-profiles
	   `(:application tramp
	     :protocol ,(file-remote-p default-directory 'method)
	     :user ,(file-remote-p default-directory 'user)
	     :machine ,(file-remote-p default-directory 'host))
	   'local-variable-profile)
	  (with-current-buffer (find-file-noselect tmp-name2)
	    (should (eq local-variable 'connect))
	    (kill-buffer (current-buffer)))

	  ;; `local-variable' is still connection-local due to Tramp.
	  ;; `find-file-hook' overrides dir-local settings.
	  (write-region
	   "((nil . ((local-variable . dir))))" nil
	   (expand-file-name ".dir-locals.el" tmp-name1))
	  (should (file-exists-p (expand-file-name ".dir-locals.el" tmp-name1)))
	  (when (memq #'tramp-set-connection-local-variables-for-buffer
		      find-file-hook)
	    (with-current-buffer (find-file-noselect tmp-name2)
	      (should (eq local-variable 'connect))
	      (kill-buffer (current-buffer))))
	  ;; `local-variable' is dir-local due to existence of .dir-locals.el.
	  (let ((find-file-hook
		 (remq #'tramp-set-connection-local-variables-for-buffer
		       find-file-hook)))
	    (with-current-buffer (find-file-noselect tmp-name2)
	      (should (eq local-variable 'dir))
	      (kill-buffer (current-buffer))))

	  ;; `local-variable' is still connection-local due to Tramp.
	  ;; `find-file-hook' overrides dir-local settings.
	  (write-region
	   "-*- mode: comint; local-variable: file; -*-" nil tmp-name2)
          (should (file-exists-p tmp-name2))
	  (when (memq #'tramp-set-connection-local-variables-for-buffer
		      find-file-hook)
	    (with-current-buffer (find-file-noselect tmp-name2)
	      (should (eq local-variable 'connect))
	      (kill-buffer (current-buffer))))
	  ;; `local-variable' is file-local due to specifying as file variable.
	  (let ((find-file-hook
		 (remq #'tramp-set-connection-local-variables-for-buffer
		       find-file-hook)))
	    (with-current-buffer (find-file-noselect tmp-name2)
	      (should (eq local-variable 'file))
	      (kill-buffer (current-buffer)))))

      ;; Cleanup.
      (custom-set-variables
       `(connection-local-profile-alist ',clpa now)
       `(connection-local-criteria-alist ',clca now))
      (ignore-errors (delete-directory tmp-name1 'recursive)))))

(ert-deftest tramp-test34-explicit-shell-file-name ()
  "Check that connection-local `explicit-shell-file-name' is set."
  :tags '(:expensive-test :tramp-asynchronous-processes)
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-supports-processes-p))
  ;; FIXME: Make it work despite if ~/.emacs_powershell.
  (skip-unless (not (tramp--test-smb-p)))

  (let ((default-directory ert-remote-temporary-file-directory)
	explicit-shell-file-name kill-buffer-query-functions
	(clpa connection-local-profile-alist)
	(clca connection-local-criteria-alist))
    (unwind-protect
	(progn
	  (connection-local-set-profile-variables
	   'remote-sh
	   `((explicit-shell-file-name . ,(tramp--test-shell-file-name))
	     (explicit-sh-args
	      . (,(tramp--test-shell-command-switch) "echo foo"))))
	  (connection-local-set-profiles
	   `(:application tramp
	     :protocol ,(file-remote-p default-directory 'method)
	     :user ,(file-remote-p default-directory 'user)
	     :machine ,(file-remote-p default-directory 'host))
	   'remote-sh)
	  (put 'explicit-shell-file-name 'safe-local-variable #'identity)
	  (put 'explicit-sh-args 'safe-local-variable #'identity)

	  ;; Run `shell' interactively.  Since the default directory
	  ;; is remote, `explicit-shell-file-name' shall be set in
	  ;; order to avoid a question.  `explicit-sh-args' echoes the
	  ;; test data.
	  (with-current-buffer (get-buffer-create "*shell*")
	    (ignore-errors (kill-process (get-buffer-process (current-buffer))))
	    (should-not explicit-shell-file-name)
	    (call-interactively #'shell)
	    (with-timeout (10)
	      (while (accept-process-output
		      (get-buffer-process (current-buffer)) nil nil t)))
	    (should (string-match-p (rx bol "foo" eol) (buffer-string)))))

      ;; Cleanup.
      (put 'explicit-shell-file-name 'permanent-local nil)
      (custom-set-variables
       `(connection-local-profile-alist ',clpa now)
       `(connection-local-criteria-alist ',clca now))
      (kill-buffer "*shell*"))))

(ert-deftest tramp-test35-exec-path ()
  "Check `exec-path' and `executable-find'."
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-supports-processes-p))
  (skip-unless (tramp--test-supports-set-file-modes-p))

  (let ((tmp-name (tramp--test-make-temp-name))
	(default-directory ert-remote-temporary-file-directory))
    (unwind-protect
	(progn
	  (should (consp (exec-path)))
	  ;; Last element is the `exec-directory'.
	  (should
	   (string-equal
	    (car (last (exec-path)))
	    (file-remote-p default-directory 'localname)))
	  ;; The shell "sh" shall always exist.
	  (should (executable-find "sh" 'remote))

	  ;; Since the last element in `exec-path' is the current
	  ;; directory, an executable file in that directory will be
	  ;; found.
	  (write-region "foo" nil tmp-name)
	  (should (file-exists-p tmp-name))

	  (set-file-modes tmp-name #o777)
	  (should (file-executable-p tmp-name))
	  (should
	   (string-equal
	    (executable-find (file-name-nondirectory tmp-name) 'remote)
	    (file-remote-p tmp-name 'localname)))
	  (should-not
	   (executable-find
	    (concat (file-name-nondirectory tmp-name) "foo") 'remote)))

      ;; Cleanup.
      (ignore-errors (delete-file tmp-name)))))

(tramp--test-deftest-direct-async-process tramp-test35-exec-path)

;; This test is inspired by Bug#33781.
(ert-deftest tramp-test35-remote-path ()
  "Check loooong `tramp-remote-path'."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-supports-environment-variables-p))

  (let* ((tmp-name1 (tramp--test-make-temp-name))
	 (default-directory ert-remote-temporary-file-directory)
         (orig-exec-path (exec-path))
         (tramp-remote-path tramp-remote-path)
	 (orig-tramp-remote-path tramp-remote-path)
	 tmp-name2 path)
    ;; The "flatpak" method modifies `tramp-remote-path'.
    (skip-unless (not (tramp-compat-connection-local-p tramp-remote-path)))
    (unwind-protect
	(progn
          ;; Non existing directories are removed.
          (setq tramp-remote-path
                (cons (file-remote-p tmp-name1 'localname) tramp-remote-path))
          (tramp-cleanup-connection tramp-test-vec 'keep-debug 'keep-password)
          (should (equal (exec-path) orig-exec-path))
          (setq tramp-remote-path orig-tramp-remote-path)

          ;; Double entries are removed.
          (setq tramp-remote-path (append '("/" "/") tramp-remote-path))
          (tramp-cleanup-connection tramp-test-vec 'keep-debug 'keep-password)
          (should (equal (exec-path) (cons "/" orig-exec-path)))
          (setq tramp-remote-path orig-tramp-remote-path)

          ;; We make a super long `tramp-remote-path'.
	  (unless (tramp--test-container-oob-p)
            (make-directory tmp-name1)
            (should (file-directory-p tmp-name1))
            (while (length< (string-join orig-exec-path ":") 5000)
              (let ((dir (make-temp-file
			  (file-name-as-directory tmp-name1) 'dir)))
		(should (file-directory-p dir))
		(setq tramp-remote-path
                      (append
		       tramp-remote-path `(,(file-remote-p dir 'localname)))
                      orig-exec-path
                      (append
		       (butlast orig-exec-path)
		       `(,(file-remote-p dir 'localname))
		       (last orig-exec-path)))))
            (tramp-cleanup-connection tramp-test-vec 'keep-debug 'keep-password)
            (should (equal (exec-path) orig-exec-path))
	    ;; Ignore trailing newline.
	    (setq path (substring (shell-command-to-string "echo $PATH") nil -1))
	    ;; The shell doesn't handle such long strings.
	    (unless (length>
		     path
		     (tramp-get-connection-property
		      tramp-test-vec "pipe-buf" 4096))
	      ;; The last element of `exec-path' is `exec-directory'.
              (should
	       (string-equal path (string-join (butlast orig-exec-path) ":"))))
	    ;; The shell "sh" shall always exist.
	    (should (executable-find "sh" 'remote))

	    ;; Since the last element in `exec-path' is the current
	    ;; directory, an executable file in that directory will be
	    ;; found.
	    (setq tmp-name2
		  (expand-file-name
		   "foo"
		   (concat (file-remote-p default-directory)
			   (car (last orig-exec-path 2)))))
	    (write-region "foo" nil tmp-name2)
	    (should (file-exists-p tmp-name2))

	    (set-file-modes tmp-name2 #o777)
	    (should (file-executable-p tmp-name2))
	    (should
	     (string-equal
	      (executable-find (file-name-nondirectory tmp-name2) 'remote)
	      (file-remote-p tmp-name2 'localname)))
	    (should-not
	     (executable-find
	      (concat (file-name-nondirectory tmp-name2) "foo") 'remote))))

      ;; Cleanup.
      (ignore-errors (delete-directory tmp-name1 'recursive))
      (tramp-cleanup-connection tramp-test-vec 'keep-debug 'keep-password)
      (setq tramp-remote-path orig-tramp-remote-path))))

(tramp--test-deftest-direct-async-process tramp-test35-remote-path)

(ert-deftest tramp-test36-vc-registered ()
  "Check `vc-registered'."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-supports-processes-p))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    ;; We must use `file-truename' for the temporary directory, in
    ;; order to establish the connection prior running an asynchronous
    ;; process.
    (let* ((default-directory
	     (file-truename ert-remote-temporary-file-directory))
	   (tmp-name1 (tramp--test-make-temp-name nil quoted))
	   (tmp-name2 (expand-file-name "foo" tmp-name1))
	   (tramp-remote-process-environment tramp-remote-process-environment)
	   ;; Suppress nasty messages.
           (inhibit-message (not (ignore-errors (edebug-mode))))
	   (vc-handled-backends
	    (cond
	     ((executable-find vc-git-program 'remote) '(Git))
	     ((executable-find vc-hg-program 'remote) '(Hg))
	     ((executable-find vc-bzr-program 'remote)
	      (setq tramp-remote-process-environment
		    (cons (format "BZR_HOME=%s"
				  (file-remote-p tmp-name1 'localname))
			  tramp-remote-process-environment))
	      ;; We must force a reconnect, in order to activate $BZR_HOME.
	      (tramp-cleanup-connection
	       tramp-test-vec 'keep-debug 'keep-password)
	      '(Bzr))
	     (t nil))))
      (skip-unless vc-handled-backends)
      (unless quoted (tramp--test-message "%s" vc-handled-backends))

      (unwind-protect
	  (progn
	    (make-directory tmp-name1)
	    (write-region "foo" nil tmp-name2)
	    (should (file-directory-p tmp-name1))
	    (should (file-exists-p tmp-name2))
	    (should-not (vc-registered tmp-name1))
	    (should-not (vc-registered tmp-name2))

	    (let ((default-directory tmp-name1))
	      ;; Create empty repository, and register the file.
	      ;; Sometimes, creation of repository fails (bzr!); we
	      ;; skip the test then.
	      (condition-case nil
		  (vc-create-repo (car vc-handled-backends))
		(error (ert-skip "`vc-create-repo' not supported")))
	      ;; The structure of VC-FILESET is not documented.  Let's
	      ;; hope it won't change.
	      (vc-register
	       (list (car vc-handled-backends)
		     (list (file-name-nondirectory tmp-name2))))
	      ;; vc-git uses an own process sentinel, Tramp's sentinel
	      ;; for flushing the cache isn't used.
	      (dired-uncache (concat (file-remote-p default-directory) "/"))
	      (should (vc-registered (file-name-nondirectory tmp-name2)))))

	;; Cleanup.
	(ignore-errors (delete-directory tmp-name1 'recursive))))))

(ert-deftest tramp-test37-make-auto-save-file-name ()
  "Check `make-auto-save-file-name'."
  (skip-unless (tramp--test-enabled))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let ((tmp-name1 (tramp--test-make-temp-name nil quoted))
	  (tmp-name2 (tramp--test-make-temp-name nil quoted))
	  tramp-allow-unsafe-temporary-files)

      (unwind-protect
	  (progn
	    ;; Use default `auto-save-file-name-transforms' mechanism.
	    ;; It isn't prepared for `separate' syntax.
	    (unless (eq tramp-syntax 'separate)
	      (let (tramp-auto-save-directory)
		(with-temp-buffer
		  (setq buffer-file-name tmp-name1)
		  (should
		   (string-equal
		    (make-auto-save-file-name)
		    ;; This is taken from original `make-auto-save-file-name'.
		    ;; We call `convert-standard-filename', because on
		    ;; MS Windows the (local) colons must be replaced by
		    ;; exclamation marks.
		    (convert-standard-filename
		     (expand-file-name
		      (format
		       "#%s#"
		       (subst-char-in-string
			?/ ?! (replace-regexp-in-string "!" "!!" tmp-name1)))
		      temporary-file-directory)))))))

	    ;; No mapping.
	    (let (tramp-auto-save-directory auto-save-file-name-transforms)
	      (with-temp-buffer
		(setq buffer-file-name tmp-name1)
		(should
		 (string-equal
		  (make-auto-save-file-name)
		  (funcall
		   (if quoted #'file-name-quote #'identity)
		   (expand-file-name
		    (format "#%s#" (file-name-nondirectory tmp-name1))
		    ert-remote-temporary-file-directory))))))

	    ;; Use default `tramp-auto-save-directory' mechanism.
	    ;; Ange-FTP doesn't care.
	    (unless (tramp--test-ange-ftp-p)
	      (let ((tramp-auto-save-directory tmp-name2))
		(with-temp-buffer
		  (setq buffer-file-name tmp-name1)
		  (should
		   (string-equal
		    (make-auto-save-file-name)
		    ;; This is taken from Tramp.
		    (expand-file-name
		     (format
		      "#%s#"
		      (tramp-subst-strs-in-string
		       '(("_" . "|")
			 ("/" . "_a")
			 (":" . "_b")
			 ("|" . "__")
			 ("[" . "_l")
			 ("]" . "_r"))
		       (file-name-unquote tmp-name1)))
		     tmp-name2)))
		  (should (file-directory-p tmp-name2)))))

	    ;; Relative file names shall work, too.  Ange-FTP doesn't care.
	    (unless (tramp--test-ange-ftp-p)
	      (let ((tramp-auto-save-directory "."))
		(with-temp-buffer
		  (setq buffer-file-name tmp-name1
			default-directory tmp-name2)
		  (should
		   (string-equal
		    (make-auto-save-file-name)
		    ;; This is taken from Tramp.
		    (expand-file-name
		     (format
		      "#%s#"
		      (tramp-subst-strs-in-string
		       '(("_" . "|")
			 ("/" . "_a")
			 (":" . "_b")
			 ("|" . "__")
			 ("[" . "_l")
			 ("]" . "_r"))
		       (file-name-unquote tmp-name1)))
		     tmp-name2)))
		  (should (file-directory-p tmp-name2)))))

	    ;; Create temporary file.  This shall check for sensible
	    ;; files, owned by root.
	    (let ((tramp-auto-save-directory temporary-file-directory))
	      (write-region "foo" nil tmp-name1)
	      (when (zerop (or (file-attribute-user-id
				(file-attributes tmp-name1))
			       tramp-unknown-id-integer))
		(with-temp-buffer
		  (setq buffer-file-name tmp-name1)
		  (tramp-cleanup-connection
		   tramp-test-vec 'keep-debug 'keep-password)
		  (let ((tramp-allow-unsafe-temporary-files t))
		    (should (stringp (make-auto-save-file-name))))
		  (tramp-cleanup-connection
		   tramp-test-vec 'keep-debug 'keep-password)
		  (cl-letf (((symbol-function #'yes-or-no-p) #'ignore))
		    (should-error
		     (make-auto-save-file-name)
		     :type 'file-error))
		  (tramp-cleanup-connection
		   tramp-test-vec 'keep-debug 'keep-password)
		  (cl-letf (((symbol-function #'yes-or-no-p) #'always))
		    (should (stringp (make-auto-save-file-name))))))))

	;; Cleanup.
	(ignore-errors (delete-file tmp-name1))
	(ignore-errors (delete-directory tmp-name2 'recursive))
	(tramp-cleanup-connection tramp-test-vec 'keep-debug 'keep-password)))))

(ert-deftest tramp-test38-find-backup-file-name ()
  "Check `find-backup-file-name'."
  (skip-unless (tramp--test-enabled))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let ((tmp-name1 (tramp--test-make-temp-name nil quoted))
	  (tmp-name2 (tramp--test-make-temp-name nil quoted))
	  (ange-ftp-make-backup-files t)
	  tramp-allow-unsafe-temporary-files
	  ;; These settings are not used by Tramp, so we ignore them.
	  version-control delete-old-versions
	  (kept-old-versions (default-toplevel-value 'kept-old-versions))
	  (kept-new-versions (default-toplevel-value 'kept-new-versions)))

      (unwind-protect
	  ;; Use default `backup-directory-alist' mechanism.
	  (let (backup-directory-alist tramp-backup-directory-alist)
	    (should
	     (equal
	      (find-backup-file-name tmp-name1)
	      (list
	       (funcall
		(if quoted #'file-name-quote #'identity)
		(expand-file-name
		 (format "%s~" (file-name-nondirectory tmp-name1))
		 ert-remote-temporary-file-directory))))))

	;; Cleanup.  Nothing to do yet.
	nil)

      (unwind-protect
	  ;; Map `backup-directory-alist'.
	  (let ((backup-directory-alist `(("." . ,tmp-name2)))
		tramp-backup-directory-alist)
	    (should
	     (equal
	      (find-backup-file-name tmp-name1)
	      (list
	       (funcall
		(if quoted #'file-name-quote #'identity)
		(expand-file-name
		 (format
		  "%s~"
		  ;; This is taken from `make-backup-file-name-1'.  We
		  ;; call `convert-standard-filename', because on MS
		  ;; Windows the (local) colons must be replaced by
		  ;; exclamation marks.
		  (subst-char-in-string
		   ?/ ?!
		   (replace-regexp-in-string
		    "!" "!!" (convert-standard-filename tmp-name1))))
		 tmp-name2)))))
	    ;; The backup directory is created.
	    (should (file-directory-p tmp-name2)))

	;; Cleanup.
	(ignore-errors (delete-directory tmp-name2 'recursive)))

      (unwind-protect
	  ;; Map `tramp-backup-directory-alist'.  Ange-FTP doesn't care.
	  (unless (tramp--test-ange-ftp-p)
	    (let ((tramp-backup-directory-alist `(("." . ,tmp-name2)))
		  backup-directory-alist)
	      (should
	       (equal
		(find-backup-file-name tmp-name1)
		(list
		 (funcall
		  (if quoted #'file-name-quote #'identity)
		  (expand-file-name
		   (format
		    "%s~"
		    ;; This is taken from `make-backup-file-name-1'.
		    ;; We call `convert-standard-filename', because on
		    ;; MS Windows the (local) colons must be replaced
		    ;; by exclamation marks.
		    (subst-char-in-string
		     ?/ ?!
		     (replace-regexp-in-string
		      "!" "!!" (convert-standard-filename tmp-name1))))
		   tmp-name2)))))
	      ;; The backup directory is created.
	      (should (file-directory-p tmp-name2))))

	;; Cleanup.
	(ignore-errors (delete-directory tmp-name2 'recursive)))

      (unwind-protect
	  ;; Map `tramp-backup-directory-alist' with local file name.
	  ;; Ange-FTP doesn't care.
	  (unless (tramp--test-ange-ftp-p)
	    (let ((tramp-backup-directory-alist
		   `(("." . ,(file-remote-p tmp-name2 'localname))))
		  backup-directory-alist)
	      (should
	       (equal
		(find-backup-file-name tmp-name1)
		(list
		 (funcall
		  (if quoted #'file-name-quote #'identity)
		  (expand-file-name
		   (format
		    "%s~"
		    ;; This is taken from `make-backup-file-name-1'.
		    ;; We call `convert-standard-filename', because on
		    ;; MS Windows the (local) colons must be replaced
		    ;; by exclamation marks.
		    (subst-char-in-string
		     ?/ ?!
		     (replace-regexp-in-string
		      "!" "!!" (convert-standard-filename tmp-name1))))
		   tmp-name2)))))
	      ;; The backup directory is created.
	      (should (file-directory-p tmp-name2))))

	;; Cleanup.
	(ignore-errors (delete-directory tmp-name2 'recursive)))

      (unwind-protect
	  ;; Create temporary file.  This shall check for sensible
	  ;; files, owned by root.
	  (let ((backup-directory-alist `(("." . ,temporary-file-directory)))
		tramp-backup-directory-alist)
	    (write-region "foo" nil tmp-name1)
	    (when (zerop (or (file-attribute-user-id (file-attributes tmp-name1))
			     tramp-unknown-id-integer))
	      (tramp-cleanup-connection
	       tramp-test-vec 'keep-debug 'keep-password)
	      (let ((tramp-allow-unsafe-temporary-files t))
		(should (stringp (car (find-backup-file-name tmp-name1)))))
	      (tramp-cleanup-connection
	       tramp-test-vec 'keep-debug 'keep-password)
	      (cl-letf (((symbol-function #'yes-or-no-p) #'ignore))
		(should-error
		 (find-backup-file-name tmp-name1)
		 :type 'file-error))
	      (tramp-cleanup-connection
	       tramp-test-vec 'keep-debug 'keep-password)
	      (cl-letf (((symbol-function #'yes-or-no-p) #'always))
		(should (stringp (car (find-backup-file-name tmp-name1)))))))

	;; Cleanup.
	(ignore-errors (delete-file tmp-name1))
	(tramp-cleanup-connection tramp-test-vec 'keep-debug 'keep-password)))))

(ert-deftest tramp-test39-make-lock-file-name ()
  "Check `make-lock-file-name', `lock-file', `unlock-file' and `file-locked-p'."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-enabled))
  (skip-unless (not (tramp--test-ange-ftp-p)))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (let ((tmp-name1 (tramp--test-make-temp-name nil quoted))
	  (tmp-name2 (tramp--test-make-temp-name nil quoted))
	  (remote-file-name-inhibit-cache t)
	  (remote-file-name-inhibit-locks nil)
	  (create-lockfiles t)
	  tramp-allow-unsafe-temporary-files
          (inhibit-message (not (ignore-errors (edebug-mode))))
	  ;; tramp-rclone.el and tramp-sshfs.el cache the mounted files.
	  (tramp-fuse-unmount-on-cleanup t)
          auto-save-default
	  noninteractive)

      (unwind-protect
	  (progn
	    ;; A simple file lock.
	    (should-not (file-locked-p tmp-name1))
	    (lock-file tmp-name1)
	    (should (eq (file-locked-p tmp-name1) t))

	    ;; If it is locked already, nothing changes.
	    (lock-file tmp-name1)
	    (should (eq (file-locked-p tmp-name1) t))

            ;; `save-buffer' removes the lock.
            (with-temp-buffer
              (set-visited-file-name tmp-name1)
              (insert "foo")
	      (should (buffer-modified-p))
              (save-buffer)
	      (should-not (buffer-modified-p)))
            (should-not (file-locked-p tmp-name1))

            ;; `kill-buffer' removes the lock.
	    ;; `kill-buffer--possibly-save' exists since Emacs 29.1.
	    (when (fboundp 'kill-buffer--possibly-save)
	      (lock-file tmp-name1)
	      (should (eq (file-locked-p tmp-name1) t))
              (with-temp-buffer
		(set-visited-file-name tmp-name1)
		(insert "foo")
		(should (buffer-modified-p))
		;; Modifying `read-from-minibuffer' doesn't work on MS Windows.
		(cl-letf (((symbol-function #'kill-buffer--possibly-save)
			   #'always))
                  (kill-buffer)))
	      (should-not (file-locked-p tmp-name1)))

            ;; `kill-buffer' should not remove the lock when the
            ;; connection is broken.  See Bug#61663.
	    ;; `kill-buffer--possibly-save' exists since Emacs 29.1.
	    (when (fboundp 'kill-buffer--possibly-save)
	      (lock-file tmp-name1)
	      (should (eq (file-locked-p tmp-name1) t))
              (with-temp-buffer
		(set-visited-file-name tmp-name1)
		(insert "foo")
		(should (buffer-modified-p))
		(tramp-cleanup-connection
		 tramp-test-vec 'keep-debug 'keep-password)
		;; Modifying `read-from-minibuffer' doesn't work on MS Windows.
		(cl-letf (((symbol-function #'kill-buffer--possibly-save)
			   #'always))
                  (kill-buffer)))
	      ;; A new connection changes process id, and also the
	      ;; lock file contents.  But it still exists.
	      (tramp-cleanup-connection tramp-test-vec 'keep-debug 'keep-password)
	      (should (stringp (file-locked-p tmp-name1))))

	    ;; When `remote-file-name-inhibit-locks' is set, nothing happens.
	    (tramp-cleanup-connection tramp-test-vec 'keep-debug 'keep-password)
	    (let ((remote-file-name-inhibit-locks t))
	      (lock-file tmp-name1)
	      (should-not (file-locked-p tmp-name1)))

	    ;; When `lock-file-name-transforms' is set, another lock
	    ;; file is used.
	    (tramp-cleanup-connection tramp-test-vec 'keep-debug 'keep-password)
	    (let ((lock-file-name-transforms `((,(rx (* nonl)) ,tmp-name2))))
	      (should
	       (string-equal
		 (make-lock-file-name tmp-name1)
		 (make-lock-file-name tmp-name2)))
	      (lock-file tmp-name1)
	      (should (eq (file-locked-p tmp-name1) t))
	      (unlock-file tmp-name1)
	      (should-not (file-locked-p tmp-name1)))

	    ;; Steal the file lock.
	    (tramp-cleanup-connection tramp-test-vec 'keep-debug 'keep-password)
	    ;; Modifying `read-char' doesn't work on MS Windows.
	    (cl-letf (((symbol-function #'ask-user-about-lock) #'always))
	      (lock-file tmp-name1))
	    (should (eq (file-locked-p tmp-name1) t))

	    ;; Ignore the file lock.
	    (tramp-cleanup-connection tramp-test-vec 'keep-debug 'keep-password)
	    ;; Modifying `read-char' doesn't work on MS Windows.
	    (cl-letf (((symbol-function #'ask-user-about-lock) #'ignore))
	      (lock-file tmp-name1))
	    (should (stringp (file-locked-p tmp-name1)))

	    ;; Quit the file lock machinery.  There are problems with
	    ;; "sftp" and "podman", so we test on Emacs 29.1 only.
	    (when (tramp--test-emacs29-p )
	      (tramp-cleanup-connection tramp-test-vec 'keep-debug 'keep-password)
	      ;; Modifying `read-char' doesn't work on MS Windows.
	      (cl-letf (((symbol-function #'ask-user-about-lock)
			 (lambda (&rest args)
			   (signal 'file-locked args))))
		(should-error
		 (lock-file tmp-name1)
		 :type 'file-locked)
		;; The same for `write-region'.
		(should-error
		 (write-region "foo" nil tmp-name1)
		 :type 'file-locked)
		(should-error
		 (write-region "foo" nil tmp-name1 nil nil tmp-name1)
		 :type 'file-locked)
		;; The same for `set-visited-file-name'.
		(with-temp-buffer
	          (should-error
                   (set-visited-file-name tmp-name1)
		   :type 'file-locked))))
	    (should (stringp (file-locked-p tmp-name1))))

	;; Cleanup.
	(ignore-errors (delete-file tmp-name1))
	(unlock-file tmp-name1)
	(unlock-file tmp-name2)
	(should-not (file-locked-p tmp-name1))
	(should-not (file-locked-p tmp-name2)))

      (unwind-protect
	  ;; Create temporary file.  This shall check for sensible
	  ;; files, owned by root.
	  (let ((lock-file-name-transforms auto-save-file-name-transforms))
	    (write-region "foo" nil tmp-name1)
	    (when (zerop (or (file-attribute-user-id (file-attributes tmp-name1))
			     tramp-unknown-id-integer))
	      (tramp-cleanup-connection
	       tramp-test-vec 'keep-debug 'keep-password)
	      (cl-letf (((symbol-function #'yes-or-no-p) #'ignore))
		(should-error
		 (write-region "foo" nil tmp-name1)
		 :type 'file-error))
	      (tramp-cleanup-connection
	       tramp-test-vec 'keep-debug 'keep-password)
	      (cl-letf (((symbol-function #'yes-or-no-p) #'always))
		(write-region "foo" nil tmp-name1))))

	;; Cleanup.
	(ignore-errors (delete-file tmp-name1))
	(tramp-cleanup-connection tramp-test-vec 'keep-debug 'keep-password)))))

(ert-deftest tramp-test39-detect-external-change ()
  "Check that an external file modification is reported."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-enabled))
  (skip-unless (not (tramp--test-ange-ftp-p)))

  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    (dolist (create-lockfiles '(nil t))
      (let ((tmp-name (tramp--test-make-temp-name nil quoted))
	    (remote-file-name-inhibit-cache t)
	    (remote-file-name-inhibit-locks nil)
	    tramp-allow-unsafe-temporary-files
            (inhibit-message (not (ignore-errors (edebug-mode))))
	    ;; tramp-rclone.el and tramp-sshfs.el cache the mounted files.
	    (tramp-fuse-unmount-on-cleanup t)
            auto-save-default
	    (backup-inhibited t)
	    noninteractive)
        (with-temp-buffer
          (unwind-protect
	      (progn
		(setq buffer-file-name tmp-name
		      buffer-file-truename tmp-name)
		(insert "foo")
		;; Bug#53207: with `create-lockfiles' nil, saving the
		;; buffer results in a prompt.
		(cl-letf (((symbol-function #'read-from-minibuffer)
			   (lambda (&rest _)
			     (ert-fail "Test failed unexpectedly"))))
		  (should (buffer-modified-p))
		  (save-buffer)
		  (should-not (buffer-modified-p)))
		(should-not (file-locked-p tmp-name))

		;; For local files, just changing the file
		;; modification on disk doesn't hurt, because file
		;; contents in buffer and on disk are equal.  For
		;; remote files, file contents is not compared.  We
		;; mock an older modification time in buffer, because
		;; Tramp regards modification times equal if they
		;; differ for less than 2 seconds.
		(set-visited-file-modtime (time-add (current-time) -60))
		;; Some Tramp methods cannot check the file
		;; modification time properly, for them it doesn't
		;; make sense to test.
		(when (not (verify-visited-file-modtime))
		  (cl-letf (((symbol-function #'read-char-choice)
			     (lambda (prompt &rest _) (message "%s" prompt) ?y)))
		    (ert-with-message-capture captured-messages
		      (insert "bar")
		      (when create-lockfiles
			(should (string-match-p
				 (rx-to-string
				  `(: bol
				      ,(if (tramp--test-crypt-p)
					   '(+ nonl)
					 (file-name-nondirectory tmp-name))
				      " changed on disk; really edit the buffer?"))
				 captured-messages))
			(should (file-locked-p tmp-name)))))

		  ;; `save-buffer' removes the file lock.
		  (cl-letf (((symbol-function #'yes-or-no-p) #'always)
			    ((symbol-function #'read-char-choice)
			     (lambda (&rest _) ?y)))
		    (should (buffer-modified-p))
		    (save-buffer)
		    (should-not (buffer-modified-p)))
		  (should-not (file-locked-p tmp-name))))

	    ;; Cleanup.
	    (set-buffer-modified-p nil)
	    (ignore-errors (delete-file tmp-name))
	    (tramp-cleanup-connection
	     tramp-test-vec 'keep-debug 'keep-password)))))))

(ert-deftest tramp-test40-make-nearby-temp-file ()
  "Check `make-nearby-temp-file' and `temporary-file-directory'."
  (skip-unless (tramp--test-enabled))
  (skip-unless (not (tramp--test-ange-ftp-p)))

  (let ((default-directory ert-remote-temporary-file-directory)
	tmp-file)
    ;; The remote host shall know a temporary file directory.
    (should (stringp (temporary-file-directory)))
    (should
     (string-equal
      (file-remote-p default-directory)
      (file-remote-p (temporary-file-directory))))

    ;; The temporary file shall be located on the remote host.
    (setq tmp-file (make-nearby-temp-file tramp-test-name-prefix))
    (should (file-exists-p tmp-file))
    (should (file-regular-p tmp-file))
    (should
     (string-equal
      (file-remote-p default-directory)
      (file-remote-p tmp-file)))
    (delete-file tmp-file)
    (should-not (file-exists-p tmp-file))

    (setq tmp-file (make-nearby-temp-file tramp-test-name-prefix 'dir))
    (should (file-exists-p tmp-file))
    (should (file-directory-p tmp-file))
    (delete-directory tmp-file)
    (should-not (file-exists-p tmp-file))))

(defun tramp--test-emacs29-p ()
  "Check for Emacs version >= 29.1.
Some semantics has been changed for there, without new functions
or variables, so we check the Emacs version directly."
  (>= emacs-major-version 29))

(defun tramp--test-emacs31-p ()
  "Check for Emacs version >= 31.1.
Some semantics has been changed for there, without new functions
or variables, so we check the Emacs version directly."
  (>= emacs-major-version 31))

(defun tramp--test-adb-p ()
  "Check, whether the remote host runs Android.
This requires restrictions of file name syntax."
  (tramp-adb-file-name-p ert-remote-temporary-file-directory))

(defun tramp--test-ange-ftp-p ()
  "Check, whether Ange-FTP is used."
  (eq
   (tramp-find-foreign-file-name-handler tramp-test-vec)
   #'tramp-ftp-file-name-handler))

(defun tramp--test-asynchronous-processes-p ()
  "Whether asynchronous processes tests are run.
This is used in tests which we don't want to tag
`:tramp-asynchronous-processes' completely."
  (and
   (ert-select-tests
    (ert--stats-selector ert--current-run-stats)
    (list (make-ert-test :name (ert-test-name (ert-running-test))
                         :body nil :tags '(:tramp-asynchronous-processes))))
   ;; tramp-adb.el cannot apply multibyte commands.
   (not (and (tramp--test-adb-p)
	     (string-match-p (rx multibyte) default-directory)))))

(defun tramp--test-box-p ()
  "Check, whether the toolbox or distrobox method is used.
This does not support `tramp-test45-asynchronous-requests'."
  (string-match-p
   (rx bol (| "toolbox" "distrobox") eol)
   (file-remote-p ert-remote-temporary-file-directory 'method)))

(defun tramp--test-container-p ()
  "Check, whether a container method is used.
This does not support some special file names."
  (string-match-p
   (rx bol (| "docker" "podman" "kubernetes" "apptainer" "run0" "nspawn"))
   (file-remote-p ert-remote-temporary-file-directory 'method)))

(defun tramp--test-container-oob-p ()
  "Check, whether the dockercp or podmancp method is used.
They does not support wildcard copy."
  (string-match-p
   (rx bol (| "dockercp" "podmancp") eol)
   (file-remote-p ert-remote-temporary-file-directory 'method)))

(defun tramp--test-crypt-p ()
  "Check, whether the remote directory is encrypted."
  (tramp-crypt-file-name-p ert-remote-temporary-file-directory))

(defun tramp--test-expensive-test-p ()
  "Whether expensive tests are run.
This is used in tests which we don't want to tag `:expensive'
completely."
  (ert-select-tests
   (ert--stats-selector ert--current-run-stats)
   (list (make-ert-test :name (ert-test-name (ert-running-test))
                        :body nil :tags '(:expensive-test)))))

(defun tramp--test-ftp-p ()
  "Check, whether an FTP-like method is used.
This does not support globbing characters in file names (yet)."
  ;; Globbing characters are ??, ?* and ?\[.
  (string-suffix-p
   "ftp" (file-remote-p ert-remote-temporary-file-directory 'method)))

(defun tramp--test-fuse-p ()
  "Check, whether an FUSE file system isused."
  (or (tramp--test-rclone-p) (tramp--test-sshfs-p)))

(defun tramp--test-gdrive-p ()
  "Check, whether the gdrive method is used."
  (string-equal
   "gdrive" (file-remote-p ert-remote-temporary-file-directory 'method)))

(defun tramp--test-gvfs-p (&optional method)
  "Check, whether the remote host runs a GVFS based method.
This requires restrictions of file name syntax.
If optional METHOD is given, it is checked first."
  (or (member method tramp-gvfs-methods)
      (tramp-gvfs-file-name-p ert-remote-temporary-file-directory)))

(defun tramp--test-hpux-p ()
  "Check, whether the remote host runs HP-UX.
Several special characters do not work properly there."
  ;; We must refill the cache.  `file-truename' does it.
  (file-truename ert-remote-temporary-file-directory)
  (ignore-errors (tramp-check-remote-uname tramp-test-vec (rx bol "HP-UX"))))

(defun tramp--test-ksh-p ()
  "Check, whether the remote shell is ksh.
ksh93 makes some strange conversions of non-latin characters into
a $'' syntax."
  ;; We must refill the cache.  `file-truename' does it.
  (file-truename ert-remote-temporary-file-directory)
  (string-suffix-p
   "ksh"
   (tramp-get-connection-property tramp-test-vec "remote-shell" "")))

(defun tramp--test-kubernetes-p ()
  "Check, whether the kubernetes method is used."
  (string-equal
   "kubernetes" (file-remote-p ert-remote-temporary-file-directory 'method)))

(defun tramp--test-macos-p ()
  "Check, whether the remote host runs macOS."
  ;; We must refill the cache.  `file-truename' does it.
  (file-truename ert-remote-temporary-file-directory)
  (ignore-errors (tramp-check-remote-uname tramp-test-vec "Darwin")))

(defun tramp--test-mock-p ()
  "Check, whether the mock method is used.
This does not support external Emacs calls."
  (string-equal
   "mock" (file-remote-p ert-remote-temporary-file-directory 'method)))

(defun tramp--test-netbsd-p ()
  "Check, whether the remote host runs NetBSD."
  ;; We must refill the cache.  `file-truename' does it.
  (file-truename ert-remote-temporary-file-directory)
  (ignore-errors (tramp-check-remote-uname tramp-test-vec "NetBSD")))

(defun tramp--test-openbsd-p ()
  "Check, whether the remote host runs OpenBSD."
  ;; We must refill the cache.  `file-truename' does it.
  (file-truename ert-remote-temporary-file-directory)
  (ignore-errors (tramp-check-remote-uname tramp-test-vec "OpenBSD")))

(defun tramp--test-out-of-band-p ()
  "Check, whether an out-of-band method is used."
  (tramp-method-out-of-band-p tramp-test-vec 1))

(defun tramp--test-putty-p ()
  "Check, whether the method uses PuTTY.
This does not support connection share for more than two connections."
  (member
   (file-remote-p ert-remote-temporary-file-directory 'method)
   '("plink" "plinkx" "pscp" "psftp")))

(defun tramp--test-rclone-p ()
  "Check, whether the remote host is offered by rclone.
This requires restrictions of file name syntax."
  (tramp-rclone-file-name-p ert-remote-temporary-file-directory))

(defun tramp--test-rsync-p ()
  "Check, whether the rsync method is used.
This does not support special file names."
  (string-equal
   "rsync" (file-remote-p ert-remote-temporary-file-directory 'method)))

(defun tramp--test-scp-p ()
  "Check, whether an scp method is used.
This does not support quoted special characters in recent sshd
implementations."
  ;; Detected with OpenSSH_9.9p1.
  (member
   (file-remote-p ert-remote-temporary-file-directory 'method)
   '("pscp" "scp" "scpx")))

(defun tramp--test-sh-p ()
  "Check, whether the remote host runs a based method from tramp-sh.el."
  (tramp-sh-file-name-handler-p tramp-test-vec))

(defun tramp--test-sh-no-ls--dired-p ()
  "Check, whether the remote host runs a based method from tramp-sh.el.
Additionally, ls does not support \"--dired\"."
  (and (tramp--test-sh-p)
       (with-temp-buffer
	 ;; We must refill the cache.  `insert-directory' does it.
	 ;; This fails for tramp-crypt.el, so we ignore that.
	 (ignore-errors
	   (insert-directory ert-remote-temporary-file-directory "-al"))
	 (not (tramp-get-connection-property tramp-test-vec "ls--dired")))))

(defun tramp--test-share-p ()
  "Check, whether the method needs a share."
  (and (tramp--test-gvfs-p)
       (string-match-p
	(rx bol (| "afp" (: "dav" (? "s")) "smb") eol)
	(file-remote-p ert-remote-temporary-file-directory 'method))))

(defun tramp--test-sshfs-p ()
  "Check, whether the remote host is offered by sshfs.
This requires restrictions of file name syntax."
  (tramp-sshfs-file-name-p ert-remote-temporary-file-directory))

(defun tramp--test-sudoedit-p ()
  "Check, whether the sudoedit method is used."
  (tramp-sudoedit-file-name-p ert-remote-temporary-file-directory))

(defun tramp--test-telnet-p ()
  "Check, whether the telnet method is used.
This does not support special file names."
  (string-equal
   "telnet" (file-remote-p ert-remote-temporary-file-directory 'method)))

(defun tramp--test-windows-nt-p ()
  "Check, whether the locale host runs MS Windows."
  (eq system-type 'windows-nt))

(defun tramp--test-windows-nt-and-out-of-band-p ()
  "Check, whether the locale host runs MS Windows and an out-of-band method.
This does not support utf8 based file transfer."
  (and (tramp--test-windows-nt-p)
       (tramp--test-out-of-band-p)))

(defun tramp--test-windows-nt-or-smb-p ()
  "Check, whether the locale or remote host runs MS Windows.
This requires restrictions of file name syntax."
  (or (tramp--test-windows-nt-p)
      (tramp--test-smb-p)))

(defun tramp--test-smb-p ()
  "Check, whether the locale or remote host runs MS Windows.
This requires restrictions of file name syntax."
  (tramp-smb-file-name-p ert-remote-temporary-file-directory))

(defun tramp--test-supports-processes-p ()
  "Return whether the method under test supports external processes."
  (unless (tramp--test-crypt-p)
    (or (tramp--test-adb-p) (tramp--test-sh-p) (tramp--test-sshfs-p)
	(and (tramp--test-smb-p)
	     (ignore-errors
	       (file-writable-p
		(file-name-concat
		 (file-remote-p ert-remote-temporary-file-directory)
		 ;; We check a directory on the "ADMIN$" share.
		 "ADMIN$" "Boot")))))))

(defun tramp--test-supports-set-file-modes-p ()
  "Return whether the method under test supports setting file modes."
  ;; "smb" does not unless the SMB server supports "posix" extensions.
  ;; "adb" does not unless the Android device is rooted.
  (or (tramp--test-sh-p) (tramp--test-sshfs-p) (tramp--test-sudoedit-p)
      ;; Not all tramp-gvfs.el methods support changing the file mode.
      (and
       (tramp--test-gvfs-p)
       (string-suffix-p
	"ftp" (file-remote-p ert-remote-temporary-file-directory 'method)))))

(defun tramp--test-supports-set-file-times-p ()
  "Return whether the method under test supports setting file times."
  (or (tramp--test-adb-p) (tramp--test-gvfs-p)
      (tramp--test-sh-p) (tramp--test-smb-p)
      (tramp--test-sudoedit-p)))

(defun tramp--test-supports-environment-variables-p ()
  "Return whether setting environment variables is supported."
  (and (tramp--test-sh-p)
       (not (tramp--test-crypt-p))))

(defun tramp--test-check-files (&rest files)
  "Run a simple but comprehensive test over every file in FILES."
  (dolist (quoted (if (tramp--test-expensive-test-p) '(nil t) '(nil)))
    ;; We must use `file-truename' for the temporary directory,
    ;; because it could be located on a symlinked directory.  This
    ;; would let the test fail.
    (let* ((ert-remote-temporary-file-directory
	    (file-truename ert-remote-temporary-file-directory))
	   (tramp-fuse-remove-hidden-files t)
	   (tmp-name1 (tramp--test-make-temp-name nil quoted))
	   (tmp-name2 (tramp--test-make-temp-name 'local quoted))
	   (files
            (tramp-compat-seq-keep
	     (lambda (x) (unless (string-empty-p x) x)) files))
	   (process-environment process-environment)
	   (sorted-files (sort (copy-sequence files) #'string-lessp))
	   buffer)
      (unwind-protect
	  (progn
	    (make-directory tmp-name1)
	    (make-directory tmp-name2)

	    (dolist (elt files)
              ;(tramp--test-message "'%s'" elt)
	      (let* ((file1 (expand-file-name elt tmp-name1))
		     (file2 (expand-file-name elt tmp-name2))
		     (file3 (expand-file-name (concat elt "foo") tmp-name1)))
		(write-region elt nil file1)
		(should (file-exists-p file1))

		;; Check file contents.
		(with-temp-buffer
		  (insert-file-contents file1)
		  (should (string-equal (buffer-string) elt)))

		;; Copy file both directions.
		(copy-file file1 (file-name-as-directory tmp-name2))
		(should (file-exists-p file2))
		(delete-file file1)
		(should-not (file-exists-p file1))
		(copy-file file2 (file-name-as-directory tmp-name1))
		(should (file-exists-p file1))

		(tramp--test-ignore-make-symbolic-link-error
		  (make-symbolic-link file1 file3)
		  (should (file-symlink-p file3))
		  (should
		   (string-equal
		    (expand-file-name file1) (file-truename file3)))
		  (should
		   (string-equal
		    (funcall
		     (if quoted #'file-name-quote #'identity)
		     (file-attribute-type (file-attributes file3)))
		    (file-remote-p (file-truename file1) 'localname)))
		  (delete-file file3))))

	    ;; Check file names.
	    (should (equal (directory-files
			    tmp-name1 nil directory-files-no-dot-files-regexp)
			   sorted-files))
	    (should (equal (directory-files
			    tmp-name2 nil directory-files-no-dot-files-regexp)
			   sorted-files))
	    (should (equal (mapcar
			    #'car
			    (directory-files-and-attributes
			     tmp-name1 nil directory-files-no-dot-files-regexp))
			   sorted-files))
	    (should (equal (mapcar
			    #'car
			    (directory-files-and-attributes
			     tmp-name2 nil directory-files-no-dot-files-regexp))
			   sorted-files))

	    ;; Check, that `insert-directory' works properly.
	    (with-current-buffer
		(setq buffer (dired-noselect tmp-name1 "--dired -al"))
	      (goto-char (point-min))
	      (while (not (eobp))
		(when-let* ((name (dired-get-filename 'no-dir 'no-error)))
		  (unless
		      (string-match-p name directory-files-no-dot-files-regexp)
		    (should (member name files))))
		(forward-line 1)))
	    (kill-buffer buffer)

	    ;; `substitute-in-file-name' could return different
	    ;; values.  For "adb", there could be strange file
	    ;; permissions preventing overwriting a file.  We don't
	    ;; care in this test case.
	    (dolist (elt files)
	      (let ((file1
		     (substitute-in-file-name (expand-file-name elt tmp-name1)))
		    (file2
		     (substitute-in-file-name
		      (expand-file-name elt tmp-name2))))
		(ignore-errors (write-region elt nil file1))
		(should (file-exists-p file1))
		(ignore-errors (write-region elt nil file2 nil 'nomessage))
		(should (file-exists-p file2))))

	    (should (equal (directory-files
			    tmp-name1 nil directory-files-no-dot-files-regexp)
			   (directory-files
			    tmp-name2 nil directory-files-no-dot-files-regexp)))

	    ;; Check directory creation.  We use a subdirectory "foo"
	    ;; in order to avoid conflicts with previous file name tests.
	    (dolist (elt files)
	      (let* ((elt1 (concat elt "foo"))
		     (file1 (expand-file-name (concat "foo/" elt) tmp-name1))
		     (file2 (expand-file-name elt file1))
		     (file3 (expand-file-name elt1 file1)))
		(make-directory file1 'parents)
		(should (file-directory-p file1))
		(write-region elt nil file2)
		(should (file-exists-p file2))
		(should
		 (equal
		  (directory-files
		   file1 nil directory-files-no-dot-files-regexp)
		  `(,elt)))
		(should
		 (equal
		  (caar (directory-files-and-attributes
			 file1 nil directory-files-no-dot-files-regexp))
		  elt))

		;; Check symlink in `directory-files-and-attributes'.
		;; It does not work in the "smb" case, only relative
		;; symlinks to existing files are shown there.  On
		;; NetBSD, there are problems with loooong file names,
		;; see Bug#65324.
		(tramp--test-ignore-make-symbolic-link-error
		  (unless (or (tramp--test-netbsd-p) (tramp--test-smb-p))
		    (make-symbolic-link file2 file3)
		    (should (file-symlink-p file3))
		    (should
		     (string-equal
		      (caar (directory-files-and-attributes
			     file1 nil (rx (literal elt1))))
		      elt1))
		    (should
		     (string-equal
		      (funcall
		       (if quoted #'file-name-quote #'identity)
		       (cadr (car (directory-files-and-attributes
				   file1 nil (rx (literal elt1))))))
		      (file-remote-p (file-truename file2) 'localname)))
		    (delete-file file3)
		    (should-not (file-exists-p file3))))

		;; Check, that a process runs on a remote
		;; `default-directory' with special characters.  See
		;; Bug#53846.
		(when (and (tramp--test-expensive-test-p)
			   (tramp--test-supports-processes-p)
			   ;; FIXME: tramp-smb.el should implement this.
			   (not (and (tramp--test-smb-p)
				     (string-match-p
				      (rx (or (any "[$") (not ascii))) file1))))
		  (let ((default-directory file1))
		    (dolist (this-shell-command
			     (append
			      ;; Synchronously.
			      '(shell-command)
			      ;; Asynchronously.
			      (and (tramp--test-asynchronous-processes-p)
				   '(tramp--test-async-shell-command))))
		      (with-temp-buffer
			(funcall this-shell-command "cat -- *" (current-buffer))
			(should
			 (string-match-p
			  (rx (literal elt) eol) (buffer-string)))))))

		(delete-file file2)
		(should-not (file-exists-p file2))
		(delete-directory file1 'recursive)
		(should-not (file-exists-p file1))))

	    ;; Check, that environment variables are set correctly.
            ;; We do not run on macOS due to encoding problems.  See
            ;; Bug#36940.
	    (when (and (tramp--test-expensive-test-p) (tramp--test-sh-p)
		       (not (tramp--test-crypt-p))
		       (not (eq system-type 'darwin)))
	      (dolist (elt files)
		(let ((envvar (concat "VAR_" (upcase (md5 elt))))
		      (elt (encode-coding-string elt coding-system-for-read))
		      (default-directory ert-remote-temporary-file-directory)
		      (process-environment process-environment))
		  (setenv envvar elt)
		  ;; The value of PS1 could confuse Tramp's detection
		  ;; of process output.  So we unset it temporarily.
		  (setenv "PS1")
		  (with-temp-buffer
		    (when (zerop (process-file "printenv" nil t nil))
		      (goto-char (point-min))
		      (should
		       (search-forward-regexp
			(rx
			 bol (literal envvar)
			 "=" (literal (getenv envvar)) eol)))))))))

	;; Cleanup.
	(ignore-errors (kill-buffer buffer))
	(ignore-errors (delete-directory tmp-name1 'recursive))
	(ignore-errors (delete-directory tmp-name2 'recursive))))))

;; These tests are inspired by Bug#17238.
(ert-deftest tramp-test41-special-characters ()
  "Check special characters in file names."
  (skip-unless (tramp--test-enabled))
  (skip-unless (not (tramp--test-rsync-p)))
  (skip-unless (not (tramp--test-rclone-p)))
  (skip-unless (not (or (eq system-type 'darwin) (tramp--test-macos-p))))

  ;; Newlines, slashes and backslashes in file names are not
  ;; supported.  So we don't test.  And we don't test the tab
  ;; character on Windows or Cygwin, because the backslash is
  ;; interpreted as a path separator, preventing "\t" from being
  ;; expanded to <TAB>.
  (let ((files
	 (list
	  (cond ((or (tramp--test-ange-ftp-p)
		     (tramp--test-container-p)
		     (tramp--test-gvfs-p)
		     (tramp--test-openbsd-p)
		     (tramp--test-rclone-p)
		     (tramp--test-sudoedit-p)
		     (tramp--test-windows-nt-or-smb-p))
		 "foo bar baz")
		((or (tramp--test-adb-p)
		     (eq system-type 'cygwin))
		 " foo bar baz ")
		((tramp--test-sh-no-ls--dired-p)
		 "\tfoo bar baz\t")
		(t " foo\tbar baz\t"))
	  "@foo@bar@baz@"
	  (unless (tramp--test-windows-nt-and-out-of-band-p) "$foo$bar$$baz$")
	  "-foo-bar-baz-"
	  (unless (tramp--test-windows-nt-and-out-of-band-p) "%foo%bar%baz%")
	  "&foo&bar&baz&"
	  (unless (or (tramp--test-ftp-p)
		      (tramp--test-gvfs-p)
		      (tramp--test-windows-nt-or-smb-p))
	    "?foo?bar?baz?")
	  (unless (or (tramp--test-container-oob-p)
		      (tramp--test-ftp-p)
		      (tramp--test-gvfs-p)
		      (tramp--test-windows-nt-or-smb-p))
	    "*foo+bar*baz+")
	  (if (or (tramp--test-gvfs-p) (tramp--test-windows-nt-or-smb-p))
	      "'foo'bar'baz'"
	    "'foo\"bar'baz\"")
	  "#foo~bar#baz~"
	  (unless (tramp--test-windows-nt-and-out-of-band-p)
	    (if (or (tramp--test-gvfs-p) (tramp--test-windows-nt-or-smb-p))
		"!foo!bar!baz!"
	      "!foo|bar!baz|"))
	  (if (or (tramp--test-gvfs-p)
		  (tramp--test-rclone-p)
		  (tramp--test-windows-nt-or-smb-p))
	      ";foo;bar;baz;"
	    ":foo;bar:baz;")
	  (unless (or (tramp--test-gvfs-p) (tramp--test-windows-nt-or-smb-p))
	    "<foo>bar<baz>")
	  "(foo)bar(baz)"
	  (unless (or (tramp--test-container-oob-p)
		      (tramp--test-ftp-p)
		      (tramp--test-gvfs-p))
	    "[foo]bar[baz]")
	  "{foo}bar{baz}")))
    ;; Simplify test in order to speed up.
    (apply #'tramp--test-check-files
	   (if (tramp--test-expensive-test-p)
	       files (list (string-join files ""))))))

(tramp--test-deftest-with-stat tramp-test41-special-characters)

(tramp--test-deftest-with-perl tramp-test41-special-characters)

(tramp--test-deftest-with-ls tramp-test41-special-characters)

(tramp--test-deftest-without-file-attributes tramp-test41-special-characters)

(tramp--test-deftest-direct-async-process tramp-test41-special-characters)

(ert-deftest tramp-test42-utf8 ()
  "Check UTF8 encoding in file names and file contents."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-enabled))
  (skip-unless (not (tramp--test-container-p)))
  (skip-unless (not (tramp--test-rsync-p)))
  (skip-unless (not (tramp--test-scp-p)))
  (skip-unless (not (tramp--test-windows-nt-and-out-of-band-p)))
  (skip-unless (not (tramp--test-ksh-p)))
  (skip-unless (not (tramp--test-gdrive-p)))
  (skip-unless (not (tramp--test-crypt-p)))
  (skip-unless (not (tramp--test-rclone-p)))
  (skip-unless (not (or (eq system-type 'darwin) (tramp--test-macos-p))))

  (let ((coding-system-for-read 'utf-8)
	(coding-system-for-write 'utf-8)
	(file-name-coding-system
	 (coding-system-change-eol-conversion 'utf-8 'unix)))
    (apply
     #'tramp--test-check-files
     (append
      (list
       (unless (tramp--test-hpux-p) "Γυρίστε το Γαλαξία με Ώτο Στοπ")
       (unless (tramp--test-hpux-p)
	 "أصبح بوسعك الآن تنزيل نسخة كاملة من موسوعة ويكيبيديا العربية لتصفحها بلا اتصال بالإنترنت")
       "银河系漫游指南系列"
       "Автостопом по гала́ктике"
       ;; Use codepoints without a name.  See Bug#31272.
       ;; Works on some Android systems only.
       (unless (or (tramp--test-adb-p) (tramp--test-openbsd-p))
	 "bung")
       ;; Use codepoints from Supplementary Multilingual Plane (U+10000
       ;; to U+1FFFF).
       "🌈🍒👋")

      (when (and (tramp--test-expensive-test-p) (not (tramp--test-windows-nt-p)))
	(seq-uniq
	 (tramp-compat-seq-keep
	  ;; Use all available language specific snippets.
	  (lambda (x)
	    (and
	     ;; The "Oriya" and "Odia" languages use some problematic
	     ;; composition characters.
	     (not (member (car x) '("Oriya" "Odia")))
             (stringp (setq x (eval (get-language-info (car x) 'sample-text) t)))
	     ;; Filter out strings which use unencodable characters.
	     (not (and (or (tramp--test-gvfs-p) (tramp--test-smb-p))
		       (unencodable-char-position
			0 (length x) file-name-coding-system nil x)))
	     ;; Filter out not displayable characters.
	     (setq x (mapconcat
		      (lambda (y)
			(and (char-displayable-p y) (char-to-string y)))
		      x ""))
             (not (string-empty-p x))
	     ;; ?\n and ?/ shouldn't be part of any file name.  ?\t,
	     ;; ?. and ?? do not work for "smb" method.  " " does not
	     ;; work at begin or end of the string for MS Windows.
	     (replace-regexp-in-string (rx (any " \t\n/.?")) "" x)))
	  language-info-alist)))))))

(tramp--test-deftest-with-stat tramp-test42-utf8)

(tramp--test-deftest-with-perl tramp-test42-utf8)

(tramp--test-deftest-with-ls tramp-test42-utf8)

(tramp--test-deftest-without-file-attributes tramp-test42-utf8)

(tramp--test-deftest-direct-async-process tramp-test42-utf8)

(ert-deftest tramp-test43-file-system-info ()
  "Check that `file-system-info' returns proper values."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-enabled))

  (when-let* ((fsi (file-system-info ert-remote-temporary-file-directory)))
    (should (consp fsi))
    (should (length= fsi 3))
    (dotimes (i (length fsi))
      (should (natnump (or (nth i fsi) 0))))))

;; `file-user-uid' and `file-group-gid' were introduced in Emacs 30.1.
(ert-deftest tramp-test44-file-user-group-ids ()
  "Check results of user/group functions.
`file-user-uid', `file-group-gid', and `tramp-get-remote-*'
should all return proper values."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-enabled))

  (let ((default-directory ert-remote-temporary-file-directory))
    ;; `file-user-uid' and `file-group-gid' exist since Emacs 30.1.
    ;; We don't want to see compiler warnings for older Emacsen.
    (when (fboundp 'file-user-uid)
      (should (integerp (with-no-warnings (file-user-uid)))))
    (when (fboundp 'file-group-gid)
      (should (integerp (with-no-warnings (file-group-gid)))))

    (with-parsed-tramp-file-name default-directory nil
      (should (or (integerp (tramp-get-remote-uid v 'integer))
		  (null (tramp-get-remote-uid v 'integer))))
      (should (or (stringp (tramp-get-remote-uid v 'string))
		  (null (tramp-get-remote-uid v 'string))))

      (should (or (integerp (tramp-get-remote-gid v 'integer))
		  (null (tramp-get-remote-gid v 'integer))))
      (should (or (stringp (tramp-get-remote-gid v 'string))
		  (null (tramp-get-remote-gid v 'string))))

      (when-let* ((groups (tramp-get-remote-groups v 'integer)))
	(should (consp groups))
	(dolist (group groups) (should (integerp group))))
      (when-let* ((groups (tramp-get-remote-groups v 'string)))
	(should (consp groups))
	(dolist (group groups) (should (stringp group)))))))

;; `tramp-test45-asynchronous-requests' could be blocked.  So we set a
;; timeout of 300 seconds, and we send a SIGUSR1 signal after 300
;; seconds.  Similar check is performed in the timer function.
(defconst tramp--test-asynchronous-requests-timeout 300
  "Timeout for `tramp-test45-asynchronous-requests'.")

;; This test is inspired by Bug#16928.
(ert-deftest tramp-test45-asynchronous-requests ()
  "Check parallel asynchronous requests.
Such requests could arrive from timers, process filters and
process sentinels.  They shall not disturb each other."
  :tags '(:expensive-test :tramp-asynchronous-processes)
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-supports-processes-p))
  (skip-unless (not (tramp--test-container-p)))
  (skip-unless (not (tramp--test-sshfs-p)))
  (skip-unless (not (tramp--test-telnet-p)))
  (skip-unless (not (tramp--test-box-p)))
  (skip-unless (not (tramp--test-windows-nt-p)))
  (skip-unless (not (tramp--test-smb-p)))

  (with-timeout
      (tramp--test-asynchronous-requests-timeout (tramp--test-timeout-handler))
    (define-key special-event-map [sigusr1] #'tramp--test-timeout-handler)
    (let* (;; For the watchdog.
	   (default-directory (expand-file-name temporary-file-directory))
	   ;; It doesn't work on w32 systems.
	   (watchdog
            (start-process-shell-command
             "*watchdog*" nil
             (format
	      "sleep %d; kill -USR1 %d"
	      tramp--test-asynchronous-requests-timeout (emacs-pid))))
           (tmp-name (tramp--test-make-temp-name))
           (default-directory tmp-name)
           ;; Do not cache Tramp properties.
           (remote-file-name-inhibit-cache t)
           (process-file-side-effects t)
           ;; Suppress nasty messages.
           (inhibit-message (not (ignore-errors (edebug-mode))))
	   ;; Do not run delayed timers.
	   (timer-max-repeats 0)
	   ;; Number of asynchronous processes for test.  Tests on
	   ;; some machines handle less parallel processes.
           (number-proc
            (cond
             ((ignore-errors
               (string-to-number (getenv "REMOTE_PARALLEL_PROCESSES"))))
             (t 10)))
	   ;; PuTTY-based methods can only share up to 10 connections.
	   (tramp-use-connection-share
	    (if (and (tramp--test-putty-p) (>= number-proc 10))
		'suppress (bound-and-true-p tramp-use-connection-share)))
           (timer-repeat 1)
	   ;; This is when all timers start.  We check inside the
	   ;; timer function, that we don't exceed timeout.
	   (timer-start (current-time))
           timer buffers kill-buffer-query-functions)

      (unwind-protect
          (progn
            (make-directory tmp-name)

            ;; Setup a timer in order to raise an ordinary command
            ;; again and again.  `vc-registered' is well suited,
            ;; because there are many checks.
            (setq
             timer
             (run-at-time
              0 timer-repeat
              (lambda ()
                (when (> (- (time-to-seconds) (time-to-seconds timer-start))
                         tramp--test-asynchronous-requests-timeout)
                  (tramp--test-timeout-handler))
                (when buffers
                  (let ((time (float-time))
                        (default-directory tmp-name)
                        (file (buffer-name (seq-random-elt buffers))))
                    (tramp--test-message
		     "Start timer %s %s" file (current-time-string))
		    (dired-uncache file)
		    (tramp--test-message
		     "Continue timer %s %s" file (file-attributes file))
		    (vc-registered file)
                    (tramp--test-message
                     "Stop timer %s %s" file (current-time-string))
                    ;; Adjust timer if it takes too much time.
                    (when (> (- (float-time) time) timer-repeat)
                      (setq timer-repeat (* 1.1 (- (float-time) time)))
                      (setf (timer--repeat-delay timer) timer-repeat)
                      (tramp--test-message
		       "Increase timer %s" timer-repeat)))))))

            ;; Create temporary buffers.  The number of buffers
            ;; corresponds to the number of processes; it could be
            ;; increased in order to make pressure on Tramp.
            (dotimes (_ number-proc)
              (setq buffers (cons (generate-new-buffer "foo") buffers)))

            ;; Open asynchronous processes.  Set process filter and sentinel.
            (dolist (buf buffers)
	      ;; Activate timer.
	      (sit-for 0.01 'nodisp)
              (let ((proc
                     (start-file-process-shell-command
                      (buffer-name buf) buf
                      (concat
		       "(read line && echo $line >$line && echo $line);"
		       "(read line && cat $line);"
		       "(read line && rm -f $line)")))
                    (file (expand-file-name (buffer-name buf))))
                ;; Remember the file name.  Add counter.
                (process-put proc 'foo file)
                (process-put proc 'bar 0)
                ;; Add process filter.
                (set-process-filter
                 proc
                 (lambda (proc string)
                   (tramp--test-message
                    "Process filter %s %s %s"
		    proc string (current-time-string))
                   (with-current-buffer (process-buffer proc)
                     (insert string))
                   (when (< (process-get proc 'bar) 2)
		     (dired-uncache (process-get proc 'foo))
                     (should (file-attributes (process-get proc 'foo))))))
                ;; Add process sentinel.  It shall not perform remote
                ;; operations, triggering Tramp processes.  This blocks.
                (set-process-sentinel
                 proc
                 (lambda (proc _state)
                   (tramp--test-message
                    "Process sentinel %s %s" proc (current-time-string))))
		(tramp--test-message "Process started %s" proc)))

            ;; Send a string to the processes.  Use a random order of
            ;; the buffers.  Mix with regular operation.
            (let ((buffers (copy-sequence buffers))
		  buf)
              (while buffers
		(setq buf (seq-random-elt buffers))
                (if-let* ((proc (get-buffer-process buf))
			  (file (process-get proc 'foo))
			  (count (process-get proc 'bar)))
		    (progn
                      (tramp--test-message
                       "Start action %d %s %s" count buf (current-time-string))
                      ;; Regular operation prior process action.
		      (dired-uncache file)
                      (if (= count 0)
			  (should-not (file-attributes file))
			(should (file-attributes file)))
                      ;; Send string to process.
                      (process-send-string
		       proc (format "%s\n" (buffer-name buf)))
                      (while (accept-process-output nil 0))
                      (tramp--test-message
                       "Continue action %d %s %s"
		       count buf (current-time-string))
                      ;; Regular operation post process action.
		      (dired-uncache file)
                      (if (= count 2)
			  (should-not (file-attributes file))
			(should (file-attributes file)))
                      (tramp--test-message
                       "Stop action %d %s %s" count buf (current-time-string))
                      (process-put proc 'bar (1+ count))
                      (unless (process-live-p proc)
			(setq buffers (delq buf buffers))))
		  (setq buffers (delq buf buffers)))))

            ;; Checks.  All process output shall exist in the
            ;; respective buffers.  All created files shall be
            ;; deleted.
            (tramp--test-message "Check %s" (current-time-string))
            (dolist (buf buffers)
              (with-current-buffer buf
                (should
		 (string-equal
		  ;; tramp-adb.el echoes, so we must add the three strings.
		  (if (tramp--test-adb-p)
		      (format "%s\n%s\n%s\n%s\n%s\n" buf buf buf buf buf)
		    (format "%s\n%s\n" buf buf))
		  (buffer-string)))))
            (should-not
             (directory-files
              tmp-name nil directory-files-no-dot-files-regexp)))

        ;; Cleanup.
        (define-key special-event-map [sigusr1] #'ignore)
        (ignore-errors (quit-process watchdog))
        (dolist (buf buffers)
          (ignore-errors (delete-process (get-buffer-process buf)))
          (ignore-errors (kill-buffer buf)))
        (ignore-errors (cancel-timer timer))
        (ignore-errors (delete-directory tmp-name 'recursive))))))

;; (tramp--test-deftest-direct-async-process tramp-test45-asynchronous-requests
;;   'unstable)

;; This test is inspired by Bug#49954 and Bug#60534.
(ert-deftest tramp-test45-force-remote-file-error ()
  "Force `remote-file-error'."
  :tags '(:expensive-test :tramp-asynchronous-processes :unstable)
  ;; It shall run only if selected explicitly.
  (skip-unless
   (eq (ert--stats-selector ert--current-run-stats)
       (ert-test-name (ert--stats-current-test ert--current-run-stats))))
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-sh-p))

  (let ((default-directory ert-remote-temporary-file-directory)
        ;; Do not cache Tramp properties.
        (remote-file-name-inhibit-cache t)
	(p (start-file-process-shell-command
	    "test" (generate-new-buffer "test" 'inhibit-buffer-hooks)
	    "while true; do echo test; sleep 0.2; done")))

    (set-process-filter
     p (lambda (&rest _)
	 (message "filter %s" default-directory)
	 (directory-files default-directory)
	 (dired-uncache default-directory)))

    (run-at-time
     0 0.2 (lambda ()
	     (message "timer %s" default-directory)
	     (directory-files default-directory)
	     (dired-uncache default-directory)))

    (while t
      (accept-process-output)
      (message "main %s" default-directory)
      (directory-files default-directory)
      (dired-uncache default-directory))))

(ert-deftest tramp-test46-dired-compress-file ()
  "Check that Tramp (un)compresses normal files."
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-sh-p))
  (skip-unless (not (tramp--test-crypt-p)))
  ;; Starting with Emacs 29.1, `dired-compress-file' is performed by
  ;; default handler.
  (skip-unless (not (tramp--test-emacs29-p)))

  (let ((default-directory ert-remote-temporary-file-directory)
        (tmp-name (tramp--test-make-temp-name)))
    (write-region "foo" nil tmp-name)
    (dired default-directory)
    (dired-revert)
    (dired-goto-file tmp-name)
    (should-not (dired-compress))
    (should (string= (concat tmp-name ".gz") (dired-get-filename)))
    (should-not (dired-compress))
    (should (string= tmp-name (dired-get-filename)))
    (delete-file tmp-name)))

(ert-deftest tramp-test46-dired-compress-dir ()
  "Check that Tramp (un)compresses directories."
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-sh-p))
  (skip-unless (not (tramp--test-crypt-p)))
  ;; Starting with Emacs 29.1, `dired-compress-dir' is performed by
  ;; default handler.
  (skip-unless (not (tramp--test-emacs29-p)))

  (let ((default-directory ert-remote-temporary-file-directory)
        (tmp-name (tramp--test-make-temp-name)))
    (make-directory tmp-name)
    (dired default-directory)
    (dired-revert)
    (dired-goto-file tmp-name)
    (should-not (dired-compress))
    (should (string= (concat tmp-name ".tar.gz") (dired-get-filename)))
    (should-not (dired-compress))
    (should (string= tmp-name (dired-get-filename)))
    (delete-directory tmp-name)
    (delete-file (concat tmp-name ".tar.gz"))))

;; More exhaustive tests are performed in filenotify-tests.el,
;; selector "remote".
(ert-deftest tramp-test46-file-notifications ()
  "Check that Tramp handles file notifications."
  :tags '(:unstable)
  (skip-unless (tramp--test-enabled))
  ;; filenotify.el was reworked in Emacs 31.
  (skip-unless (tramp--test-emacs31-p))

  (let* ((tmp-name (tramp--test-make-temp-name))
	 ;(file-notify-debug t)
	 (desc1
	  (ignore-error file-notify-error
	    (file-notify-add-watch
	     tmp-name '(change attribute-change) #'ignore)))
	 (desc2
	  (ignore-error file-notify-error
	    (file-notify-add-watch
	     ert-remote-temporary-file-directory
	     '(change attribute-change) #'ignore))))
    (skip-unless (and desc1 desc2))

    (unwind-protect
	(progn
	  (tramp--test-message "%S" desc1)
	  (should-not (file-exists-p tmp-name))
	  (should (file-notify-valid-p desc2))

	  ;; Create the file.  `file-notify-valid-p' requires that the
	  ;; watched file exists, so we cannot check it earlier for `desc1'.
	  (write-region "foo" nil tmp-name)
	  (should (file-exists-p tmp-name))
	  (should (file-notify-valid-p desc1))
	  ;; Modify.
	  (write-region "foo" nil tmp-name)
	  (should (file-exists-p tmp-name))
	  ;; Delete.
	  (delete-file tmp-name)
	  (should-not (file-exists-p tmp-name))

	  (while (read-event nil nil 0.1))
	  ;; This has been stopped because the file was deleted.
	  (should-not (file-notify-valid-p desc1))

	  ;; This is still valid.
	  (should (file-notify-valid-p desc2))
	  (file-notify-rm-watch desc2)
	  (should-not (file-notify-valid-p desc2)))

      ;; Cleanup.
      (ignore-errors (delete-file tmp-name))
      ;; `file-notify-rm-all-watches' exists since Emacs 30.1.
      ;; We don't want to see compiler warnings for older Emacsen.
      (when (fboundp 'file-notify-rm-all-watches)
	(with-no-warnings (file-notify-rm-all-watches))))))

(ert-deftest tramp-test47-read-password ()
  "Check Tramp password handling."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-mock-p))
  ;; Not all read commands understand argument "-s" or "-p".
  (skip-unless
   (string-empty-p
    (let ((shell-file-name tramp-default-remote-shell))
      (shell-command-to-string "read -s -p Password: pass"))))

  (let* ((pass "secret")
	 (tramp-connection-properties
	  `((nil "login-args"
		 (("-c")
		  (,(tramp-shell-quote-argument
		     (concat
		      "read -s -p 'Password: ' pass; echo; "
		      "(test \"pass$pass\" != \"pass" pass "\" && "
		      "echo \"Login incorrect\" || "
		      tramp-default-remote-shell " -i)")))))))
	 mocked-input auth-sources)
    ;; We must mock `read-string', in order to avoid interactive
    ;; arguments.
    (cl-letf* (((symbol-function #'read-string)
		(lambda (&rest _args) (pop mocked-input))))

      ;; Reading password from stdin works.
      (tramp-cleanup-connection tramp-test-vec 'keep-debug)
      ;; We don't want to invalidate the password.
      (setq mocked-input `(,(copy-sequence pass)))
      (should (file-exists-p ert-remote-temporary-file-directory))

      ;; Don't entering a password returns in error.
      (tramp-cleanup-connection tramp-test-vec 'keep-debug)
      (setq mocked-input nil)
      (should-error (file-exists-p ert-remote-temporary-file-directory))

      ;; A wrong password doesn't work either.
      (tramp-cleanup-connection tramp-test-vec 'keep-debug)
      (setq mocked-input `(,(concat pass pass)))
      (should-error (file-exists-p ert-remote-temporary-file-directory))

      ;; Reading password from auth-source works.  We use the netrc
      ;; backend; the other backends shall behave similar.
      ;; Macro `ert-with-temp-file' was introduced in Emacs 29.1.
      (with-no-warnings (when (symbol-plist #'ert-with-temp-file)
	(tramp-cleanup-connection tramp-test-vec 'keep-debug)
	(setq mocked-input nil)
	(auth-source-forget-all-cached)
	(ert-with-temp-file netrc-file
	  :prefix tramp-test-name-prefix :suffix ""
	  :text (format
		 "machine %s port mock password %s"
		 (file-remote-p ert-remote-temporary-file-directory 'host) pass)
	  (let ((auth-sources `(,netrc-file)))
	    (should (file-exists-p ert-remote-temporary-file-directory))))))

      ;; Checking session-timeout.
      (with-no-warnings (when (symbol-plist #'ert-with-temp-file)
	(tramp-cleanup-connection tramp-test-vec 'keep-debug)
	(let ((tramp-connection-properties
	       (cons '(nil "session-timeout" 1)
		     tramp-connection-properties)))
	  (setq mocked-input nil)
	  (auth-source-forget-all-cached)
	  (ert-with-temp-file netrc-file
	    :prefix tramp-test-name-prefix :suffix ""
	    :text (format
		   "machine %s port mock password %s"
		   (file-remote-p ert-remote-temporary-file-directory 'host)
		   pass)
	    (let ((auth-sources `(,netrc-file)))
	      (should (file-exists-p ert-remote-temporary-file-directory))))
	  ;; Session established, password cached.
	  (should
	   (password-in-cache-p
	    (auth-source-format-cache-entry
	     (tramp-get-connection-property tramp-test-vec " pw-spec"))))
	  ;; We want to see the timeout message.
	  (tramp--test-instrument-test-case 3
	    (sleep-for 2))
	  ;; Session canceled, no password in cache.
	  (should-not
	   (password-in-cache-p
	    (auth-source-format-cache-entry
	     (tramp-get-connection-property tramp-test-vec " pw-spec")))))))))

  ;; Cleanup.
  (tramp-cleanup-connection tramp-test-vec 'keep-debug))

(ert-deftest tramp-test47-read-otp-password ()
  "Check Tramp one-time password handling."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-mock-p))
  ;; Not all read commands understand argument "-s" or "-p".
  (skip-unless
   (string-empty-p
    (let ((shell-file-name tramp-default-remote-shell))
      (shell-command-to-string "read -s -p Password: pass"))))

  (let* ((pass "secret")
	 (tramp-connection-properties
	  `((nil "login-args"
		 (("-c")
		  (,(tramp-shell-quote-argument
		     (concat
		      "read -s -p 'Verification code: ' pass; echo; "
		      "(test \"pass$pass\" != \"pass" pass "\" && "
		      "echo \"Login incorrect\" || "
		      tramp-default-remote-shell " -i)")))))))
	 mocked-input auth-sources)
    ;; We must mock `read-string', in order to avoid interactive
    ;; arguments.
    (cl-letf* (((symbol-function #'read-string)
		(lambda (&rest _args) (pop mocked-input))))

      ;; Reading password from stdin works.
      (tramp-cleanup-connection tramp-test-vec 'keep-debug)
      ;; We don't want to invalidate the password.
      (setq mocked-input `(,(copy-sequence pass)))
      (should (file-exists-p ert-remote-temporary-file-directory))

      ;; Don't entering a password returns in error.
      (tramp-cleanup-connection tramp-test-vec 'keep-debug)
      (setq mocked-input nil)
      (should-error (file-exists-p ert-remote-temporary-file-directory))

      ;; A wrong password doesn't work either.
      (tramp-cleanup-connection tramp-test-vec 'keep-debug)
      (setq mocked-input `(,(concat pass pass)))
      (should-error (file-exists-p ert-remote-temporary-file-directory))

      ;; The password shouldn't be read from auth-source.
      ;; Macro `ert-with-temp-file' was introduced in Emacs 29.1.
      (with-no-warnings (when (symbol-plist #'ert-with-temp-file)
	(tramp-cleanup-connection tramp-test-vec 'keep-debug)
	(setq mocked-input nil)
	(auth-source-forget-all-cached)
	(ert-with-temp-file netrc-file
	  :prefix tramp-test-name-prefix :suffix ""
	  :text (format
		 "machine %s port mock password %s"
		 (file-remote-p ert-remote-temporary-file-directory 'host)
		 pass)
	  (let ((auth-sources `(,netrc-file)))
	    (should-error
	     (file-exists-p ert-remote-temporary-file-directory))))))))

  ;; Cleanup.
  (tramp-cleanup-connection tramp-test-vec 'keep-debug))

(ert-deftest tramp-test47-read-security-key ()
  "Check Tramp security key handling."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-mock-p))
  ;; Not all read commands understand argument "-s" or "-p".
  (skip-unless
   (string-empty-p
    (let ((shell-file-name tramp-default-remote-shell))
      (shell-command-to-string "read -s -p Password: pass"))))

  (let (;; Suppress "exec".
	(tramp-restricted-shell-hosts-alist `(,tramp-system-name)))

    ;; Reading security key works.
    (tramp-cleanup-connection tramp-test-vec 'keep-debug)
    (let ((tramp-connection-properties
	   `((nil "login-args"
		  (("-c")
		   (,(tramp-shell-quote-argument
		      "echo Confirm user presence for key XXX"))
		   (";") ("sleep" "1")
		   (";") ("echo" "User presence confirmed")
		   (";") ("sleep" "1")
		   (";") (,tramp-default-remote-shell "-i"))))))
      (should (file-exists-p ert-remote-temporary-file-directory)))

    (let* ((pin "1234")
	   (tramp-connection-properties
	    `((nil "login-args"
		   (("-c")
		    (,(tramp-shell-quote-argument
		       (concat
			"echo Confirm user presence for key XXX; sleep 1; "
			"read -s -p 'Enter PIN for XXX ' pin; echo; "
			"(test \"pin$pin\" != \"pin" pin "\" && "
			"echo \"Login incorrect\" || "
			"(echo User presence confirmed; "
			tramp-default-remote-shell " -i))")))))))
	   mocked-input auth-sources)
      ;; We must mock `read-string', in order to avoid interactive
      ;; arguments.
      (cl-letf* (((symbol-function #'read-string)
		  (lambda (&rest _args) (pop mocked-input))))

	;; Reading security PIN works.
	(tramp-cleanup-connection tramp-test-vec 'keep-debug)
	;; We don't want to invalidate the pin.
	(setq mocked-input `(,(copy-sequence pin)))
	(should (file-exists-p ert-remote-temporary-file-directory))

	;; Don't entering a security PIN returns in error.
	(tramp-cleanup-connection tramp-test-vec 'keep-debug)
	(setq mocked-input nil)
	(should-error (file-exists-p ert-remote-temporary-file-directory))

	;; A wrong security PIN doesn't work either.
	(tramp-cleanup-connection tramp-test-vec 'keep-debug)
	(setq mocked-input `(,(concat pin pin)))
	(should-error (file-exists-p ert-remote-temporary-file-directory))))

    ;; Timeout is detected.
    (tramp-cleanup-connection tramp-test-vec 'keep-debug)
    (let ((tramp-connection-properties
	   `((nil "login-args"
		  (("-c")
		   (,(tramp-shell-quote-argument
		      "echo Confirm user presence for key XXX"))
		   (";") ("sleep" "1")
		   (";") ("echo" "sign_and_send_pubkey: signing failed for XXX")
		   (";") ("sleep" "1")
		   (";") ("exit" "1"))))))
      (should-error (file-exists-p ert-remote-temporary-file-directory))))

  ;; Cleanup.
  (tramp-cleanup-connection tramp-test-vec 'keep-debug))

(ert-deftest tramp-test47-read-fingerprint ()
  "Check Tramp fingerprint handling."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-mock-p))

  (let (;; Suppress "exec".
	(tramp-restricted-shell-hosts-alist `(,tramp-system-name)))

    ;; Reading fingerprint works.
    (tramp-cleanup-connection tramp-test-vec 'keep-debug)
    (let ((tramp-connection-properties
	   `((nil "login-args"
		  (("-c")
		   (,(tramp-shell-quote-argument
		      "echo Place your finger on the fingerprint reader"))
		   (";") ("sleep" "1")
		   (";") (,tramp-default-remote-shell "-i"))))))
      (should (file-exists-p ert-remote-temporary-file-directory)))

    ;; Falling back after a timeout works.
    (tramp-cleanup-connection tramp-test-vec 'keep-debug)
    (let ((tramp-connection-properties
	   `((nil "login-args"
		  (("-c")
		   (,(tramp-shell-quote-argument
		      "echo Place your finger on the fingerprint reader"))
		   (";") ("sleep" "1")
		   (";") ("echo" "Failed to match fingerprint")
		   (";") (,tramp-default-remote-shell "-i"))))))
      (should (file-exists-p ert-remote-temporary-file-directory)))

    ;; Interrupting the fingerprint handshaking works.
    (tramp-cleanup-connection tramp-test-vec 'keep-debug)
    (let ((tramp-connection-properties
	   `((nil "login-args"
		  (("-c")
		   (,(tramp-shell-quote-argument
		      "echo Place your finger on the fingerprint reader"))
		   (";") ("sleep" "1")
		   (";") (,tramp-default-remote-shell "-i")))))
	  tramp-use-fingerprint)
      (should (file-exists-p ert-remote-temporary-file-directory))))

  ;; Cleanup.
  (tramp-cleanup-connection tramp-test-vec 'keep-debug))

;; This test is inspired by Bug#78572.
(ert-deftest tramp-test48-session-timeout ()
  "Check that Tramp handles a session timeout properly."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-enabled))
  (skip-unless (tramp--test-sh-p))

  ;; We want to see the timeout message.
  (tramp--test-instrument-test-case 3
    (let ((remote-file-name-inhibit-cache t)
	  (tmp-name (tramp--test-make-temp-name)))
      (unwind-protect
	  (progn
	    (should-not (file-exists-p tmp-name))
	    (write-region "foo" nil tmp-name)
	    (should (file-exists-p tmp-name))

	    (tramp-timeout-session tramp-test-vec)
	    (should (file-exists-p tmp-name))
	    (should (directory-files (file-name-directory tmp-name)))

	    ;; `project-mode-line' was introduced in Emacs 30.1.
	    (when (boundp 'project-mode-line)
	      (require 'project)
	      (ert-with-message-capture captured-messages
		(let ((project-mode-line t))
		  (with-temp-buffer
		    (set-visited-file-name tmp-name)
		    (insert "foo")
		    (should (buffer-modified-p))
		    (tramp-timeout-session tramp-test-vec)
		    ;; This calls `file-directory-p' and
		    ;; `directory-files'.  Shouldn't raise an error when
		    ;; not connected.
		    (project-mode-line-format)
		    ;; Steal the file lock.
		    (cl-letf (((symbol-function #'ask-user-about-lock) #'always))
		      (save-buffer)))
		  (should-not
		   (string-match-p "File is missing:" captured-messages)))))

	    ;; A modified buffer suppresses session timeout.
	    (with-temp-buffer
	      (set-visited-file-name tmp-name)
	      (insert "foo")
	      (should (buffer-modified-p))
	      (tramp-timeout-session tramp-test-vec)
	      (should
	       (process-live-p (tramp-get-connection-process tramp-test-vec)))
	      ;; Steal the file lock.
	      (cl-letf (((symbol-function #'ask-user-about-lock) #'always))
		(save-buffer))
	      (tramp-timeout-session tramp-test-vec)
	      (should-not
	       (process-live-p (tramp-get-connection-process tramp-test-vec))))

	    ;; An auto-reverted buffer suppresses session timeout.
	    (with-temp-buffer
	      (set-visited-file-name tmp-name)
	      (auto-revert-mode 1)
	      ;; Steal the file lock.
	      (cl-letf (((symbol-function #'ask-user-about-lock) #'always))
		(save-buffer))
	      (tramp-timeout-session tramp-test-vec)
	      (should
	       (process-live-p (tramp-get-connection-process tramp-test-vec)))
	      (auto-revert-mode -1)
	      (tramp-timeout-session tramp-test-vec)
	      (should-not
	       (process-live-p (tramp-get-connection-process tramp-test-vec)))))

	;; Cleanup.
	(ignore-errors (delete-file tmp-name))))))

(defun tramp--test-operation (&optional _file)
  "Test operation."
  "Test operation")

(defun tramp--handle-test-operation (&optional _file)
  "Test operation handler."
  "Test operation handler")

(defun tramp--handle-process-id (process)
  "Handler for `process-id'."
  ;; Return something else.
  (1+ (tramp-run-real-handler #'process-id (list process))))

(defun tramp--test-operation-file-name-for-operation (_operation &optional _file)
  "Helper function for `tramp--test-operation' handler."
  default-directory)

(ert-deftest tramp-test49-external-backend-function ()
  "Check that Tramp handles external functions for a given backend."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-enabled))
  (skip-unless (not (tramp--test-ange-ftp-p)))

  (let* ((file-name-handler
	  (tramp-find-foreign-file-name-handler tramp-test-vec))
	 (backend
	  (intern
	   (string-remove-suffix
	    "-file-name-handler" (symbol-name file-name-handler)))))
    ;; Cleanup.
    (tramp-remove-external-operation #'tramp--test-operation backend)
    (tramp-remove-external-operation #'process-id backend)

    ;; There is no backend specific code.
    (should-not
     (string-equal (tramp--test-operation ert-remote-temporary-file-directory)
		   (tramp--handle-test-operation
		    ert-remote-temporary-file-directory)))
    (should-not
     (string-equal (tramp--test-operation temporary-file-directory)
		   (tramp--handle-test-operation
		    temporary-file-directory)))
    (let ((default-directory ert-remote-temporary-file-directory))
      (should-not
       (string-equal (tramp--test-operation)
		     (tramp--handle-test-operation))))
    (let ((default-directory temporary-file-directory))
      (should-not
       (string-equal (tramp--test-operation)
		     (tramp--handle-test-operation))))

    (should-error
     (tramp-add-external-operation
      #'tramp--test-operation #'tramp--handle-test-operation 'foo)
     :type 'file-missing)
    (should-error
     (tramp-add-external-operation
      #'tramp--test-operation #'tramp--handle-test-operation backend 'foo)
     :type 'remote-file-error)
    ;; This doesn't hurt.
    (tramp-add-external-operation
     #'tramp--test-operation #'tramp--handle-test-operation backend 'file)
    (should
     (eq #'tramp--handle-test-operation
	 (tramp-external-operation-p #'tramp--test-operation backend)))

    ;; The backend specific function is called.
    (should
     (string-equal (tramp--test-operation ert-remote-temporary-file-directory)
		   (tramp--handle-test-operation
		    ert-remote-temporary-file-directory)))
    (should-not
     (string-equal (tramp--test-operation temporary-file-directory)
		   (tramp--handle-test-operation
		    temporary-file-directory)))
    (let ((default-directory ert-remote-temporary-file-directory))
      (should
       (string-equal (tramp--test-operation)
		     (tramp--handle-test-operation)))
      (should
       (string-equal (tramp--test-operation "foo")
		     (tramp--handle-test-operation "foo"))))
    (let ((default-directory temporary-file-directory))
      (should-not
       (string-equal (tramp--test-operation)
		     (tramp--handle-test-operation)))
      (should-not
       (string-equal (tramp--test-operation "foo")
		     (tramp--handle-test-operation "foo"))))

    (tramp-remove-external-operation #'tramp--test-operation backend)
    ;; There is no backend specific code.
    (should-not (tramp-external-operation-p #'tramp--test-operation backend))
    (should-not
     (string-equal (tramp--test-operation ert-remote-temporary-file-directory)
		   (tramp--handle-test-operation
		    ert-remote-temporary-file-directory)))
    (should-not
     (string-equal (tramp--test-operation temporary-file-directory)
		   (tramp--handle-test-operation
		    temporary-file-directory)))
    (let ((default-directory ert-remote-temporary-file-directory))
      (should-not
       (string-equal (tramp--test-operation)
		     (tramp--handle-test-operation)))
      (should-not
       (string-equal (tramp--test-operation "foo")
		     (tramp--handle-test-operation "foo"))))
    (let ((default-directory temporary-file-directory))
      (should-not
       (string-equal (tramp--test-operation)
		     (tramp--handle-test-operation)))
      (should-not
       (string-equal (tramp--test-operation "foo")
		     (tramp--handle-test-operation "foo"))))

    ;; Test `default-directory' arg type.
    (tramp-add-external-operation
     #'tramp--test-operation #'tramp--handle-test-operation
     backend 'default-directory)
    (should
     (eq #'tramp--handle-test-operation
	 (tramp-external-operation-p #'tramp--test-operation backend)))

    ;; The backend specific function is called.
    (let ((default-directory ert-remote-temporary-file-directory))
      (should
       (string-equal (tramp--test-operation "foo")
		     (tramp--handle-test-operation "foo"))))
    (let ((default-directory temporary-file-directory))
      (should-not
       (string-equal (tramp--test-operation "foo")
		     (tramp--handle-test-operation "foo"))))

    (tramp-remove-external-operation #'tramp--test-operation backend)
    ;; There is no backend specific code.
    (should-not (tramp-external-operation-p #'tramp--test-operation backend))
    (let ((default-directory ert-remote-temporary-file-directory))
      (should-not
       (string-equal (tramp--test-operation)
		     (tramp--handle-test-operation)))
      (should-not
       (string-equal (tramp--test-operation "foo")
		     (tramp--handle-test-operation "foo"))))
    (let ((default-directory temporary-file-directory))
      (should-not
       (string-equal (tramp--test-operation)
		     (tramp--handle-test-operation)))
      (should-not
       (string-equal (tramp--test-operation "foo")
		     (tramp--handle-test-operation "foo"))))

    ;; Test `process' arg type.
    (when (and (tramp--test-supports-processes-p) (not (tramp--test-smb-p)))
      (let ((default-directory ert-remote-temporary-file-directory)
	    proc command id)
	(unwind-protect
	    (with-temp-buffer
	      (setq command '("cat")
		    proc
		    (make-process
		     :name "test" :buffer (current-buffer) :command command
		     :file-handler t))
	      (should (processp proc))
	      (should (eq (process-status proc) 'run))
	      (should (natnump (setq id (process-id proc))))
	      (tramp-add-external-operation
	       #'process-id #'tramp--handle-process-id backend 'process)
	      (should
	       (eq #'tramp--handle-process-id
		   (tramp-external-operation-p #'process-id backend)))
	      (should (= (process-id proc) (1+ id))))

	  ;; Cleanup.
	  (tramp-remove-external-operation #'process-id backend)
	  (should-not (tramp-external-operation-p #'process-id backend))
	  (ignore-errors (delete-process proc)))))

    ;; Test `tramp-file-name' arg type.
    (tramp-add-external-operation
     #'tramp--test-operation #'tramp--handle-test-operation
     backend 'tramp-file-name)
    (should
     (eq #'tramp--handle-test-operation
	 (tramp-external-operation-p #'tramp--test-operation backend)))

    ;; The backend specific function is called.
    (should
     (string-equal (tramp--test-operation tramp-test-vec)
		   (tramp--handle-test-operation tramp-test-vec)))
    (let ((vec (copy-tramp-file-name tramp-test-vec))
	  ;; This is needed for the `simplified' syntax.
	  (tramp-default-method (if (tramp--test-sh-p) "rclone" "sudo"))
	  ;; "rclone" is not multi-hop capable.
	  (tramp-multi-hop-p-hook #'always))
      (setf (tramp-file-name-method vec) tramp-default-method)
      (should-not
       (string-equal (tramp--test-operation vec)
		     (tramp--handle-test-operation vec))))

    (tramp-remove-external-operation #'tramp--test-operation backend)
    ;; There is no backend specific code.
    (should-not (tramp-external-operation-p #'tramp--test-operation backend))
    (should-not
     (string-equal (tramp--test-operation tramp-test-vec)
		   (tramp--handle-test-operation tramp-test-vec)))

    ;; Test function arg type.
    (tramp-add-external-operation
     #'tramp--test-operation #'tramp--handle-test-operation
     backend #'tramp--test-operation-file-name-for-operation)
    (should
     (eq #'tramp--handle-test-operation
	 (tramp-external-operation-p #'tramp--test-operation backend)))

    ;; The backend specific function is called.
    (let ((default-directory ert-remote-temporary-file-directory))
      (should
       (string-equal (tramp--test-operation "foo")
		     (tramp--handle-test-operation "foo"))))
    (let ((default-directory temporary-file-directory))
      (should-not
       (string-equal (tramp--test-operation "foo")
		     (tramp--handle-test-operation "foo"))))

    (tramp-remove-external-operation #'tramp--test-operation backend)
    ;; There is no backend specific code.
    (should-not (tramp-external-operation-p #'tramp--test-operation backend))
    (let ((default-directory ert-remote-temporary-file-directory))
      (should-not
       (string-equal (tramp--test-operation)
		     (tramp--handle-test-operation)))
      (should-not
       (string-equal (tramp--test-operation "foo")
		     (tramp--handle-test-operation "foo"))))
    (let ((default-directory temporary-file-directory))
      (should-not
       (string-equal (tramp--test-operation)
		     (tramp--handle-test-operation)))
      (should-not
       (string-equal (tramp--test-operation "foo")
		     (tramp--handle-test-operation "foo"))))))

;; This test is inspired by Bug#29163.
(ert-deftest tramp-test50-auto-load ()
  "Check that Tramp autoloads properly."
  ;; If we use another syntax but `default', Tramp is already loaded
  ;; due to the `tramp-change-syntax' call.
  (skip-unless (eq tramp-syntax 'default))
  (skip-unless (tramp--test-enabled))

  (let ((default-directory (expand-file-name temporary-file-directory))
	(code
	 (format
	  ;; Suppress method name check.
	  "(let ((non-essential t)) \
             (message \"Tramp loaded: %%s\" (and (file-remote-p %S) t)))"
	  ert-remote-temporary-file-directory)))
    (should
     (string-match-p
      (rx "Tramp loaded: t" (+ (any "\r\n")))
      (shell-command-to-string
       (format
	"%s -batch -Q -L %s --eval %s"
	(shell-quote-argument
	 (expand-file-name invocation-name invocation-directory))
	(mapconcat #'shell-quote-argument load-path " -L ")
	(shell-quote-argument code)))))))

(ert-deftest tramp-test50-delay-load ()
  "Check that Tramp is loaded lazily, only when needed."
  ;; Tramp is neither loaded at Emacs startup, nor when completing a
  ;; non-Tramp file name like "/foo".  Completing a Tramp-alike file
  ;; name like "/foo:" autoloads Tramp, when `tramp-mode' is t.
  (let ((default-directory (expand-file-name temporary-file-directory))
	(code
	 "(progn \
           (setq tramp-mode %s) \
	   (message \"Tramp loaded: %%s\" (featurep 'tramp)) \
	   (file-name-all-completions \"/foo\" \"/\") \
	   (message \"Tramp loaded: %%s\" (featurep 'tramp)) \
	   (file-name-all-completions \"/foo:\" \"/\") \
	   (message \"Tramp loaded: %%s\" (featurep 'tramp)))"))
    ;; Tramp doesn't load when `tramp-mode' is nil.
    (dolist (tm '(t nil))
      (should
       (string-match-p
	(rx
	 "Tramp loaded: nil" (+ (any "\r\n"))
	 "Tramp loaded: nil" (+ (any "\r\n"))
	 "Tramp loaded: " (literal (symbol-name tm)) (+ (any "\r\n")))
	(shell-command-to-string
	 (format
	  "%s -batch -Q -L %s --eval %s"
	  (shell-quote-argument
	   (expand-file-name invocation-name invocation-directory))
	  (mapconcat #'shell-quote-argument load-path " -L ")
	  (shell-quote-argument (format code tm)))))))))

(ert-deftest tramp-test50-recursive-load ()
  "Check that Tramp does not fail due to recursive load."
  :tags '(:expensive-test)
  (skip-unless (tramp--test-enabled))

  (let ((default-directory (expand-file-name temporary-file-directory)))
    (dolist (code
	     (list
	      (format
	       "(expand-file-name %S)" ert-remote-temporary-file-directory)
	      (format
	       "(let ((default-directory %S)) (expand-file-name %S))"
	       ert-remote-temporary-file-directory
	       temporary-file-directory)))
      (should-not
       (string-match-p
	"Recursive load"
	(shell-command-to-string
	 (format
	  "%s -batch -Q -L %s --eval %s"
	  (shell-quote-argument
	   (expand-file-name invocation-name invocation-directory))
	  (mapconcat #'shell-quote-argument load-path " -L ")
	  (shell-quote-argument code))))))))

(ert-deftest tramp-test50-remote-load-path ()
  "Check that Tramp autoloads its packages with remote `load-path'."
  ;; `tramp-cleanup-all-connections' is autoloaded from tramp-cmds.el.
  ;; It shall still work, when a remote file name is in the
  ;; `load-path'.
  (let ((default-directory (expand-file-name temporary-file-directory))
	(code
	 "(let ((force-load-messages t) \
		(load-path (cons \"/foo:bar:\" load-path))) \
	    (tramp-cleanup-all-connections))"))
    (should
     (string-match-p
      (rx
       "Loading "
       (literal
        (expand-file-name
         "tramp-cmds" (file-name-directory (locate-library "tramp")))))
      (shell-command-to-string
       (format
	"%s -batch -Q -L %s -l tramp-sh --eval %s"
	(shell-quote-argument
	 (expand-file-name invocation-name invocation-directory))
	(mapconcat #'shell-quote-argument load-path " -L ")
	(shell-quote-argument code)))))))

(ert-deftest tramp-test51-without-remote-files ()
  "Check that Tramp can be suppressed."
  (skip-unless (tramp--test-enabled))

  (should (file-remote-p ert-remote-temporary-file-directory))
  (should-not
   (without-remote-files (file-remote-p ert-remote-temporary-file-directory)))
  (should (file-remote-p ert-remote-temporary-file-directory))

  (inhibit-remote-files)
  (should-not (file-remote-p ert-remote-temporary-file-directory))
  (tramp-register-file-name-handlers)
  (setq tramp-mode t)
  (should (file-remote-p ert-remote-temporary-file-directory)))

(ert-deftest tramp-test52-unload ()
  "Check that Tramp and its subpackages unload completely.
Since it unloads Tramp, it shall be the last test to run."
  :tags '(:expensive-test)
  ;; We have autoloaded objects from tramp.el and tramp-archive.el.
  ;; In order to remove them, we first need to load both packages.
  (require 'tramp)
  (require 'tramp-archive)
  (should (featurep 'tramp))
  (should (featurep 'tramp-archive))
  ;; This unloads also tramp-archive.el and tramp-theme.el if needed.
  (unload-feature 'tramp 'force)

  ;; No Tramp feature must be left except the test packages.
  (should-not (featurep 'tramp))
  (should-not (featurep 'tramp-archive))
  (should-not (featurep 'tramp-theme))
  (should-not
   (all-completions
    "tramp" (delq 'tramp-tests (delq 'tramp-archive-tests features))))

  ;; `file-name-handler-alist' must be clean.
  (should-not (all-completions "tramp" (mapcar #'cdr file-name-handler-alist)))

  ;; There shouldn't be left a bound symbol, except buffer-local
  ;; variables, and autoloaded functions.  We do not regard our test
  ;; symbols, and the Tramp unload hooks.
  (mapatoms
   (lambda (x)
     (and (or (and (boundp x) (null (local-variable-if-set-p x)))
	      (and (functionp x) (null (autoloadp (symbol-function x))))
	      (macrop x))
	  (string-prefix-p "tramp" (symbol-name x))
	  (string-match-p (rx bol "with" (| "tramp" "parsed")) (symbol-name x))
	  ;; `tramp-register-archive-file-name-handler' is autoloaded
	  ;; in Emacs < 29.1.
	  (not (eq 'tramp-register-archive-file-name-handler x))
	  ;; `tramp-compat-rx' is autoloaded in Emacs 29.1.
	  (not (eq 'tramp-compat-rx x))
	  (not (string-match-p
		(rx bol "tramp" (? "-archive") (** 1 2 "-") "test")
		(symbol-name x)))
	  (not (string-suffix-p "unload-hook" (symbol-name x)))
	  (not (get x 'tramp-autoload))
	  (ert-fail (format "`%s' still bound" x)))))

  ;; The defstruct `tramp-file-name' and all its internal functions
  ;; shall be purged.
  (should-not (cl--find-class 'tramp-file-name))
  (mapatoms
   (lambda (x)
     (and (functionp x) (null (autoloadp (symbol-function x)))
          (string-prefix-p "tramp-file-name" (symbol-name x))
          (ert-fail (format "Structure function `%s' still exists" x)))))

  ;; There shouldn't be left a hook function containing a Tramp
  ;; function.  We do not regard the Tramp unload hooks.
  (mapatoms
   (lambda (x)
     (and (boundp x)
	  (string-match-p
	   (rx "-" (| "hook" "function") (? "s") eol) (symbol-name x))
	  (not (string-suffix-p "unload-hook" (symbol-name x)))
	  (consp (symbol-value x))
	  (ignore-errors (all-completions "tramp" (symbol-value x)))
	  (ert-fail (format "Hook `%s' still contains Tramp function" x)))))

  ;; There shouldn't be left an advice function from Tramp.
  (mapatoms
   (lambda (x)
     (and (functionp x)
	  (advice-mapc
	   (lambda (fun _symbol)
	     (and (string-prefix-p "tramp" (symbol-name fun))
		  (ert-fail
		   (format "Function `%s' still contains Tramp advice" x))))
	   x))))

  ;; Reload.
  (require 'tramp)
  (require 'tramp-archive)
  (should (featurep 'tramp))
  (should (featurep 'tramp-archive))

  ;; Disabled further tests.
  (setq tramp--test-enabled-checked '(t)))

(defun tramp-test-all (&optional interactive)
  "Run all tests for \\[tramp].
If INTERACTIVE is non-nil, the tests are run interactively."
  (interactive "p")
  (funcall
   (if interactive #'ert-run-tests-interactively #'ert-run-tests-batch)
   (rx bol "tramp")))

;; TODO:

;; * dired-uncache (partly done in other test functions)
;; * file-equal-p (partly done in `tramp-test21-file-links')
;; * file-in-directory-p
;; * file-name-case-insensitive-p
;; * tramp-get-home-directory
;; * tramp-set-file-uid-gid

;; * Work on skipped tests.  Make a comment, when it is impossible.
;; * Revisit expensive tests, once problems in `tramp-error' are solved.
;; * Fix `tramp-test06-directory-file-name' for "ftp".
;; * In `tramp-test26-file-name-completion', check also user, domain,
;;   port and hop.
;; * In `tramp-test26-interactive-file-name-completion', should
;;   `completion-pcm--delim-wild-regex' be bound?  Check also domain and port.
;; * Check, why a process filter t doesn't work in
;;   `tramp-test29-start-file-process' and
;;   `tramp-test30-make-process'.
;; * Implement `tramp-test31-interrupt-process' and
;;   `tramp-test31-signal-process' for "adb", "sshfs" and for direct
;;   async processes.  Check, why they don't run stable.
;; * Fix the limitations for "smb" in `tramp--test-check-files'.
;; * Check, why `tramp-test45-asynchronous-requests' often fails.  The
;;   famous reentrant error?
;; * Check, why direct async processes do not work for
;;   `tramp-test45-asynchronous-requests'.

;; Use `skip-when' starting with Emacs 30.1.

(provide 'tramp-tests)

;;; tramp-tests.el ends here
