aboutsummaryrefslogtreecommitdiff
path: root/tests/test-duet-classify-path.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-06 10:37:59 -0500
committerCraig Jennings <c@cjennings.net>2026-06-06 10:37:59 -0500
commit04f9eb281529965c4aff9ca9176b549fac4ae30f (patch)
treeaf3dcba1f2a0c26e6e80a31225db33c3d005b3c7 /tests/test-duet-classify-path.el
parent95dbb5abdbb746cf5da9f7926740d17205ac8d55 (diff)
downloadduet-04f9eb281529965c4aff9ca9176b549fac4ae30f.tar.gz
duet-04f9eb281529965c4aff9ca9176b549fac4ae30f.zip
feat: add duet--classify-path
duet--classify-path turns a path string into a plist describing where it lives and how to reach it: :locality (local or remote), :method, :user, :host, :port, :localname, and :hop for multi-hop paths. It's the pure foundation the transfer-spec and connection layers build on (Phase 1 in the design spec). TRAMP does the dissection, so any path file-remote-p recognizes is remote and everything else is local. A local path is expanded, so a leading ~ resolves to the home directory. A remote localname is kept verbatim because a ~ there is the remote home, not this machine's. Classification never signals: an incomplete string like /ssh:host that TRAMP rejects as a remote name falls back to local, since validating raw TRAMP input belongs to the connection reader, not here. I probed TRAMP's real contract before writing the tests (port comes back as a string, a multi-hop path reports the final host with the leading hops in :hop), so the Normal/Boundary/Error cases assert what TRAMP actually returns rather than what I'd have guessed.
Diffstat (limited to 'tests/test-duet-classify-path.el')
-rw-r--r--tests/test-duet-classify-path.el105
1 files changed, 105 insertions, 0 deletions
diff --git a/tests/test-duet-classify-path.el b/tests/test-duet-classify-path.el
new file mode 100644
index 0000000..b9e8264
--- /dev/null
+++ b/tests/test-duet-classify-path.el
@@ -0,0 +1,105 @@
+;;; test-duet-classify-path.el --- Tests for duet--classify-path -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Craig Jennings
+
+;; Author: Craig Jennings <c@cjennings.net>
+
+;; This program 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.
+
+;; This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Normal/Boundary/Error coverage for `duet--classify-path', the pure path
+;; classifier that turns a path string into a `:locality'/`:method'/`:user'/
+;; `:host'/`:port'/`:localname'/`:hop' plist. TRAMP owns the dissection; these
+;; tests verify the locality decision, the extracted fields, and graceful
+;; handling of paths TRAMP does not recognize as remote.
+
+;;; Code:
+
+(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
+
+;;; Normal cases
+
+(ert-deftest test-duet-classify-path-local-absolute ()
+ "An absolute local path classifies as local with no remote fields."
+ (let ((p (duet--classify-path "/home/cjennings/file")))
+ (should (eq 'local (plist-get p :locality)))
+ (should (null (plist-get p :method)))
+ (should (null (plist-get p :user)))
+ (should (null (plist-get p :host)))
+ (should (null (plist-get p :port)))
+ (should (null (plist-get p :hop)))
+ (should (equal "/home/cjennings/file" (plist-get p :localname)))))
+
+(ert-deftest test-duet-classify-path-ssh-host-only ()
+ "A remote ssh path with no user fills method/host/localname, user nil."
+ (let ((p (duet--classify-path "/ssh:host:/path")))
+ (should (eq 'remote (plist-get p :locality)))
+ (should (equal "ssh" (plist-get p :method)))
+ (should (null (plist-get p :user)))
+ (should (equal "host" (plist-get p :host)))
+ (should (null (plist-get p :port)))
+ (should (equal "/path" (plist-get p :localname)))))
+
+(ert-deftest test-duet-classify-path-ssh-user-host ()
+ "A user@host ssh path extracts the user."
+ (let ((p (duet--classify-path "/ssh:user@host:/path")))
+ (should (eq 'remote (plist-get p :locality)))
+ (should (equal "user" (plist-get p :user)))
+ (should (equal "host" (plist-get p :host)))
+ (should (equal "/path" (plist-get p :localname)))))
+
+;;; Boundary cases
+
+(ert-deftest test-duet-classify-path-tilde-expands ()
+ "A leading ~ in a local path expands to the home directory."
+ (let ((p (duet--classify-path "~/media")))
+ (should (eq 'local (plist-get p :locality)))
+ (should (equal (expand-file-name "~/media") (plist-get p :localname)))))
+
+(ert-deftest test-duet-classify-path-host-with-port ()
+ "A host#port path splits the port out into the :port field."
+ (let ((p (duet--classify-path "/ssh:user@host#2222:/path")))
+ (should (eq 'remote (plist-get p :locality)))
+ (should (equal "host" (plist-get p :host)))
+ (should (equal "2222" (plist-get p :port)))
+ (should (equal "/path" (plist-get p :localname)))))
+
+(ert-deftest test-duet-classify-path-sshx-and-scp-methods ()
+ "Methods other than ssh are preserved verbatim."
+ (should (equal "sshx" (plist-get (duet--classify-path "/sshx:u@h:/p") :method)))
+ (should (equal "scp" (plist-get (duet--classify-path "/scp:u@h:/p") :method))))
+
+(ert-deftest test-duet-classify-path-multi-hop ()
+ "A multi-hop path reports the final host and preserves the leading hops."
+ (let ((p (duet--classify-path "/ssh:host1|ssh:host2:/path")))
+ (should (eq 'remote (plist-get p :locality)))
+ (should (equal "host2" (plist-get p :host)))
+ (should (equal "/path" (plist-get p :localname)))
+ (should (stringp (plist-get p :hop)))
+ (should (string-match-p "host1" (plist-get p :hop)))))
+
+;;; Error / edge cases
+
+(ert-deftest test-duet-classify-path-malformed-is-local-not-error ()
+ "A TRAMP-looking string TRAMP does not accept as remote is treated as local.
+Validation of raw TRAMP entry belongs to the connection reader, not the
+classifier; classification stays total and never throws."
+ (dolist (bad '("/ssh:" "/ssh:host"))
+ (let ((p (duet--classify-path bad)))
+ (should (eq 'local (plist-get p :locality)))
+ (should (null (plist-get p :method))))))
+
+(provide 'test-duet-classify-path)
+;;; test-duet-classify-path.el ends here