aboutsummaryrefslogtreecommitdiff
path: root/tests/test-integration-duet-transfer.el
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test-integration-duet-transfer.el')
-rw-r--r--tests/test-integration-duet-transfer.el168
1 files changed, 168 insertions, 0 deletions
diff --git a/tests/test-integration-duet-transfer.el b/tests/test-integration-duet-transfer.el
new file mode 100644
index 0000000..3b7bcf6
--- /dev/null
+++ b/tests/test-integration-duet-transfer.el
@@ -0,0 +1,168 @@
+;;; test-integration-duet-transfer.el --- Real-rsync transfer integration -*- 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:
+
+;; Integration tests that exercise the whole transfer engine against a real
+;; local rsync on temporary directories: file and directory copies, the
+;; delete-after-success move, and the failed-move-preserves-source guarantee.
+;;
+;; Components integrated:
+;; - duet--run-transfer / duet--launch-process (real make-process)
+;; - rsync (REAL, local, version-independent flags)
+;; - duet--transfer-sentinel / duet--process-result / duet--transfer-handle-result
+;; - duet--finalize-move (real delete on success)
+;;
+;; Tagged :slow, so `make test' skips them and `make test-all' runs them.
+
+;;; Code:
+
+(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
+
+(defmacro duet-itest--with-clean-engine (&rest body)
+ "Run BODY with a fresh engine and the real launcher, cancelling timers after."
+ (declare (indent 0))
+ `(let ((duet--transfer-id-counter 0)
+ (duet--transfers nil)
+ (duet--transfer-queue nil)
+ (duet--log-render-timer nil)
+ (duet--refresh-pending nil)
+ (duet--refresh-timer nil)
+ (duet-transfer-stall-timeout nil)
+ (duet-max-concurrent-transfers 1))
+ (unwind-protect (progn ,@body)
+ (when (timerp duet--log-render-timer) (cancel-timer duet--log-render-timer))
+ (when (timerp duet--refresh-timer) (cancel-timer duet--refresh-timer)))))
+
+(defun duet-itest--wait (tr &optional timeout)
+ "Block until transfer TR reaches a terminal status or TIMEOUT seconds pass."
+ (let ((deadline (+ (float-time) (or timeout 15))))
+ (while (and (not (memq (duet-transfer-status tr) duet--terminal-statuses))
+ (< (float-time) deadline))
+ (accept-process-output (duet-transfer-process tr) 0.05))
+ (duet-transfer-status tr)))
+
+(defun duet-itest--tmpdir ()
+ "Create and return a fresh temporary directory."
+ (file-name-as-directory (make-temp-file "duet-itest-" t)))
+
+(defun duet-itest--write (path contents)
+ "Write CONTENTS into PATH, creating parent directories."
+ (make-directory (file-name-directory path) t)
+ (with-temp-file path (insert contents)))
+
+(ert-deftest test-integration-duet-rsync-copies-file ()
+ :tags '(:slow)
+ (duet-itest--with-clean-engine
+ (let* ((src (duet-itest--tmpdir))
+ (dst (duet-itest--tmpdir))
+ (file (expand-file-name "hello.txt" src)))
+ (unwind-protect
+ (progn
+ (duet-itest--write file "payload\n")
+ (let ((tr (duet--run-transfer
+ (duet--transfer-spec (list file) dst))))
+ (should (eq 'done (duet-itest--wait tr)))
+ (should (= 0 (duet-transfer-exit tr)))
+ (should (file-exists-p (expand-file-name "hello.txt" dst)))
+ (should (duet-transfer-cleanup-verified tr))))
+ (delete-directory src t)
+ (delete-directory dst t)))))
+
+(ert-deftest test-integration-duet-rsync-copies-directory ()
+ :tags '(:slow)
+ (duet-itest--with-clean-engine
+ (let* ((root (duet-itest--tmpdir))
+ (dst (duet-itest--tmpdir))
+ (tree (expand-file-name "tree" root)))
+ (unwind-protect
+ (progn
+ (duet-itest--write (expand-file-name "a/one.txt" tree) "1")
+ (duet-itest--write (expand-file-name "b/two.txt" tree) "2")
+ (let ((tr (duet--run-transfer
+ (duet--transfer-spec (list tree) dst))))
+ (should (eq 'done (duet-itest--wait tr)))
+ (should (file-exists-p (expand-file-name "tree/a/one.txt" dst)))
+ (should (file-exists-p (expand-file-name "tree/b/two.txt" dst)))))
+ (delete-directory root t)
+ (delete-directory dst t)))))
+
+(ert-deftest test-integration-duet-move-deletes-source-on-success ()
+ :tags '(:slow)
+ (duet-itest--with-clean-engine
+ (let* ((src (duet-itest--tmpdir))
+ (dst (duet-itest--tmpdir))
+ (file (expand-file-name "movable.txt" src)))
+ (unwind-protect
+ (progn
+ (duet-itest--write file "move me\n")
+ (let ((tr (duet--run-transfer
+ (duet--transfer-spec (list file) dst) t)))
+ (should (eq 'done (duet-itest--wait tr)))
+ (should (file-exists-p (expand-file-name "movable.txt" dst)))
+ (should-not (file-exists-p file))))
+ (delete-directory src t)
+ (delete-directory dst t)))))
+
+(ert-deftest test-integration-duet-failed-move-preserves-source ()
+ :tags '(:slow)
+ (when (zerop (user-uid))
+ (ert-skip "root bypasses directory permissions"))
+ (duet-itest--with-clean-engine
+ (let* ((src (duet-itest--tmpdir))
+ (dst (duet-itest--tmpdir))
+ (file (expand-file-name "keep.txt" src)))
+ (unwind-protect
+ (progn
+ (duet-itest--write file "must survive\n")
+ ;; Read+execute only: rsync cannot create files in the destination.
+ (set-file-modes dst #o500)
+ (let ((tr (duet--run-transfer
+ (duet--transfer-spec (list file) dst) t)))
+ (should (eq 'failed (duet-itest--wait tr)))
+ (should (/= 0 (duet-transfer-exit tr)))
+ ;; The copy failed, so the move never deleted the source.
+ (should (file-exists-p file))))
+ (set-file-modes dst #o700)
+ (delete-directory src t)
+ (delete-directory dst t)))))
+
+(ert-deftest test-integration-duet-kill-process-terminates-live ()
+ :tags '(:slow)
+ (duet-itest--with-clean-engine
+ (let* ((proc (make-process :name "duet-kill-test"
+ :command '("sleep" "30") :noquery t))
+ (tr (duet-transfer-create :id 1 :process proc)))
+ (should (process-live-p proc))
+ (duet--kill-process tr)
+ (should-not (process-live-p proc)))))
+
+(ert-deftest test-integration-duet-process-result-classifies-signal ()
+ :tags '(:slow)
+ (let ((proc (make-process :name "duet-signal-test"
+ :command '("sleep" "30") :noquery t)))
+ (interrupt-process proc)
+ (let ((deadline (+ (float-time) 5)))
+ (while (and (process-live-p proc) (< (float-time) deadline))
+ (accept-process-output proc 0.05)))
+ (should (eq 'signal (process-status proc)))
+ (should (eq :signal (car (duet--process-result proc))))))
+
+(provide 'test-integration-duet-transfer)
+;;; test-integration-duet-transfer.el ends here