diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-06 10:37:59 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-06 10:37:59 -0500 |
| commit | 04f9eb281529965c4aff9ca9176b549fac4ae30f (patch) | |
| tree | af3dcba1f2a0c26e6e80a31225db33c3d005b3c7 /tests/test-duet-classify-path.el | |
| parent | 95dbb5abdbb746cf5da9f7926740d17205ac8d55 (diff) | |
| download | duet-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.el | 105 |
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 |
