diff options
Diffstat (limited to 'tests/test-integration-duet-transfer.el')
| -rw-r--r-- | tests/test-integration-duet-transfer.el | 168 |
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 |
